mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-25 08:55:10 +03:00
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:
parent
c58a8f1a04
commit
cb0b8b4c4b
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -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"
|
||||
|
@ -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
@ -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();
|
||||
}
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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");
|
||||
|
@ -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,
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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| {
|
||||
|
@ -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()
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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| {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user