language: Only block the foreground on buffer reparsing when necessary (#43888)

Gist is we only need to block the foreground thread for reparsing if
immediate language changes are useful to the user. That is usually only
the case when they edit the buffer

Release Notes:

- Improved performance of large project searches and project diffs

Co-authored by: David Kleingeld <david@zed.dev>
This commit is contained in:
Lukas Wirth
2025-12-01 14:57:15 +01:00
committed by GitHub
parent 3969109aa3
commit 9af6e82e65
54 changed files with 363 additions and 250 deletions
+97 -41
View File
@@ -1036,6 +1036,16 @@ impl Buffer {
self
}
/// Assign a language to the buffer, blocking for up to 1ms to reparse the buffer, returning the buffer.
pub fn with_language_immediate(
mut self,
language: Arc<Language>,
cx: &mut Context<Self>,
) -> Self {
self.set_language_immediate(Some(language), cx);
self
}
/// Returns the [`Capability`] of this buffer.
pub fn capability(&self) -> Capability {
self.capability
@@ -1210,7 +1220,7 @@ impl Buffer {
}
// Reparse the branch buffer so that we get syntax highlighting immediately.
branch.reparse(cx);
branch.reparse(cx, true);
branch
})
@@ -1363,11 +1373,29 @@ impl Buffer {
/// Assign a language to the buffer.
pub fn set_language(&mut self, language: Option<Arc<Language>>, cx: &mut Context<Self>) {
self.set_language_(language, cfg!(any(test, feature = "test-support")), cx);
}
/// Assign a language to the buffer, blocking for up to 1ms to reparse the buffer.
pub fn set_language_immediate(
&mut self,
language: Option<Arc<Language>>,
cx: &mut Context<Self>,
) {
self.set_language_(language, true, cx);
}
fn set_language_(
&mut self,
language: Option<Arc<Language>>,
may_block: bool,
cx: &mut Context<Self>,
) {
self.non_text_state_update_count += 1;
self.syntax_map.lock().clear(&self.text);
self.language = language;
self.was_changed();
self.reparse(cx);
self.reparse(cx, may_block);
cx.emit(BufferEvent::LanguageChanged);
}
@@ -1610,9 +1638,9 @@ impl Buffer {
/// The snapshot with the interpolated edits is sent to a background thread,
/// where we ask Tree-sitter to perform an incremental parse.
///
/// Meanwhile, in the foreground, we block the main thread for up to 1ms
/// waiting on the parse to complete. As soon as it completes, we proceed
/// synchronously, unless a 1ms timeout elapses.
/// Meanwhile, in the foreground if `may_block` is true, we block the main
/// thread for up to 1ms waiting on the parse to complete. As soon as it
/// completes, we proceed synchronously, unless a 1ms timeout elapses.
///
/// If we time out waiting on the parse, we spawn a second task waiting
/// until the parse does complete and return with the interpolated tree still
@@ -1623,7 +1651,7 @@ impl Buffer {
/// initiate an additional reparse recursively. To avoid concurrent parses
/// for the same buffer, we only initiate a new parse if we are not already
/// parsing in the background.
pub fn reparse(&mut self, cx: &mut Context<Self>) {
pub fn reparse(&mut self, cx: &mut Context<Self>, may_block: bool) {
if self.reparse.is_some() {
return;
}
@@ -1652,42 +1680,70 @@ impl Buffer {
});
self.parse_status.0.send(ParseStatus::Parsing).unwrap();
match cx
.background_executor()
.block_with_timeout(self.sync_parse_timeout, parse_task)
{
Ok(new_syntax_snapshot) => {
self.did_finish_parsing(new_syntax_snapshot, cx);
self.reparse = None;
}
Err(parse_task) => {
// todo(lw): hot foreground spawn
self.reparse = Some(cx.spawn(async move |this, cx| {
let new_syntax_map = cx.background_spawn(parse_task).await;
this.update(cx, move |this, cx| {
let grammar_changed = || {
this.language.as_ref().is_none_or(|current_language| {
!Arc::ptr_eq(&language, current_language)
})
};
let language_registry_changed = || {
new_syntax_map.contains_unknown_injections()
&& language_registry.is_some_and(|registry| {
registry.version() != new_syntax_map.language_registry_version()
if may_block {
match cx
.background_executor()
.block_with_timeout(self.sync_parse_timeout, parse_task)
{
Ok(new_syntax_snapshot) => {
self.did_finish_parsing(new_syntax_snapshot, cx);
self.reparse = None;
}
Err(parse_task) => {
self.reparse = Some(cx.spawn(async move |this, cx| {
let new_syntax_map = cx.background_spawn(parse_task).await;
this.update(cx, move |this, cx| {
let grammar_changed = || {
this.language.as_ref().is_none_or(|current_language| {
!Arc::ptr_eq(&language, current_language)
})
};
let parse_again = this.version.changed_since(&parsed_version)
|| language_registry_changed()
|| grammar_changed();
this.did_finish_parsing(new_syntax_map, cx);
this.reparse = None;
if parse_again {
this.reparse(cx);
}
})
.ok();
}));
};
let language_registry_changed = || {
new_syntax_map.contains_unknown_injections()
&& language_registry.is_some_and(|registry| {
registry.version()
!= new_syntax_map.language_registry_version()
})
};
let parse_again = this.version.changed_since(&parsed_version)
|| language_registry_changed()
|| grammar_changed();
this.did_finish_parsing(new_syntax_map, cx);
this.reparse = None;
if parse_again {
this.reparse(cx, false);
}
})
.ok();
}));
}
}
} else {
self.reparse = Some(cx.spawn(async move |this, cx| {
let new_syntax_map = cx.background_spawn(parse_task).await;
this.update(cx, move |this, cx| {
let grammar_changed = || {
this.language.as_ref().is_none_or(|current_language| {
!Arc::ptr_eq(&language, current_language)
})
};
let language_registry_changed = || {
new_syntax_map.contains_unknown_injections()
&& language_registry.is_some_and(|registry| {
registry.version() != new_syntax_map.language_registry_version()
})
};
let parse_again = this.version.changed_since(&parsed_version)
|| language_registry_changed()
|| grammar_changed();
this.did_finish_parsing(new_syntax_map, cx);
this.reparse = None;
if parse_again {
this.reparse(cx, false);
}
})
.ok();
}));
}
}
@@ -2588,7 +2644,7 @@ impl Buffer {
return;
}
self.reparse(cx);
self.reparse(cx, true);
cx.emit(BufferEvent::Edited);
if was_dirty != self.is_dirty() {
cx.emit(BufferEvent::DirtyChanged);
+51 -40
View File
@@ -46,8 +46,8 @@ fn test_line_endings(cx: &mut gpui::App) {
init_settings(cx, |_| {});
cx.new(|cx| {
let mut buffer =
Buffer::local("one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local("one\r\ntwo\rthree", cx)
.with_language_immediate(Arc::new(rust_lang()), cx);
assert_eq!(buffer.text(), "one\ntwo\nthree");
assert_eq!(buffer.line_ending(), LineEnding::Windows);
@@ -608,7 +608,8 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_reparse(cx: &mut gpui::TestAppContext) {
let text = "fn a() {}";
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx));
// Wait for the initial text to parse
cx.executor().run_until_parked();
@@ -735,7 +736,7 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) {
#[gpui::test]
async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local("{}", cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local("{}", cx).with_language_immediate(Arc::new(rust_lang()), cx);
buffer.set_sync_parse_timeout(Duration::ZERO);
buffer
});
@@ -748,7 +749,7 @@ async fn test_resetting_language(cx: &mut gpui::TestAppContext) {
);
buffer.update(cx, |buffer, cx| {
buffer.set_language(Some(Arc::new(json_lang())), cx)
buffer.set_language_immediate(Some(Arc::new(json_lang())), cx)
});
cx.executor().run_until_parked();
assert_eq!(get_tree_sexp(&buffer, cx), "(document (object))");
@@ -783,7 +784,8 @@ async fn test_outline(cx: &mut gpui::TestAppContext) {
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx));
let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
@@ -865,7 +867,8 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) {
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx));
let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
@@ -899,7 +902,8 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) {
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(language), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(language), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
// extra context nodes are included in the outline.
@@ -945,7 +949,8 @@ fn test_outline_annotations(cx: &mut App) {
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx));
let outline = buffer.update(cx, |buffer, _| buffer.snapshot().outline(None));
assert_eq!(
@@ -993,7 +998,8 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
"#
.unindent();
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
// point is at the start of an item
@@ -1068,7 +1074,8 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) {
"
.unindent(),
);
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx));
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
// note, it would be nice to actually return the method test in this
@@ -1087,8 +1094,9 @@ fn test_text_objects(cx: &mut App) {
false,
);
let buffer =
cx.new(|cx| Buffer::local(text.clone(), cx).with_language(Arc::new(rust_lang()), cx));
let buffer = cx.new(|cx| {
Buffer::local(text.clone(), cx).with_language_immediate(Arc::new(rust_lang()), cx)
});
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
let matches = snapshot
@@ -1268,7 +1276,7 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: &
fn test_range_for_syntax_ancestor(cx: &mut App) {
cx.new(|cx| {
let text = "fn a() { b(|c| {}) }";
let buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
let snapshot = buffer.snapshot();
assert_eq!(
@@ -1320,7 +1328,7 @@ fn test_autoindent_with_soft_tabs(cx: &mut App) {
cx.new(|cx| {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
assert_eq!(buffer.text(), "fn a() {\n \n}");
@@ -1362,7 +1370,7 @@ fn test_autoindent_with_hard_tabs(cx: &mut App) {
cx.new(|cx| {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx);
assert_eq!(buffer.text(), "fn a() {\n\t\n}");
@@ -1411,7 +1419,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut App)
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
.with_language_immediate(Arc::new(rust_lang()), cx);
// Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
// their indentation is not adjusted.
@@ -1552,7 +1560,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut App)
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
.with_language_immediate(Arc::new(rust_lang()), cx);
// Insert a closing brace. It is outdented.
buffer.edit_via_marked_text(
@@ -1615,7 +1623,7 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
.with_language_immediate(Arc::new(rust_lang()), cx);
// Regression test: line does not get outdented due to syntax error
buffer.edit_via_marked_text(
@@ -1674,7 +1682,7 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut App) {
.unindent(),
cx,
)
.with_language(Arc::new(rust_lang()), cx);
.with_language_immediate(Arc::new(rust_lang()), cx);
buffer.edit_via_marked_text(
&"
@@ -1724,7 +1732,7 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut App) {
cx.new(|cx| {
let text = "a\nb";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
buffer.edit(
[(0..1, "\n"), (2..3, "\n")],
Some(AutoindentMode::EachLine),
@@ -1750,7 +1758,7 @@ fn test_autoindent_multi_line_insertion(cx: &mut App) {
"
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
buffer.edit(
[(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")],
Some(AutoindentMode::EachLine),
@@ -1787,7 +1795,7 @@ fn test_autoindent_block_mode(cx: &mut App) {
}
"#
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
// When this text was copied, both of the quotation marks were at the same
// indent level, but the indentation of the first line was not included in
@@ -1870,7 +1878,7 @@ fn test_autoindent_block_mode_with_newline(cx: &mut App) {
}
"#
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
// First line contains just '\n', it's indentation is stored in "original_indent_columns"
let original_indent_columns = vec![Some(4)];
@@ -1922,7 +1930,7 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut App) {
}
"#
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
// The original indent columns are not known, so this text is
// auto-indented in a block as if the first line was copied in
@@ -2013,7 +2021,7 @@ fn test_autoindent_block_mode_multiple_adjacent_ranges(cx: &mut App) {
false,
);
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
buffer.edit(
[
@@ -2064,7 +2072,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut App) {
"
.unindent();
let mut buffer = Buffer::local(text, cx).with_language(
let mut buffer = Buffer::local(text, cx).with_language_immediate(
Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
@@ -2142,7 +2150,7 @@ fn test_autoindent_with_injected_languages(cx: &mut App) {
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry);
buffer.set_language(Some(html_language), cx);
buffer.set_language_immediate(Some(html_language), cx);
buffer.edit(
ranges.into_iter().map(|range| (range, "\na")),
Some(AutoindentMode::EachLine),
@@ -2176,7 +2184,7 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut App) {
});
cx.new(|cx| {
let mut buffer = Buffer::local("", cx).with_language(Arc::new(ruby_lang()), cx);
let mut buffer = Buffer::local("", cx).with_language_immediate(Arc::new(ruby_lang()), cx);
let text = r#"
class C
@@ -2221,7 +2229,7 @@ async fn test_async_autoindents_preserve_preview(cx: &mut TestAppContext) {
// Then we request that a preview tab be preserved for the new version, even though it's edited.
let buffer = cx.new(|cx| {
let text = "fn a() {}";
let mut buffer = Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx);
let mut buffer = Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx);
// This causes autoindent to be async.
buffer.set_sync_parse_timeout(Duration::ZERO);
@@ -2434,7 +2442,7 @@ fn test_language_scope_at_with_javascript(cx: &mut App) {
"#
.unindent();
let buffer = Buffer::local(&text, cx).with_language(Arc::new(language), cx);
let buffer = Buffer::local(&text, cx).with_language_immediate(Arc::new(language), cx);
let snapshot = buffer.snapshot();
let config = snapshot.language_scope_at(0).unwrap();
@@ -2592,7 +2600,8 @@ fn test_language_scope_at_with_rust(cx: &mut App) {
"#
.unindent();
let buffer = Buffer::local(text.clone(), cx).with_language(Arc::new(language), cx);
let buffer =
Buffer::local(text.clone(), cx).with_language_immediate(Arc::new(language), cx);
let snapshot = buffer.snapshot();
// By default, all brackets are enabled
@@ -2638,7 +2647,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut App) {
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
buffer.set_language_immediate(
language_registry
.language_for_name("HTML+ERB")
.now_or_never()
@@ -2684,7 +2693,7 @@ fn test_language_at_with_hidden_languages(cx: &mut App) {
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
buffer.set_language_immediate(
language_registry
.language_for_name("Markdown")
.now_or_never()
@@ -2727,7 +2736,7 @@ fn test_language_at_for_markdown_code_block(cx: &mut App) {
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
buffer.set_language_immediate(
language_registry
.language_for_name("Markdown")
.now_or_never()
@@ -2780,7 +2789,7 @@ fn test_syntax_layer_at_for_injected_languages(cx: &mut App) {
let mut buffer = Buffer::local(text, cx);
buffer.set_language_registry(language_registry.clone());
buffer.set_language(
buffer.set_language_immediate(
language_registry
.language_for_name("HTML+ERB")
.now_or_never()
@@ -3120,7 +3129,8 @@ async fn test_preview_edits(cx: &mut TestAppContext) {
cx: &mut TestAppContext,
assert_fn: impl Fn(HighlightedText),
) {
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(Arc::new(rust_lang()), cx));
let buffer =
cx.new(|cx| Buffer::local(text, cx).with_language_immediate(Arc::new(rust_lang()), cx));
let edits = buffer.read_with(cx, |buffer, _| {
edits
.into_iter()
@@ -3531,7 +3541,7 @@ let word=öäpple.bar你 Öäpple word2-öÄpPlE-Pizza-word ÖÄPPLE word
"#;
let buffer = cx.new(|cx| {
let buffer = Buffer::local(contents, cx).with_language(Arc::new(rust_lang()), cx);
let buffer = Buffer::local(contents, cx).with_language_immediate(Arc::new(rust_lang()), cx);
assert_eq!(buffer.text(), contents);
buffer.check_invariants();
buffer
@@ -3921,8 +3931,9 @@ fn assert_bracket_pairs(
cx: &mut App,
) {
let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false);
let buffer =
cx.new(|cx| Buffer::local(expected_text.clone(), cx).with_language(Arc::new(language), cx));
let buffer = cx.new(|cx| {
Buffer::local(expected_text.clone(), cx).with_language_immediate(Arc::new(language), cx)
});
let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot());
let selection_range = selection_ranges[0].clone();