Introduce multi-cursor inline transformations (#13368)

https://github.com/zed-industries/zed/assets/482957/591def34-e5c8-4402-9c6b-372cbca720c3

Release Notes:

- N/A

---------

Co-authored-by: Richard Feldman <oss@rtfeldman.com>
This commit is contained in:
Antonio Scandurra 2024-06-21 17:41:43 +02:00 committed by GitHub
parent c58a8f1a04
commit cb0b8b4c4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 1335 additions and 698 deletions

10
Cargo.lock generated
View File

@ -362,6 +362,7 @@ dependencies = [
"anthropic",
"anyhow",
"assistant_slash_command",
"async-watch",
"cargo_toml",
"chrono",
"client",
@ -873,6 +874,15 @@ dependencies = [
"tungstenite 0.16.0",
]
[[package]]
name = "async-watch"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a078faf4e27c0c6cc0efb20e5da59dcccc04968ebf2801d8e0b2195124cdcdb2"
dependencies = [
"event-listener 2.5.3",
]
[[package]]
name = "async_zip"
version = "0.0.17"

View File

@ -16,6 +16,7 @@ doctest = false
anyhow.workspace = true
anthropic = { workspace = true, features = ["schemars"] }
assistant_slash_command.workspace = true
async-watch.workspace = true
cargo_toml.workspace = true
chrono.workspace = true
client.workspace = true

File diff suppressed because it is too large Load Diff

View File

@ -33,35 +33,32 @@ pub fn generate_content_prompt(
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
writeln!(
prompt,
"The user has the following file open in the editor:"
)?;
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
write!(prompt, "```")?;
if let Some(language_name) = language_name {
write!(prompt, "{language_name}")?;
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
for chunk in buffer.as_rope().chunks_in_range(0..range.start) {
prompt.push_str(chunk);
}
prompt.push_str("<|CURSOR|>");
for chunk in buffer.as_rope().chunks_in_range(range.start..buffer.len()) {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
"Assume the cursor is located where the `<|CURSOR|>` span is."
)
.unwrap();
writeln!(
@ -75,11 +72,42 @@ pub fn generate_content_prompt(
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
write!(prompt, "```")?;
for chunk in buffer.as_rope().chunks() {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
"In particular, the following piece of text is selected:"
)?;
write!(prompt, "```")?;
if let Some(language_name) = language_name {
write!(prompt, "{language_name}")?;
}
prompt.push('\n');
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !prompt.ends_with('\n') {
prompt.push('\n');
}
writeln!(prompt, "```")?;
prompt.push('\n');
writeln!(
prompt,
"Modify the user's selected {content_type} based upon the users prompt: {user_prompt}"
)
.unwrap();
writeln!(
prompt,
"You must reply with only the adjusted {content_type}, not the entire file."
)
.unwrap();
}

View File

@ -1204,7 +1204,7 @@ async fn test_share_project(
buffer_a.read_with(cx_a, |buffer, _| {
buffer
.snapshot()
.remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
.count()
== 1
});
@ -1245,7 +1245,7 @@ async fn test_share_project(
buffer_a.read_with(cx_a, |buffer, _| {
buffer
.snapshot()
.remote_selections_in_range(text::Anchor::MIN..text::Anchor::MAX)
.selections_in_range(text::Anchor::MIN..text::Anchor::MAX, false)
.count()
== 0
});

View File

@ -137,7 +137,7 @@ impl ProjectDiagnosticsEditor {
this.summary = project.read(cx).diagnostic_summary(false, cx);
cx.emit(EditorEvent::TitleChanged);
if this.editor.read(cx).is_focused(cx) || this.focus_handle.is_focused(cx) {
if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts");

View File

@ -169,7 +169,7 @@ impl DisplayMap {
let (wrap_snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits);
let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits).snapshot;
DisplaySnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(cx),
@ -348,6 +348,25 @@ impl DisplayMap {
block_map.remove(ids);
}
pub fn row_for_block(
&mut self,
block_id: BlockId,
cx: &mut ModelContext<Self>,
) -> Option<DisplayRow> {
let snapshot = self.buffer.read(cx).snapshot(cx);
let edits = self.buffer_subscription.consume().into_inner();
let tab_size = Self::tab_size(&self.buffer, cx);
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
let (snapshot, edits) = self
.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let block_map = self.block_map.read(snapshot, edits);
let block_row = block_map.row_for_block(block_id)?;
Some(DisplayRow(block_row.0))
}
pub fn highlight_text(
&mut self,
type_id: TypeId,

View File

@ -37,6 +37,11 @@ pub struct BlockMap {
excerpt_footer_height: u8,
}
pub struct BlockMapReader<'a> {
blocks: &'a Vec<Arc<Block>>,
pub snapshot: BlockSnapshot,
}
pub struct BlockMapWriter<'a>(&'a mut BlockMap);
#[derive(Clone)]
@ -246,12 +251,15 @@ impl BlockMap {
map
}
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockSnapshot {
pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapReader {
self.sync(&wrap_snapshot, edits);
*self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
BlockSnapshot {
wrap_snapshot,
transforms: self.transforms.borrow().clone(),
BlockMapReader {
blocks: &self.blocks,
snapshot: BlockSnapshot {
wrap_snapshot,
transforms: self.transforms.borrow().clone(),
},
}
}
@ -606,6 +614,62 @@ impl std::ops::DerefMut for BlockPoint {
}
}
impl<'a> Deref for BlockMapReader<'a> {
type Target = BlockSnapshot;
fn deref(&self) -> &Self::Target {
&self.snapshot
}
}
impl<'a> DerefMut for BlockMapReader<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.snapshot
}
}
impl<'a> BlockMapReader<'a> {
pub fn row_for_block(&self, block_id: BlockId) -> Option<BlockRow> {
let block = self.blocks.iter().find(|block| block.id == block_id)?;
let buffer_row = block
.position
.to_point(self.wrap_snapshot.buffer_snapshot())
.row;
let wrap_row = self
.wrap_snapshot
.make_wrap_point(Point::new(buffer_row, 0), Bias::Left)
.row();
let start_wrap_row = WrapRow(
self.wrap_snapshot
.prev_row_boundary(WrapPoint::new(wrap_row, 0)),
);
let end_wrap_row = WrapRow(
self.wrap_snapshot
.next_row_boundary(WrapPoint::new(wrap_row, 0))
.unwrap_or(self.wrap_snapshot.max_point().row() + 1),
);
let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>();
cursor.seek(&start_wrap_row, Bias::Left, &());
while let Some(transform) = cursor.item() {
if cursor.start().0 > end_wrap_row {
break;
}
if let Some(BlockType::Custom(id)) =
transform.block.as_ref().map(|block| block.block_type())
{
if id == block_id {
return Some(cursor.start().1);
}
}
cursor.next(&());
}
None
}
}
impl<'a> BlockMapWriter<'a> {
pub fn insert(
&mut self,
@ -1784,6 +1848,15 @@ mod tests {
expected_block_positions
);
for (block_row, block) in expected_block_positions {
if let BlockType::Custom(block_id) = block.block_type() {
assert_eq!(
blocks_snapshot.row_for_block(block_id),
Some(BlockRow(block_row))
);
}
}
let mut expected_longest_rows = Vec::new();
let mut longest_line_len = -1_isize;
for (row, line) in expected_lines.iter().enumerate() {

View File

@ -457,6 +457,9 @@ pub struct Editor {
pub display_map: Model<DisplayMap>,
pub selections: SelectionsCollection,
pub scroll_manager: ScrollManager,
/// When inline assist editors are linked, they all render cursors because
/// typing enters text into each of them, even the ones that aren't focused.
pub(crate) show_cursor_when_unfocused: bool,
columnar_selection_tail: Option<Anchor>,
add_selections_state: Option<AddSelectionsState>,
select_next_state: Option<SelectNextState>,
@ -1635,7 +1638,7 @@ impl Editor {
clone
}
fn new(
pub fn new(
mode: EditorMode,
buffer: Model<MultiBuffer>,
project: Option<Model<Project>>,
@ -1752,6 +1755,7 @@ impl Editor {
let mut this = Self {
focus_handle,
show_cursor_when_unfocused: false,
last_focused_descendant: None,
buffer: buffer.clone(),
display_map: display_map.clone(),
@ -2220,7 +2224,7 @@ impl Editor {
// Copy selections to primary selection buffer
#[cfg(target_os = "linux")]
if local {
let selections = &self.selections.disjoint;
let selections = self.selections.all::<usize>(cx);
let buffer_handle = self.buffer.read(cx).read(cx);
let mut text = String::new();
@ -9964,6 +9968,15 @@ impl Editor {
}
}
pub fn row_for_block(
&self,
block_id: BlockId,
cx: &mut ViewContext<Self>,
) -> Option<DisplayRow> {
self.display_map
.update(cx, |map, cx| map.row_for_block(block_id, cx))
}
pub fn insert_creases(
&mut self,
creases: impl IntoIterator<Item = Crease>,
@ -10902,6 +10915,11 @@ impl Editor {
&& self.focus_handle.is_focused(cx)
}
pub fn set_show_cursor_when_unfocused(&mut self, is_enabled: bool, cx: &mut ViewContext<Self>) {
self.show_cursor_when_unfocused = is_enabled;
cx.notify();
}
fn on_buffer_changed(&mut self, _: Model<MultiBuffer>, cx: &mut ViewContext<Self>) {
cx.notify();
}
@ -11722,7 +11740,7 @@ impl EditorSnapshot {
.map(|(_, collaborator)| (collaborator.replica_id, collaborator))
.collect::<HashMap<_, _>>();
self.buffer_snapshot
.remote_selections_in_range(range)
.selections_in_range(range, false)
.filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
let collaborator = collaborators_by_replica_id.get(&replica_id)?;
let participant_index = participant_indices.get(&collaborator.user_id).copied();

View File

@ -859,6 +859,28 @@ impl EditorElement {
}
selections.extend(remote_selections.into_values());
} else if !editor.is_focused(cx) && editor.show_cursor_when_unfocused {
let player = if editor.read_only(cx) {
cx.theme().players().read_only()
} else {
self.style.local_player
};
let layouts = snapshot
.buffer_snapshot
.selections_in_range(&(start_anchor..end_anchor), true)
.map(move |(_, line_mode, cursor_shape, selection)| {
SelectionLayout::new(
selection,
line_mode,
cursor_shape,
&snapshot.display_snapshot,
false,
false,
None,
)
})
.collect::<Vec<_>>();
selections.push((player, layouts));
}
(selections, active_rows, newest_selection_head)
}
@ -3631,12 +3653,12 @@ impl EditorElement {
let forbid_vertical_scroll = editor.scroll_manager.forbid_vertical_scroll();
if forbid_vertical_scroll {
scroll_position.y = current_scroll_position.y;
if scroll_position == current_scroll_position {
return;
}
}
editor.scroll(scroll_position, axis, cx);
cx.stop_propagation();
if scroll_position != current_scroll_position {
editor.scroll(scroll_position, axis, cx);
cx.stop_propagation();
}
});
}
}
@ -4621,13 +4643,29 @@ impl Element for EditorElement {
let content_origin =
text_hitbox.origin + point(gutter_dimensions.margin, Pixels::ZERO);
let height_in_lines = bounds.size.height / line_height;
let max_scroll_top = if matches!(snapshot.mode, EditorMode::AutoHeight { .. }) {
(snapshot.max_point().row().as_f32() - height_in_lines + 1.).max(0.)
} else {
let settings = EditorSettings::get_global(cx);
let max_row = snapshot.max_point().row().as_f32();
match settings.scroll_beyond_last_line {
ScrollBeyondLastLine::OnePage => max_row,
ScrollBeyondLastLine::Off => (max_row - height_in_lines + 1.0).max(0.0),
ScrollBeyondLastLine::VerticalScrollMargin => {
(max_row - height_in_lines + 1.0 + settings.vertical_scroll_margin)
.max(0.0)
}
}
};
let mut autoscroll_containing_element = false;
let mut autoscroll_horizontally = false;
self.editor.update(cx, |editor, cx| {
autoscroll_containing_element =
editor.autoscroll_requested() || editor.has_pending_selection();
autoscroll_horizontally =
editor.autoscroll_vertically(bounds, line_height, cx);
editor.autoscroll_vertically(bounds, line_height, max_scroll_top, cx);
snapshot = editor.snapshot(cx);
});
@ -4635,7 +4673,6 @@ impl Element for EditorElement {
// The scroll position is a fractional point, the whole number of which represents
// the top of the window in terms of display rows.
let start_row = DisplayRow(scroll_position.y as u32);
let height_in_lines = bounds.size.height / line_height;
let max_row = snapshot.max_point().row();
let end_row = cmp::min(
(scroll_position.y + height_in_lines).ceil() as u32,
@ -4817,22 +4854,9 @@ impl Element for EditorElement {
cx,
);
let settings = EditorSettings::get_global(cx);
let scroll_max_row = max_row.as_f32();
let scroll_max_row = match settings.scroll_beyond_last_line {
ScrollBeyondLastLine::OnePage => scroll_max_row,
ScrollBeyondLastLine::Off => {
(scroll_max_row - height_in_lines + 1.0).max(0.0)
}
ScrollBeyondLastLine::VerticalScrollMargin => (scroll_max_row
- height_in_lines
+ 1.0
+ settings.vertical_scroll_margin)
.max(0.0),
};
let scroll_max = point(
((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
scroll_max_row,
max_scroll_top,
);
self.editor.update(cx, |editor, cx| {

View File

@ -1201,20 +1201,22 @@ impl SearchableItem for Editor {
for (excerpt_id, search_buffer, search_range) in
buffer.excerpts_in_ranges(search_within_ranges)
{
ranges.extend(
query
.search(&search_buffer, Some(search_range.clone()))
.await
.into_iter()
.map(|match_range| {
let start = search_buffer
.anchor_after(search_range.start + match_range.start);
let end = search_buffer
.anchor_before(search_range.start + match_range.end);
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
}),
);
if !search_range.is_empty() {
ranges.extend(
query
.search(&search_buffer, Some(search_range.clone()))
.await
.into_iter()
.map(|match_range| {
let start = search_buffer
.anchor_after(search_range.start + match_range.start);
let end = search_buffer
.anchor_before(search_range.start + match_range.end);
buffer.anchor_in_excerpt(excerpt_id, start).unwrap()
..buffer.anchor_in_excerpt(excerpt_id, end).unwrap()
}),
);
}
}
};

View File

@ -69,6 +69,7 @@ impl Editor {
&mut self,
bounds: Bounds<Pixels>,
line_height: Pixels,
max_scroll_top: f32,
cx: &mut ViewContext<Editor>,
) -> bool {
let viewport_height = bounds.size.height;
@ -84,11 +85,6 @@ impl Editor {
}
}
}
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
(display_map.max_point().row().as_f32() - visible_lines + 1.).max(0.)
} else {
display_map.max_point().row().as_f32()
};
if scroll_position.y > max_scroll_top {
scroll_position.y = max_scroll_top;
}

View File

@ -93,6 +93,16 @@ struct WindowFocusEvent {
current_focus_path: SmallVec<[FocusId; 8]>,
}
impl WindowFocusEvent {
pub fn is_focus_in(&self, focus_id: FocusId) -> bool {
!self.previous_focus_path.contains(&focus_id) && self.current_focus_path.contains(&focus_id)
}
pub fn is_focus_out(&self, focus_id: FocusId) -> bool {
self.previous_focus_path.contains(&focus_id) && !self.current_focus_path.contains(&focus_id)
}
}
/// This is provided when subscribing for `ViewContext::on_focus_out` events.
pub struct FocusOutEvent {
/// A weak focus handle representing what was blurred.
@ -2883,6 +2893,53 @@ impl<'a> WindowContext<'a> {
));
}
/// Register a listener to be called when the given focus handle or one of its descendants receives focus.
/// This does not fire if the given focus handle - or one of its descendants - was previously focused.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_in(
&mut self,
handle: &FocusHandle,
mut listener: impl FnMut(&mut WindowContext) + 'static,
) -> Subscription {
let focus_id = handle.id;
let (subscription, activate) =
self.window.new_focus_listener(Box::new(move |event, cx| {
if event.is_focus_in(focus_id) {
listener(cx);
}
true
}));
self.app.defer(move |_| activate());
subscription
}
/// Register a listener to be called when the given focus handle or one of its descendants loses focus.
/// Returns a subscription and persists until the subscription is dropped.
pub fn on_focus_out(
&mut self,
handle: &FocusHandle,
mut listener: impl FnMut(FocusOutEvent, &mut WindowContext) + 'static,
) -> Subscription {
let focus_id = handle.id;
let (subscription, activate) =
self.window.new_focus_listener(Box::new(move |event, cx| {
if let Some(blurred_id) = event.previous_focus_path.last().copied() {
if event.is_focus_out(focus_id) {
let event = FocusOutEvent {
blurred: WeakFocusHandle {
id: blurred_id,
handles: Arc::downgrade(&cx.window.focus_handles),
},
};
listener(event, cx)
}
}
true
}));
self.app.defer(move |_| activate());
subscription
}
fn reset_cursor_style(&self) {
// Set the cursor only if we're the active window.
if self.is_window_active() {
@ -4109,9 +4166,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
let (subscription, activate) =
self.window.new_focus_listener(Box::new(move |event, cx| {
view.update(cx, |view, cx| {
if !event.previous_focus_path.contains(&focus_id)
&& event.current_focus_path.contains(&focus_id)
{
if event.is_focus_in(focus_id) {
listener(view, cx)
}
})
@ -4175,9 +4230,7 @@ impl<'a, V: 'static> ViewContext<'a, V> {
self.window.new_focus_listener(Box::new(move |event, cx| {
view.update(cx, |view, cx| {
if let Some(blurred_id) = event.previous_focus_path.last().copied() {
if event.previous_focus_path.contains(&focus_id)
&& !event.current_focus_path.contains(&focus_id)
{
if event.is_focus_out(focus_id) {
let event = FocusOutEvent {
blurred: WeakFocusHandle {
id: blurred_id,

View File

@ -1701,6 +1701,8 @@ impl Buffer {
},
cx,
);
self.selections_update_count += 1;
cx.notify();
}
/// Clears the selections, so that other replicas of the buffer do not see any selections for
@ -3355,9 +3357,10 @@ impl BufferSnapshot {
/// Returns selections for remote peers intersecting the given range.
#[allow(clippy::type_complexity)]
pub fn remote_selections_in_range(
pub fn selections_in_range(
&self,
range: Range<Anchor>,
include_local: bool,
) -> impl Iterator<
Item = (
ReplicaId,
@ -3368,8 +3371,9 @@ impl BufferSnapshot {
> + '_ {
self.remote_selections
.iter()
.filter(|(replica_id, set)| {
**replica_id != self.text.replica_id() && !set.selections.is_empty()
.filter(move |(replica_id, set)| {
(include_local || **replica_id != self.text.replica_id())
&& !set.selections.is_empty()
})
.map(move |(replica_id, set)| {
let start_ix = match set.selections.binary_search_by(|probe| {

View File

@ -2416,7 +2416,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) {
for buffer in &buffers {
let buffer = buffer.read(cx).snapshot();
let actual_remote_selections = buffer
.remote_selections_in_range(Anchor::MIN..Anchor::MAX)
.selections_in_range(Anchor::MIN..Anchor::MAX, false)
.map(|(replica_id, _, _, selections)| (replica_id, selections.collect::<Vec<_>>()))
.collect::<Vec<_>>();
let expected_remote_selections = active_selections

View File

@ -3834,8 +3834,7 @@ impl MultiBufferSnapshot {
return None;
}
if range.as_ref().unwrap().is_empty() || *cursor.start() >= range.as_ref().unwrap().end
{
if *cursor.start() >= range.as_ref().unwrap().end {
range = next_range(&mut cursor);
if range.is_none() {
return None;
@ -3867,9 +3866,10 @@ impl MultiBufferSnapshot {
})
}
pub fn remote_selections_in_range<'a>(
pub fn selections_in_range<'a>(
&'a self,
range: &'a Range<Anchor>,
include_local: bool,
) -> impl 'a + Iterator<Item = (ReplicaId, bool, CursorShape, Selection<Anchor>)> {
let mut cursor = self.excerpts.cursor::<ExcerptSummary>();
let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id);
@ -3888,7 +3888,7 @@ impl MultiBufferSnapshot {
excerpt
.buffer
.remote_selections_in_range(query_range)
.selections_in_range(query_range, include_local)
.flat_map(move |(replica_id, line_mode, cursor_shape, selections)| {
selections.map(move |selection| {
let mut start = Anchor {