From 2786d367865c4c0bdb8257a498a630e6032fd5fb Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Thu, 19 Feb 2026 10:52:33 +0100 Subject: [PATCH] editor: Yield less frequently in `WrapSnapshot::update` (#49497) Every yield will cause the background task to get rescheduled causing additional thread/context switching, so doing so for every wrapped row is a bit excessive Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/editor/src/display_map/wrap_map.rs | 194 ++++++++++-------- crates/editor/src/element.rs | 6 - .../project/src/lsp_store/semantic_tokens.rs | 3 - 3 files changed, 114 insertions(+), 89 deletions(-) diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index 9ea2064ebed1e9bc630e971f494bdbfe92df0002..650ee99918e9c9f7a95a367db7e4d4f01b02d6ed 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -19,6 +19,8 @@ pub type WrapPatch = text::Patch; #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct WrapRow(pub u32); +const WRAP_YIELD_ROW_INTERVAL: usize = 100; + impl_for_row_types! { WrapRow => RowDelta } @@ -191,49 +193,58 @@ impl WrapMap { if let Some(wrap_width) = self.wrap_width { let mut new_snapshot = self.snapshot.clone(); - let text_system = cx.text_system().clone(); + let text_system = cx.text_system(); let (font, font_size) = self.font_with_size.clone(); - let task = cx.background_spawn(async move { - let mut line_wrapper = text_system.line_wrapper(font, font_size); - let tab_snapshot = new_snapshot.tab_snapshot.clone(); - let range = TabPoint::zero()..tab_snapshot.max_point(); - let edits = new_snapshot - .update( - tab_snapshot, - &[TabEdit { - old: range.clone(), - new: range.clone(), - }], - wrap_width, - &mut line_wrapper, - ) - .await; - (new_snapshot, edits) - }); + let mut line_wrapper = text_system.line_wrapper(font, font_size); + let tab_snapshot = new_snapshot.tab_snapshot.clone(); + let total_rows = tab_snapshot.max_point().row() as usize + 1; + let range = TabPoint::zero()..tab_snapshot.max_point(); + let tab_edits = [TabEdit { + old: range.clone(), + new: range, + }]; + + if total_rows < WRAP_YIELD_ROW_INTERVAL { + let edits = smol::block_on(new_snapshot.update( + tab_snapshot, + &tab_edits, + wrap_width, + &mut line_wrapper, + )); + self.snapshot = new_snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&edits); + } else { + let task = cx.background_spawn(async move { + let edits = new_snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) + .await; + (new_snapshot, edits) + }); - match cx - .foreground_executor() - .block_with_timeout(Duration::from_millis(5), task) - { - Ok((snapshot, edits)) => { - self.snapshot = snapshot; - self.edits_since_sync = self.edits_since_sync.compose(&edits); - } - Err(wrap_task) => { - self.background_task = Some(cx.spawn(async move |this, cx| { - let (snapshot, edits) = wrap_task.await; - this.update(cx, |this, cx| { - this.snapshot = snapshot; - this.edits_since_sync = this - .edits_since_sync - .compose(mem::take(&mut this.interpolated_edits).invert()) - .compose(&edits); - this.background_task = None; - this.flush_edits(cx); - cx.notify(); - }) - .ok(); - })); + match cx + .foreground_executor() + .block_with_timeout(Duration::from_millis(5), task) + { + Ok((snapshot, edits)) => { + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&edits); + } + Err(wrap_task) => { + self.background_task = Some(cx.spawn(async move |this, cx| { + let (snapshot, edits) = wrap_task.await; + this.update(cx, |this, cx| { + this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); + this.background_task = None; + this.flush_edits(cx); + cx.notify(); + }) + .ok(); + })); + } } } } else { @@ -275,45 +286,63 @@ impl WrapMap { if let Some(wrap_width) = self.wrap_width && self.background_task.is_none() { - let pending_edits = self.pending_edits.clone(); + let mut pending_edits = self.pending_edits.clone(); let mut snapshot = self.snapshot.clone(); let text_system = cx.text_system().clone(); let (font, font_size) = self.font_with_size.clone(); - let update_task = cx.background_spawn(async move { - let mut edits = Patch::default(); - let mut line_wrapper = text_system.line_wrapper(font, font_size); - for (tab_snapshot, tab_edits) in pending_edits { - let wrap_edits = snapshot - .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) - .await; - edits = edits.compose(&wrap_edits); - } - (snapshot, edits) - }); - - match cx - .foreground_executor() - .block_with_timeout(Duration::from_millis(1), update_task) + let mut line_wrapper = text_system.line_wrapper(font, font_size); + + if pending_edits.len() == 1 + && let Some((_, tab_edits)) = pending_edits.back() + && let [edit] = &**tab_edits + && ((edit.new.end.row().saturating_sub(edit.new.start.row()) + 1) as usize) + < WRAP_YIELD_ROW_INTERVAL + && let Some((tab_snapshot, tab_edits)) = pending_edits.pop_back() { - Ok((snapshot, output_edits)) => { - self.snapshot = snapshot; - self.edits_since_sync = self.edits_since_sync.compose(&output_edits); - } - Err(update_task) => { - self.background_task = Some(cx.spawn(async move |this, cx| { - let (snapshot, edits) = update_task.await; - this.update(cx, |this, cx| { - this.snapshot = snapshot; - this.edits_since_sync = this - .edits_since_sync - .compose(mem::take(&mut this.interpolated_edits).invert()) - .compose(&edits); - this.background_task = None; - this.flush_edits(cx); - cx.notify(); - }) - .ok(); - })); + let wrap_edits = smol::block_on(snapshot.update( + tab_snapshot, + &tab_edits, + wrap_width, + &mut line_wrapper, + )); + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&wrap_edits); + } else { + let update_task = cx.background_spawn(async move { + let mut edits = Patch::default(); + for (tab_snapshot, tab_edits) in pending_edits { + let wrap_edits = snapshot + .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper) + .await; + edits = edits.compose(&wrap_edits); + } + (snapshot, edits) + }); + + match cx + .foreground_executor() + .block_with_timeout(Duration::from_millis(1), update_task) + { + Ok((snapshot, output_edits)) => { + self.snapshot = snapshot; + self.edits_since_sync = self.edits_since_sync.compose(&output_edits); + } + Err(update_task) => { + self.background_task = Some(cx.spawn(async move |this, cx| { + let (snapshot, edits) = update_task.await; + this.update(cx, |this, cx| { + this.snapshot = snapshot; + this.edits_since_sync = this + .edits_since_sync + .compose(mem::take(&mut this.interpolated_edits).invert()) + .compose(&edits); + this.background_task = None; + this.flush_edits(cx); + cx.notify(); + }) + .ok(); + })); + } } } } @@ -445,8 +474,10 @@ impl WrapSnapshot { while let Some(next_edit) = tab_edits_iter.peek() { if next_edit.old.start.row() <= row_edit.old_rows.end { - row_edit.old_rows.end = next_edit.old.end.row() + 1; - row_edit.new_rows.end = next_edit.new.end.row() + 1; + row_edit.old_rows.end = + cmp::max(row_edit.old_rows.end, next_edit.old.end.row() + 1); + row_edit.new_rows.end = + cmp::max(row_edit.new_rows.end, next_edit.new.end.row() + 1); tab_edits_iter.next(); } else { break; @@ -486,7 +517,7 @@ impl WrapSnapshot { Highlights::default(), ); let mut edit_transforms = Vec::::new(); - for _ in edit.new_rows.start..edit.new_rows.end { + for (i, _) in (edit.new_rows.start..edit.new_rows.end).enumerate() { while let Some(chunk) = remaining.take().or_else(|| chunks.next()) { if let Some(ix) = chunk.text.find('\n') { let (prefix, suffix) = chunk.text.split_at(ix + 1); @@ -531,7 +562,9 @@ impl WrapSnapshot { line.clear(); line_fragments.clear(); - yield_now().await; + if i % WRAP_YIELD_ROW_INTERVAL == WRAP_YIELD_ROW_INTERVAL - 1 { + yield_now().await; + } } let mut edit_transforms = edit_transforms.into_iter(); @@ -556,6 +589,7 @@ impl WrapSnapshot { (), ); } + yield_now().await; } else { if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) { let summary = self.tab_snapshot.text_summary_for_range( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ae4713ee4ce337b0d535a89da4c1246a024f6ad..71dffc0eda5d266f29fa78d948c51629a76652df 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -12228,9 +12228,6 @@ mod tests { #[gpui::test] async fn test_soft_wrap_editor_width_auto_height_editor(cx: &mut TestAppContext) { init_test(cx, |_| {}); - // Ensure wrap completes synchronously by giving block_with_timeout enough ticks - cx.dispatcher.scheduler().set_timeout_ticks(1000..=1000); - let window = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); let mut editor = Editor::new( @@ -12267,9 +12264,6 @@ mod tests { #[gpui::test] async fn test_soft_wrap_editor_width_full_editor(cx: &mut TestAppContext) { init_test(cx, |_| {}); - // Ensure wrap completes synchronously by giving block_with_timeout enough ticks - cx.dispatcher.scheduler().set_timeout_ticks(1000..=1000); - let window = cx.add_window(|window, cx| { let buffer = MultiBuffer::build_simple(&"a ".to_string().repeat(100), cx); let mut editor = Editor::new(EditorMode::full(), buffer, None, window, cx); diff --git a/crates/project/src/lsp_store/semantic_tokens.rs b/crates/project/src/lsp_store/semantic_tokens.rs index 00e4a4c1890278526edc0174a8b6fcf9652226f5..516fb75eaae13752c235d0ad42db460740529c4d 100644 --- a/crates/project/src/lsp_store/semantic_tokens.rs +++ b/crates/project/src/lsp_store/semantic_tokens.rs @@ -564,9 +564,6 @@ async fn raw_to_buffer_semantic_tokens( } let end = buffer_snapshot.as_rope().offset_utf16_to_offset(end_offset); - if end < last { - return None; - } last = end; if start == end {