Detailed changes
@@ -14082,6 +14082,7 @@ dependencies = [
"smol",
"sysinfo 0.37.2",
"task",
+ "theme",
"thiserror 2.0.17",
"toml 0.8.23",
"unindent",
@@ -15135,6 +15136,7 @@ dependencies = [
"language",
"lsp",
"menu",
+ "pretty_assertions",
"project",
"schemars 1.0.4",
"serde",
@@ -255,6 +255,12 @@
// Whether to display inline and alongside documentation for items in the
// completions menu
"show_completion_documentation": true,
+ // Whether to colorize brackets in the editor.
+ // (also known as "rainbow brackets")
+ //
+ // The colors that are used for different indentation levels are defined in the theme (theme key: `accents`).
+ // They can be customized by using theme overrides.
+ "colorize_brackets": false,
// When to show the scrollbar in the completion menu.
// This setting can take four values:
//
@@ -242,6 +242,7 @@ impl Console {
start_offset,
vec![range],
style,
+ false,
cx,
);
}
@@ -0,0 +1,1287 @@
+//! Bracket highlights, also known as "rainbow brackets".
+//! Uses tree-sitter queries from brackets.scm to capture bracket pairs,
+//! and theme accents to colorize those.
+
+use std::ops::Range;
+
+use crate::Editor;
+use collections::HashMap;
+use gpui::{Context, HighlightStyle};
+use language::language_settings;
+use multi_buffer::{Anchor, ExcerptId};
+use ui::{ActiveTheme, utils::ensure_minimum_contrast};
+
+struct ColorizedBracketsHighlight;
+
+impl Editor {
+ pub(crate) fn colorize_brackets(&mut self, invalidate: bool, cx: &mut Context<Editor>) {
+ if !self.mode.is_full() {
+ return;
+ }
+
+ if invalidate {
+ self.fetched_tree_sitter_chunks.clear();
+ }
+
+ let accents_count = cx.theme().accents().0.len();
+ let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx);
+ let all_excerpts = self.buffer().read(cx).excerpt_ids();
+ let anchor_in_multi_buffer = |current_excerpt: ExcerptId, text_anchor: text::Anchor| {
+ multi_buffer_snapshot
+ .anchor_in_excerpt(current_excerpt, text_anchor)
+ .or_else(|| {
+ all_excerpts
+ .iter()
+ .filter(|&&excerpt_id| excerpt_id != current_excerpt)
+ .find_map(|&excerpt_id| {
+ multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)
+ })
+ })
+ };
+
+ let bracket_matches_by_accent = self.visible_excerpts(cx).into_iter().fold(
+ HashMap::default(),
+ |mut acc, (excerpt_id, (buffer, buffer_version, buffer_range))| {
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ if language_settings::language_settings(
+ buffer_snapshot.language().map(|language| language.name()),
+ buffer_snapshot.file(),
+ cx,
+ )
+ .colorize_brackets
+ {
+ let fetched_chunks = self
+ .fetched_tree_sitter_chunks
+ .entry(excerpt_id)
+ .or_default();
+
+ let brackets_by_accent = buffer_snapshot
+ .fetch_bracket_ranges(
+ buffer_range.start..buffer_range.end,
+ Some((&buffer_version, fetched_chunks)),
+ )
+ .into_iter()
+ .flat_map(|(chunk_range, pairs)| {
+ if fetched_chunks.insert(chunk_range) {
+ pairs
+ } else {
+ Vec::new()
+ }
+ })
+ .filter_map(|pair| {
+ let color_index = pair.color_index?;
+
+ let buffer_open_range = buffer_snapshot
+ .anchor_before(pair.open_range.start)
+ ..buffer_snapshot.anchor_after(pair.open_range.end);
+ let buffer_close_range = buffer_snapshot
+ .anchor_before(pair.close_range.start)
+ ..buffer_snapshot.anchor_after(pair.close_range.end);
+ let multi_buffer_open_range =
+ anchor_in_multi_buffer(excerpt_id, buffer_open_range.start)
+ .zip(anchor_in_multi_buffer(excerpt_id, buffer_open_range.end));
+ let multi_buffer_close_range =
+ anchor_in_multi_buffer(excerpt_id, buffer_close_range.start).zip(
+ anchor_in_multi_buffer(excerpt_id, buffer_close_range.end),
+ );
+
+ let mut ranges = Vec::with_capacity(2);
+ if let Some((open_start, open_end)) = multi_buffer_open_range {
+ ranges.push(open_start..open_end);
+ }
+ if let Some((close_start, close_end)) = multi_buffer_close_range {
+ ranges.push(close_start..close_end);
+ }
+ if ranges.is_empty() {
+ None
+ } else {
+ Some((color_index % accents_count, ranges))
+ }
+ });
+
+ for (accent_number, new_ranges) in brackets_by_accent {
+ let ranges = acc
+ .entry(accent_number)
+ .or_insert_with(Vec::<Range<Anchor>>::new);
+
+ for new_range in new_ranges {
+ let i = ranges
+ .binary_search_by(|probe| {
+ probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
+ })
+ .unwrap_or_else(|i| i);
+ ranges.insert(i, new_range);
+ }
+ }
+ }
+
+ acc
+ },
+ );
+
+ if invalidate {
+ self.clear_highlights::<ColorizedBracketsHighlight>(cx);
+ }
+
+ let editor_background = cx.theme().colors().editor_background;
+ for (accent_number, bracket_highlights) in bracket_matches_by_accent {
+ let bracket_color = cx.theme().accents().color_for_index(accent_number as u32);
+ let adjusted_color = ensure_minimum_contrast(bracket_color, editor_background, 55.0);
+ let style = HighlightStyle {
+ color: Some(adjusted_color),
+ ..HighlightStyle::default()
+ };
+
+ self.highlight_text_key::<ColorizedBracketsHighlight>(
+ accent_number,
+ bracket_highlights,
+ style,
+ true,
+ cx,
+ );
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::{cmp, sync::Arc, time::Duration};
+
+ use super::*;
+ use crate::{
+ DisplayPoint, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
+ display_map::{DisplayRow, ToDisplayPoint},
+ editor_tests::init_test,
+ test::{
+ editor_lsp_test_context::EditorLspTestContext, editor_test_context::EditorTestContext,
+ },
+ };
+ use collections::HashSet;
+ use fs::FakeFs;
+ use gpui::{AppContext as _, UpdateGlobal as _};
+ use indoc::indoc;
+ use itertools::Itertools;
+ use language::Capability;
+ use languages::rust_lang;
+ use multi_buffer::{ExcerptRange, MultiBuffer};
+ use pretty_assertions::assert_eq;
+ use project::Project;
+ use rope::Point;
+ use serde_json::json;
+ use settings::{AccentContent, SettingsStore};
+ use text::{Bias, OffsetRangeExt, ToOffset};
+ use theme::ThemeStyleContent;
+ use ui::SharedString;
+ use util::{path, post_inc};
+
+ #[gpui::test]
+ async fn test_basic_bracket_colorization(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |language_settings| {
+ language_settings.defaults.colorize_brackets = Some(true);
+ });
+ let mut cx = EditorLspTestContext::new(
+ Arc::into_inner(rust_lang()).unwrap(),
+ lsp::ServerCapabilities::default(),
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {r#"ˇuse std::{collections::HashMap, future::Future};
+
+fn main() {
+ let a = one((), { () }, ());
+ println!("{a}");
+ println!("{a}");
+ for i in 0..a {
+ println!("{i}");
+ }
+
+ let b = {
+ {
+ {
+ [([([([([([([([([([((), ())])])])])])])])])])]
+ }
+ }
+ };
+}
+
+#[rustfmt::skip]
+fn one(a: (), (): (), c: ()) -> usize { 1 }
+
+fn two<T>(a: HashMap<String, Vec<Option<T>>>) -> usize
+where
+ T: Future<Output = HashMap<String, Vec<Option<Box<()>>>>>,
+{
+ 2
+}
+"#});
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+
+ assert_eq!(
+ r#"use std::«1{collections::HashMap, future::Future}1»;
+
+fn main«1()1» «1{
+ let a = one«2(«3()3», «3{ «4()4» }3», «3()3»)2»;
+ println!«2("{a}")2»;
+ println!«2("{a}")2»;
+ for i in 0..a «2{
+ println!«3("{i}")3»;
+ }2»
+
+ let b = «2{
+ «3{
+ «4{
+ «5[«6(«7[«1(«2[«3(«4[«5(«6[«7(«1[«2(«3[«4(«5[«6(«7[«1(«2[«3(«4()4», «4()4»)3»]2»)1»]7»)6»]5»)4»]3»)2»]1»)7»]6»)5»]4»)3»]2»)1»]7»)6»]5»
+ }4»
+ }3»
+ }2»;
+}1»
+
+#«1[rustfmt::skip]1»
+fn one«1(a: «2()2», «2()2»: «2()2», c: «2()2»)1» -> usize «1{ 1 }1»
+
+fn two«1<T>1»«1(a: HashMap«2<String, Vec«3<Option«4<T>4»>3»>2»)1» -> usize
+where
+ T: Future«1<Output = HashMap«2<String, Vec«3<Option«4<Box«5<«6()6»>5»>4»>3»>2»>1»,
+«1{
+ 2
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+6 hsla(95.00, 38.00%, 62.00%, 1.00)
+7 hsla(39.00, 67.00%, 69.00%, 1.00)
+"#,
+ &bracket_colors_markup(&mut cx),
+ "All brackets should be colored based on their depth"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_bracket_colorization_when_editing(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |language_settings| {
+ language_settings.defaults.colorize_brackets = Some(true);
+ });
+ let mut cx = EditorLspTestContext::new(
+ Arc::into_inner(rust_lang()).unwrap(),
+ lsp::ServerCapabilities::default(),
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {r#"
+struct Foo<'a, T> {
+ data: Vec<Option<&'a T>>,
+}
+
+fn process_data() {
+ let map:ˇ
+}
+"#});
+
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input(" Result<", window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ indoc! {r#"
+struct Foo«1<'a, T>1» «1{
+ data: Vec«2<Option«3<&'a T>3»>2»,
+}1»
+
+fn process_data«1()1» «1{
+ let map: Result<
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+"#},
+ &bracket_colors_markup(&mut cx),
+ "Brackets without pairs should be ignored and not colored"
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("Option<Foo<'_, ()", window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ indoc! {r#"
+struct Foo«1<'a, T>1» «1{
+ data: Vec«2<Option«3<&'a T>3»>2»,
+}1»
+
+fn process_data«1()1» «1{
+ let map: Result<Option<Foo<'_, «2()2»
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+"#},
+ &bracket_colors_markup(&mut cx),
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input(">", window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ indoc! {r#"
+struct Foo«1<'a, T>1» «1{
+ data: Vec«2<Option«3<&'a T>3»>2»,
+}1»
+
+fn process_data«1()1» «1{
+ let map: Result<Option<Foo«2<'_, «3()3»>2»
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+"#},
+ &bracket_colors_markup(&mut cx),
+ "When brackets start to get closed, inner brackets are re-colored based on their depth"
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input(">", window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ indoc! {r#"
+struct Foo«1<'a, T>1» «1{
+ data: Vec«2<Option«3<&'a T>3»>2»,
+}1»
+
+fn process_data«1()1» «1{
+ let map: Result<Option«2<Foo«3<'_, «4()4»>3»>2»
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+"#},
+ &bracket_colors_markup(&mut cx),
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input(", ()> = unimplemented!();", window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ indoc! {r#"
+struct Foo«1<'a, T>1» «1{
+ data: Vec«2<Option«3<&'a T>3»>2»,
+}1»
+
+fn process_data«1()1» «1{
+ let map: Result«2<Option«3<Foo«4<'_, «5()5»>4»>3», «3()3»>2» = unimplemented!«2()2»;
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#},
+ &bracket_colors_markup(&mut cx),
+ );
+ }
+
+ #[gpui::test]
+ async fn test_bracket_colorization_chunks(cx: &mut gpui::TestAppContext) {
+ let comment_lines = 100;
+
+ init_test(cx, |language_settings| {
+ language_settings.defaults.colorize_brackets = Some(true);
+ });
+ let mut cx = EditorLspTestContext::new(
+ Arc::into_inner(rust_lang()).unwrap(),
+ lsp::ServerCapabilities::default(),
+ cx,
+ )
+ .await;
+
+ cx.set_state(&separate_with_comment_lines(
+ indoc! {r#"
+mod foo {
+ ˇfn process_data_1() {
+ let map: Option<Vec<()>> = None;
+ }
+"#},
+ indoc! {r#"
+ fn process_data_2() {
+ let map: Option<Vec<()>> = None;
+ }
+}
+"#},
+ comment_lines,
+ ));
+
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ &separate_with_comment_lines(
+ indoc! {r#"
+mod foo «1{
+ fn process_data_1«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }2»
+"#},
+ indoc! {r#"
+ fn process_data_2() {
+ let map: Option<Vec<()>> = None;
+ }
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#},
+ comment_lines,
+ ),
+ &bracket_colors_markup(&mut cx),
+ "First, the only visible chunk is getting the bracket highlights"
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ editor.move_to_end(&MoveToEnd, window, cx);
+ editor.move_up(&MoveUp, window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ &separate_with_comment_lines(
+ indoc! {r#"
+mod foo «1{
+ fn process_data_1«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }2»
+"#},
+ indoc! {r#"
+ fn process_data_2«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }2»
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#},
+ comment_lines,
+ ),
+ &bracket_colors_markup(&mut cx),
+ "After scrolling to the bottom, both chunks should have the highlights"
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ editor.handle_input("{{}}}", window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ &separate_with_comment_lines(
+ indoc! {r#"
+mod foo «1{
+ fn process_data_1() {
+ let map: Option<Vec<()>> = None;
+ }
+"#},
+ indoc! {r#"
+ fn process_data_2«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }
+ «3{«4{}4»}3»}2»}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#},
+ comment_lines,
+ ),
+ &bracket_colors_markup(&mut cx),
+ "First chunk's brackets are invalidated after an edit, and only 2nd (visible) chunk is re-colorized"
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ editor.move_to_beginning(&MoveToBeginning, window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ assert_eq!(
+ &separate_with_comment_lines(
+ indoc! {r#"
+mod foo «1{
+ fn process_data_1«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }2»
+"#},
+ indoc! {r#"
+ fn process_data_2«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }
+ «3{«4{}4»}3»}2»}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#},
+ comment_lines,
+ ),
+ &bracket_colors_markup(&mut cx),
+ "Scrolling back to top should re-colorize all chunks' brackets"
+ );
+
+ cx.update(|_, cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings(cx, |settings| {
+ settings.project.all_languages.defaults.colorize_brackets = Some(false);
+ });
+ });
+ });
+ assert_eq!(
+ &separate_with_comment_lines(
+ indoc! {r#"
+mod foo {
+ fn process_data_1() {
+ let map: Option<Vec<()>> = None;
+ }
+"#},
+ r#" fn process_data_2() {
+ let map: Option<Vec<()>> = None;
+ }
+ {{}}}}
+
+"#,
+ comment_lines,
+ ),
+ &bracket_colors_markup(&mut cx),
+ "Turning bracket colorization off should remove all bracket colors"
+ );
+
+ cx.update(|_, cx| {
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings(cx, |settings| {
+ settings.project.all_languages.defaults.colorize_brackets = Some(true);
+ });
+ });
+ });
+ assert_eq!(
+ &separate_with_comment_lines(
+ indoc! {r#"
+mod foo «1{
+ fn process_data_1«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }2»
+"#},
+ r#" fn process_data_2() {
+ let map: Option<Vec<()>> = None;
+ }
+ {{}}}}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#,
+ comment_lines,
+ ),
+ &bracket_colors_markup(&mut cx),
+ "Turning bracket colorization back on refreshes the visible excerpts' bracket colors"
+ );
+ }
+
+ #[gpui::test]
+ async fn test_rainbow_bracket_highlights(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |language_settings| {
+ language_settings.defaults.colorize_brackets = Some(true);
+ });
+ let mut cx = EditorLspTestContext::new(
+ Arc::into_inner(rust_lang()).unwrap(),
+ lsp::ServerCapabilities::default(),
+ cx,
+ )
+ .await;
+
+ // taken from r-a https://github.com/rust-lang/rust-analyzer/blob/d733c07552a2dc0ec0cc8f4df3f0ca969a93fd90/crates/ide/src/inlay_hints.rs#L81-L297
+ cx.set_state(indoc! {r#"ˇ
+ pub(crate) fn inlay_hints(
+ db: &RootDatabase,
+ file_id: FileId,
+ range_limit: Option<TextRange>,
+ config: &InlayHintsConfig,
+ ) -> Vec<InlayHint> {
+ let _p = tracing::info_span!("inlay_hints").entered();
+ let sema = Semantics::new(db);
+ let file_id = sema
+ .attach_first_edition(file_id)
+ .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
+ let file = sema.parse(file_id);
+ let file = file.syntax();
+
+ let mut acc = Vec::new();
+
+ let Some(scope) = sema.scope(file) else {
+ return acc;
+ };
+ let famous_defs = FamousDefs(&sema, scope.krate());
+ let display_target = famous_defs.1.to_display_target(sema.db);
+
+ let ctx = &mut InlayHintCtx::default();
+ let mut hints = |event| {
+ if let Some(node) = handle_event(ctx, event) {
+ hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
+ }
+ };
+ let mut preorder = file.preorder();
+ salsa::attach(sema.db, || {
+ while let Some(event) = preorder.next() {
+ if matches!((&event, range_limit), (WalkEvent::Enter(node), Some(range)) if range.intersect(node.text_range()).is_none())
+ {
+ preorder.skip_subtree();
+ continue;
+ }
+ hints(event);
+ }
+ });
+ if let Some(range_limit) = range_limit {
+ acc.retain(|hint| range_limit.contains_range(hint.range));
+ }
+ acc
+ }
+
+ #[derive(Default)]
+ struct InlayHintCtx {
+ lifetime_stacks: Vec<Vec<SmolStr>>,
+ extern_block_parent: Option<ast::ExternBlock>,
+ }
+
+ pub(crate) fn inlay_hints_resolve(
+ db: &RootDatabase,
+ file_id: FileId,
+ resolve_range: TextRange,
+ hash: u64,
+ config: &InlayHintsConfig,
+ hasher: impl Fn(&InlayHint) -> u64,
+ ) -> Option<InlayHint> {
+ let _p = tracing::info_span!("inlay_hints_resolve").entered();
+ let sema = Semantics::new(db);
+ let file_id = sema
+ .attach_first_edition(file_id)
+ .unwrap_or_else(|| EditionedFileId::current_edition(db, file_id));
+ let file = sema.parse(file_id);
+ let file = file.syntax();
+
+ let scope = sema.scope(file)?;
+ let famous_defs = FamousDefs(&sema, scope.krate());
+ let mut acc = Vec::new();
+
+ let display_target = famous_defs.1.to_display_target(sema.db);
+
+ let ctx = &mut InlayHintCtx::default();
+ let mut hints = |event| {
+ if let Some(node) = handle_event(ctx, event) {
+ hints(&mut acc, ctx, &famous_defs, config, file_id, display_target, node);
+ }
+ };
+
+ let mut preorder = file.preorder();
+ while let Some(event) = preorder.next() {
+ // This can miss some hints that require the parent of the range to calculate
+ if matches!(&event, WalkEvent::Enter(node) if resolve_range.intersect(node.text_range()).is_none())
+ {
+ preorder.skip_subtree();
+ continue;
+ }
+ hints(event);
+ }
+ acc.into_iter().find(|hint| hasher(hint) == hash)
+ }
+
+ fn handle_event(ctx: &mut InlayHintCtx, node: WalkEvent<SyntaxNode>) -> Option<SyntaxNode> {
+ match node {
+ WalkEvent::Enter(node) => {
+ if let Some(node) = ast::AnyHasGenericParams::cast(node.clone()) {
+ let params = node
+ .generic_param_list()
+ .map(|it| {
+ it.lifetime_params()
+ .filter_map(|it| {
+ it.lifetime().map(|it| format_smolstr!("{}", &it.text()[1..]))
+ })
+ .collect()
+ })
+ .unwrap_or_default();
+ ctx.lifetime_stacks.push(params);
+ }
+ if let Some(node) = ast::ExternBlock::cast(node.clone()) {
+ ctx.extern_block_parent = Some(node);
+ }
+ Some(node)
+ }
+ WalkEvent::Leave(n) => {
+ if ast::AnyHasGenericParams::can_cast(n.kind()) {
+ ctx.lifetime_stacks.pop();
+ }
+ if ast::ExternBlock::can_cast(n.kind()) {
+ ctx.extern_block_parent = None;
+ }
+ None
+ }
+ }
+ }
+
+ // At some point when our hir infra is fleshed out enough we should flip this and traverse the
+ // HIR instead of the syntax tree.
+ fn hints(
+ hints: &mut Vec<InlayHint>,
+ ctx: &mut InlayHintCtx,
+ famous_defs @ FamousDefs(sema, _krate): &FamousDefs<'_, '_>,
+ config: &InlayHintsConfig,
+ file_id: EditionedFileId,
+ display_target: DisplayTarget,
+ node: SyntaxNode,
+ ) {
+ closing_brace::hints(
+ hints,
+ sema,
+ config,
+ display_target,
+ InRealFile { file_id, value: node.clone() },
+ );
+ if let Some(any_has_generic_args) = ast::AnyHasGenericArgs::cast(node.clone()) {
+ generic_param::hints(hints, famous_defs, config, any_has_generic_args);
+ }
+
+ match_ast! {
+ match node {
+ ast::Expr(expr) => {
+ chaining::hints(hints, famous_defs, config, display_target, &expr);
+ adjustment::hints(hints, famous_defs, config, display_target, &expr);
+ match expr {
+ ast::Expr::CallExpr(it) => param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it)),
+ ast::Expr::MethodCallExpr(it) => {
+ param_name::hints(hints, famous_defs, config, file_id, ast::Expr::from(it))
+ }
+ ast::Expr::ClosureExpr(it) => {
+ closure_captures::hints(hints, famous_defs, config, it.clone());
+ closure_ret::hints(hints, famous_defs, config, display_target, it)
+ },
+ ast::Expr::RangeExpr(it) => range_exclusive::hints(hints, famous_defs, config, it),
+ _ => Some(()),
+ }
+ },
+ ast::Pat(it) => {
+ binding_mode::hints(hints, famous_defs, config, &it);
+ match it {
+ ast::Pat::IdentPat(it) => {
+ bind_pat::hints(hints, famous_defs, config, display_target, &it);
+ }
+ ast::Pat::RangePat(it) => {
+ range_exclusive::hints(hints, famous_defs, config, it);
+ }
+ _ => {}
+ }
+ Some(())
+ },
+ ast::Item(it) => match it {
+ ast::Item::Fn(it) => {
+ implicit_drop::hints(hints, famous_defs, config, display_target, &it);
+ if let Some(extern_block) = &ctx.extern_block_parent {
+ extern_block::fn_hints(hints, famous_defs, config, &it, extern_block);
+ }
+ lifetime::fn_hints(hints, ctx, famous_defs, config, it)
+ },
+ ast::Item::Static(it) => {
+ if let Some(extern_block) = &ctx.extern_block_parent {
+ extern_block::static_hints(hints, famous_defs, config, &it, extern_block);
+ }
+ implicit_static::hints(hints, famous_defs, config, Either::Left(it))
+ },
+ ast::Item::Const(it) => implicit_static::hints(hints, famous_defs, config, Either::Right(it)),
+ ast::Item::Enum(it) => discriminant::enum_hints(hints, famous_defs, config, it),
+ ast::Item::ExternBlock(it) => extern_block::extern_block_hints(hints, famous_defs, config, it),
+ _ => None,
+ },
+ // trait object type elisions
+ ast::Type(ty) => match ty {
+ ast::Type::FnPtrType(ptr) => lifetime::fn_ptr_hints(hints, ctx, famous_defs, config, ptr),
+ ast::Type::PathType(path) => {
+ lifetime::fn_path_hints(hints, ctx, famous_defs, config, &path);
+ implied_dyn_trait::hints(hints, famous_defs, config, Either::Left(path));
+ Some(())
+ },
+ ast::Type::DynTraitType(dyn_) => {
+ implied_dyn_trait::hints(hints, famous_defs, config, Either::Right(dyn_));
+ Some(())
+ },
+ _ => Some(()),
+ },
+ ast::GenericParamList(it) => bounds::hints(hints, famous_defs, config, it),
+ _ => Some(()),
+ }
+ };
+ }
+ "#});
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+
+ let actual_ranges = cx.update_editor(|editor, window, cx| {
+ editor
+ .snapshot(window, cx)
+ .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
+ });
+
+ let mut highlighted_brackets = HashMap::default();
+ for (color, range) in actual_ranges.iter().cloned() {
+ highlighted_brackets.insert(range, color);
+ }
+
+ let last_bracket = actual_ranges
+ .iter()
+ .max_by_key(|(_, p)| p.end.row)
+ .unwrap()
+ .clone();
+
+ cx.update_editor(|editor, window, cx| {
+ let was_scrolled = editor.set_scroll_position(
+ gpui::Point::new(0.0, last_bracket.1.end.row as f64 * 2.0),
+ window,
+ cx,
+ );
+ assert!(was_scrolled.0);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+
+ let ranges_after_scrolling = cx.update_editor(|editor, window, cx| {
+ editor
+ .snapshot(window, cx)
+ .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
+ });
+ let new_last_bracket = ranges_after_scrolling
+ .iter()
+ .max_by_key(|(_, p)| p.end.row)
+ .unwrap()
+ .clone();
+
+ assert_ne!(
+ last_bracket, new_last_bracket,
+ "After scrolling down, we should have highlighted more brackets"
+ );
+
+ cx.update_editor(|editor, window, cx| {
+ let was_scrolled = editor.set_scroll_position(gpui::Point::default(), window, cx);
+ assert!(was_scrolled.0);
+ });
+
+ for _ in 0..200 {
+ cx.update_editor(|editor, window, cx| {
+ editor.apply_scroll_delta(gpui::Point::new(0.0, 0.25), window, cx);
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+
+ let colored_brackets = cx.update_editor(|editor, window, cx| {
+ editor
+ .snapshot(window, cx)
+ .all_text_highlight_ranges::<ColorizedBracketsHighlight>()
+ });
+ for (color, range) in colored_brackets.clone() {
+ assert!(
+ highlighted_brackets.entry(range).or_insert(color) == &color,
+ "Colors should stay consistent while scrolling!"
+ );
+ }
+
+ let snapshot = cx.update_editor(|editor, window, cx| editor.snapshot(window, cx));
+ let scroll_position = snapshot.scroll_position();
+ let visible_lines =
+ cx.update_editor(|editor, _, _| editor.visible_line_count().unwrap());
+ let visible_range = DisplayRow(scroll_position.y as u32)
+ ..DisplayRow((scroll_position.y + visible_lines) as u32);
+
+ let current_highlighted_bracket_set: HashSet<Point> = HashSet::from_iter(
+ colored_brackets
+ .iter()
+ .flat_map(|(_, range)| [range.start, range.end]),
+ );
+
+ for highlight_range in highlighted_brackets.keys().filter(|bracket_range| {
+ visible_range.contains(&bracket_range.start.to_display_point(&snapshot).row())
+ || visible_range.contains(&bracket_range.end.to_display_point(&snapshot).row())
+ }) {
+ assert!(
+ current_highlighted_bracket_set.contains(&highlight_range.start)
+ || current_highlighted_bracket_set.contains(&highlight_range.end),
+ "Should not lose highlights while scrolling in the visible range!"
+ );
+ }
+
+ let buffer_snapshot = snapshot.buffer().as_singleton().unwrap().2;
+ for bracket_match in buffer_snapshot
+ .fetch_bracket_ranges(
+ snapshot
+ .display_point_to_point(
+ DisplayPoint::new(visible_range.start, 0),
+ Bias::Left,
+ )
+ .to_offset(&buffer_snapshot)
+ ..snapshot
+ .display_point_to_point(
+ DisplayPoint::new(
+ visible_range.end,
+ snapshot.line_len(visible_range.end),
+ ),
+ Bias::Right,
+ )
+ .to_offset(&buffer_snapshot),
+ None,
+ )
+ .iter()
+ .flat_map(|entry| entry.1)
+ .filter(|bracket_match| bracket_match.color_index.is_some())
+ {
+ let start = bracket_match.open_range.to_point(buffer_snapshot);
+ let end = bracket_match.close_range.to_point(buffer_snapshot);
+ let start_bracket = colored_brackets.iter().find(|(_, range)| *range == start);
+ assert!(
+ start_bracket.is_some(),
+ "Existing bracket start in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
+ buffer_snapshot
+ .text_for_range(start.start..end.end)
+ .collect::<String>(),
+ start
+ );
+
+ let end_bracket = colored_brackets.iter().find(|(_, range)| *range == end);
+ assert!(
+ end_bracket.is_some(),
+ "Existing bracket end in the visible range should be highlighted. Missing color for match: \"{}\" at position {:?}",
+ buffer_snapshot
+ .text_for_range(start.start..end.end)
+ .collect::<String>(),
+ start
+ );
+
+ assert_eq!(
+ start_bracket.unwrap().0,
+ end_bracket.unwrap().0,
+ "Bracket pair should be highlighted the same color!"
+ )
+ }
+ }
+ }
+
+ #[gpui::test]
+ async fn test_multi_buffer(cx: &mut gpui::TestAppContext) {
+ let comment_lines = 100;
+
+ init_test(cx, |language_settings| {
+ language_settings.defaults.colorize_brackets = Some(true);
+ });
+ let fs = FakeFs::new(cx.background_executor.clone());
+ fs.insert_tree(
+ path!("/a"),
+ json!({
+ "main.rs": "fn main() {{()}}",
+ "lib.rs": separate_with_comment_lines(
+ indoc! {r#"
+ mod foo {
+ fn process_data_1() {
+ let map: Option<Vec<()>> = None;
+ // a
+ // b
+ // c
+ }
+ "#},
+ indoc! {r#"
+ fn process_data_2() {
+ let other_map: Option<Vec<()>> = None;
+ }
+ }
+ "#},
+ comment_lines,
+ )
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
+ let language_registry = project.read_with(cx, |project, _| project.languages().clone());
+ language_registry.add(rust_lang());
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer(path!("/a/lib.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer(path!("/a/main.rs"), cx)
+ })
+ .await
+ .unwrap();
+
+ let multi_buffer = cx.new(|cx| {
+ let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
+ multi_buffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
+ cx,
+ );
+
+ let excerpt_rows = 5;
+ let rest_of_first_except_rows = 3;
+ multi_buffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ ExcerptRange::new(Point::new(0, 0)..Point::new(excerpt_rows, 0)),
+ ExcerptRange::new(
+ Point::new(
+ comment_lines as u32 + excerpt_rows + rest_of_first_except_rows,
+ 0,
+ )
+ ..Point::new(
+ comment_lines as u32
+ + excerpt_rows
+ + rest_of_first_except_rows
+ + excerpt_rows,
+ 0,
+ ),
+ ),
+ ],
+ cx,
+ );
+ multi_buffer
+ });
+
+ let editor = cx.add_window(|window, cx| {
+ Editor::for_multibuffer(multi_buffer, Some(project.clone()), window, cx)
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+
+ let editor_snapshot = editor
+ .update(cx, |editor, window, cx| editor.snapshot(window, cx))
+ .unwrap();
+ assert_eq!(
+ indoc! {r#"
+
+
+fn main«1()1» «1{«2{«3()3»}2»}1»
+
+
+mod foo «1{
+ fn process_data_1«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ // a
+ // b
+
+
+ fn process_data_2«2()2» «2{
+ let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }2»
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#,},
+ &editor_bracket_colors_markup(&editor_snapshot),
+ "Multi buffers should have their brackets colored even if no excerpts contain the bracket counterpart (after fn `process_data_2()`) \
+or if the buffer pair spans across multiple excerpts (the one after `mod foo`)"
+ );
+
+ editor
+ .update(cx, |editor, window, cx| {
+ editor.handle_input("{[]", window, cx);
+ })
+ .unwrap();
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ let editor_snapshot = editor
+ .update(cx, |editor, window, cx| editor.snapshot(window, cx))
+ .unwrap();
+ assert_eq!(
+ indoc! {r#"
+
+
+{«1[]1»fn main«1()1» «1{«2{«3()3»}2»}1»
+
+
+mod foo «1{
+ fn process_data_1«2()2» «2{
+ let map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ // a
+ // b
+
+
+ fn process_data_2«2()2» «2{
+ let other_map: Option«3<Vec«4<«5()5»>4»>3» = None;
+ }2»
+}1»
+
+1 hsla(207.80, 16.20%, 69.19%, 1.00)
+2 hsla(29.00, 54.00%, 65.88%, 1.00)
+3 hsla(286.00, 51.00%, 75.25%, 1.00)
+4 hsla(187.00, 47.00%, 59.22%, 1.00)
+5 hsla(355.00, 65.00%, 75.94%, 1.00)
+"#,},
+ &editor_bracket_colors_markup(&editor_snapshot),
+ );
+
+ cx.update(|cx| {
+ let theme = cx.theme().name.clone();
+ SettingsStore::update_global(cx, |store, cx| {
+ store.update_user_settings(cx, |settings| {
+ settings.theme.theme_overrides = HashMap::from_iter([(
+ theme.to_string(),
+ ThemeStyleContent {
+ accents: vec![
+ AccentContent(Some(SharedString::new("#ff0000"))),
+ AccentContent(Some(SharedString::new("#0000ff"))),
+ ],
+ ..ThemeStyleContent::default()
+ },
+ )]);
+ });
+ });
+ });
+ cx.executor().advance_clock(Duration::from_millis(100));
+ cx.executor().run_until_parked();
+ let editor_snapshot = editor
+ .update(cx, |editor, window, cx| editor.snapshot(window, cx))
+ .unwrap();
+ assert_eq!(
+ indoc! {r#"
+
+
+{«1[]1»fn main«1()1» «1{«2{«1()1»}2»}1»
+
+
+mod foo «1{
+ fn process_data_1«2()2» «2{
+ let map: Option«1<Vec«2<«1()1»>2»>1» = None;
+ // a
+ // b
+
+
+ fn process_data_2«2()2» «2{
+ let other_map: Option«1<Vec«2<«1()1»>2»>1» = None;
+ }2»
+}1»
+
+1 hsla(0.00, 100.00%, 78.12%, 1.00)
+2 hsla(240.00, 100.00%, 82.81%, 1.00)
+"#,},
+ &editor_bracket_colors_markup(&editor_snapshot),
+ "After updating theme accents, the editor should update the bracket coloring"
+ );
+ }
+
+ fn separate_with_comment_lines(head: &str, tail: &str, comment_lines: usize) -> String {
+ let mut result = head.to_string();
+ result.push_str("\n");
+ result.push_str(&"//\n".repeat(comment_lines));
+ result.push_str(tail);
+ result
+ }
+
+ fn bracket_colors_markup(cx: &mut EditorTestContext) -> String {
+ cx.update_editor(|editor, window, cx| {
+ editor_bracket_colors_markup(&editor.snapshot(window, cx))
+ })
+ }
+
+ fn editor_bracket_colors_markup(snapshot: &EditorSnapshot) -> String {
+ fn display_point_to_offset(text: &str, point: DisplayPoint) -> usize {
+ let mut offset = 0;
+ for (row_idx, line) in text.lines().enumerate() {
+ if row_idx < point.row().0 as usize {
+ offset += line.len() + 1; // +1 for newline
+ } else {
+ offset += point.column() as usize;
+ break;
+ }
+ }
+ offset
+ }
+
+ let actual_ranges = snapshot.all_text_highlight_ranges::<ColorizedBracketsHighlight>();
+ let editor_text = snapshot.text();
+
+ let mut next_index = 1;
+ let mut color_to_index = HashMap::default();
+ let mut annotations = Vec::new();
+ for (color, range) in &actual_ranges {
+ let color_index = *color_to_index
+ .entry(*color)
+ .or_insert_with(|| post_inc(&mut next_index));
+ let start = snapshot.point_to_display_point(range.start, Bias::Left);
+ let end = snapshot.point_to_display_point(range.end, Bias::Right);
+ let start_offset = display_point_to_offset(&editor_text, start);
+ let end_offset = display_point_to_offset(&editor_text, end);
+ let bracket_text = &editor_text[start_offset..end_offset];
+ let bracket_char = bracket_text.chars().next().unwrap();
+
+ if matches!(bracket_char, '{' | '[' | '(' | '<') {
+ annotations.push((start_offset, format!("«{color_index}")));
+ } else {
+ annotations.push((end_offset, format!("{color_index}»")));
+ }
+ }
+
+ annotations.sort_by(|(pos_a, text_a), (pos_b, text_b)| {
+ pos_a.cmp(pos_b).reverse().then_with(|| {
+ let a_is_opening = text_a.starts_with('«');
+ let b_is_opening = text_b.starts_with('«');
+ match (a_is_opening, b_is_opening) {
+ (true, false) => cmp::Ordering::Less,
+ (false, true) => cmp::Ordering::Greater,
+ _ => cmp::Ordering::Equal,
+ }
+ })
+ });
+ annotations.dedup();
+
+ let mut markup = editor_text;
+ for (offset, text) in annotations {
+ markup.insert_str(offset, &text);
+ }
+
+ markup.push_str("\n");
+ for (index, color) in color_to_index
+ .iter()
+ .map(|(color, index)| (*index, *color))
+ .sorted_by_key(|(index, _)| *index)
+ {
+ markup.push_str(&format!("{index} {color}\n"));
+ }
+
+ markup
+ }
+}
@@ -483,8 +483,26 @@ impl DisplayMap {
key: HighlightKey,
ranges: Vec<Range<Anchor>>,
style: HighlightStyle,
+ merge: bool,
+ cx: &App,
) {
- self.text_highlights.insert(key, Arc::new((style, ranges)));
+ let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let to_insert = match self.text_highlights.remove(&key).filter(|_| merge) {
+ Some(previous) => {
+ let mut merged_ranges = previous.1.clone();
+ for new_range in ranges {
+ let i = merged_ranges
+ .binary_search_by(|probe| {
+ probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
+ })
+ .unwrap_or_else(|i| i);
+ merged_ranges.insert(i, new_range);
+ }
+ Arc::new((style, merged_ranges))
+ }
+ None => Arc::new((style, ranges)),
+ };
+ self.text_highlights.insert(key, to_insert);
}
pub(crate) fn highlight_inlays(
@@ -523,6 +541,15 @@ impl DisplayMap {
.text_highlights
.remove(&HighlightKey::Type(type_id))
.is_some();
+ self.text_highlights.retain(|key, _| {
+ let retain = if let HighlightKey::TypePlus(key_type_id, _) = key {
+ key_type_id != &type_id
+ } else {
+ true
+ };
+ cleared |= !retain;
+ retain
+ });
cleared |= self.inlay_highlights.remove(&type_id).is_some();
cleared
}
@@ -1382,6 +1409,33 @@ impl DisplaySnapshot {
.cloned()
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn all_text_highlight_ranges<Tag: ?Sized + 'static>(
+ &self,
+ ) -> Vec<(gpui::Hsla, Range<Point>)> {
+ use itertools::Itertools;
+
+ let required_type_id = TypeId::of::<Tag>();
+ self.text_highlights
+ .iter()
+ .filter(|(key, _)| match key {
+ HighlightKey::Type(type_id) => type_id == &required_type_id,
+ HighlightKey::TypePlus(type_id, _) => type_id == &required_type_id,
+ })
+ .map(|(_, value)| value.clone())
+ .flat_map(|ranges| {
+ ranges
+ .1
+ .iter()
+ .flat_map(|range| {
+ Some((ranges.0.color?, range.to_point(self.buffer_snapshot())))
+ })
+ .collect::<Vec<_>>()
+ })
+ .sorted_by_key(|(_, range)| range.start)
+ .collect()
+ }
+
#[allow(unused)]
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn inlay_highlights<Tag: ?Sized + 'static>(
@@ -2387,6 +2441,8 @@ pub mod tests {
..buffer_snapshot.anchor_after(Point::new(3, 18)),
],
red.into(),
+ false,
+ cx,
);
map.insert_blocks(
[BlockProperties {
@@ -2698,7 +2754,7 @@ pub mod tests {
..Default::default()
};
- map.update(cx, |map, _cx| {
+ map.update(cx, |map, cx| {
map.highlight_text(
HighlightKey::Type(TypeId::of::<MyType>()),
highlighted_ranges
@@ -2710,6 +2766,8 @@ pub mod tests {
})
.collect(),
style,
+ false,
+ cx,
);
});
@@ -40,7 +40,6 @@ impl<'a> CustomHighlightsChunks<'a> {
buffer_chunks: multibuffer_snapshot.chunks(range.clone(), language_aware),
buffer_chunk: None,
offset: range.start,
-
text_highlights,
highlight_endpoints: create_highlight_endpoints(
&range,
@@ -75,16 +74,9 @@ fn create_highlight_endpoints(
let style = text_highlights.0;
let ranges = &text_highlights.1;
- let start_ix = match ranges.binary_search_by(|probe| {
- let cmp = probe.end.cmp(&start, buffer);
- if cmp.is_gt() {
- cmp::Ordering::Greater
- } else {
- cmp::Ordering::Less
- }
- }) {
- Ok(i) | Err(i) => i,
- };
+ let start_ix = ranges
+ .binary_search_by(|probe| probe.end.cmp(&start, buffer).then(cmp::Ordering::Less))
+ .unwrap_or_else(|i| i);
for range in &ranges[start_ix..] {
if range.start.cmp(&end, buffer).is_ge() {
@@ -13,6 +13,7 @@
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior.
pub mod actions;
mod blink_manager;
+mod bracket_colorization;
mod clangd_ext;
pub mod code_context_menus;
pub mod display_map;
@@ -118,11 +119,11 @@ use language::{
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
- IndentSize, Language, LanguageRegistry, OffsetRangeExt, OutlineItem, Point, Runnable,
- Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
+ IndentSize, Language, LanguageName, LanguageRegistry, OffsetRangeExt, OutlineItem, Point,
+ Runnable, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
language_settings::{
- self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
- language_settings,
+ self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
+ all_language_settings, language_settings,
},
point_from_lsp, point_to_lsp, text_diff_with_options,
};
@@ -175,6 +176,7 @@ use std::{
borrow::Cow,
cell::{OnceCell, RefCell},
cmp::{self, Ordering, Reverse},
+ collections::hash_map,
iter::{self, Peekable},
mem,
num::NonZeroU32,
@@ -1193,6 +1195,9 @@ pub struct Editor {
folding_newlines: Task<()>,
select_next_is_case_sensitive: Option<bool>,
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
+ applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
+ accent_overrides: Vec<SharedString>,
+ fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
}
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
@@ -2333,12 +2338,18 @@ impl Editor {
folding_newlines: Task::ready(()),
lookup_key: None,
select_next_is_case_sensitive: None,
+ applicable_language_settings: HashMap::default(),
+ accent_overrides: Vec::new(),
+ fetched_tree_sitter_chunks: HashMap::default(),
};
if is_minimap {
return editor;
}
+ editor.applicable_language_settings = editor.fetch_applicable_language_settings(cx);
+ editor.accent_overrides = editor.fetch_accent_overrides(cx);
+
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
._subscriptions
@@ -2378,6 +2389,7 @@ impl Editor {
InlayHintRefreshReason::NewLinesShown,
cx,
);
+ editor.colorize_brackets(false, cx);
})
.ok();
});
@@ -21141,13 +21153,16 @@ impl Editor {
key: usize,
ranges: Vec<Range<Anchor>>,
style: HighlightStyle,
+ merge: bool,
cx: &mut Context<Self>,
) {
- self.display_map.update(cx, |map, _| {
+ self.display_map.update(cx, |map, cx| {
map.highlight_text(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
ranges,
style,
+ merge,
+ cx,
);
});
cx.notify();
@@ -21159,8 +21174,14 @@ impl Editor {
style: HighlightStyle,
cx: &mut Context<Self>,
) {
- self.display_map.update(cx, |map, _| {
- map.highlight_text(HighlightKey::Type(TypeId::of::<T>()), ranges, style)
+ self.display_map.update(cx, |map, cx| {
+ map.highlight_text(
+ HighlightKey::Type(TypeId::of::<T>()),
+ ranges,
+ style,
+ false,
+ cx,
+ )
});
cx.notify();
}
@@ -21308,7 +21329,6 @@ impl Editor {
self.active_indent_guides_state.dirty = true;
self.refresh_active_diagnostics(cx);
self.refresh_code_actions(window, cx);
- self.refresh_selected_text_highlights(true, window, cx);
self.refresh_single_line_folds(window, cx);
self.refresh_matching_bracket_highlights(window, cx);
if self.has_active_edit_prediction() {
@@ -21364,6 +21384,7 @@ impl Editor {
}
self.update_lsp_data(Some(buffer_id), window, cx);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+ self.colorize_brackets(false, cx);
cx.emit(EditorEvent::ExcerptsAdded {
buffer: buffer.clone(),
predecessor: *predecessor,
@@ -21401,10 +21422,16 @@ impl Editor {
multi_buffer::Event::ExcerptsExpanded { ids } => {
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
self.refresh_document_highlights(cx);
+ for id in ids {
+ self.fetched_tree_sitter_chunks.remove(id);
+ }
+ self.colorize_brackets(false, cx);
cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
}
multi_buffer::Event::Reparsed(buffer_id) => {
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
+ self.refresh_selected_text_highlights(true, window, cx);
+ self.colorize_brackets(true, cx);
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
cx.emit(EditorEvent::Reparsed(*buffer_id));
@@ -21475,7 +21502,52 @@ impl Editor {
cx.notify();
}
+ fn fetch_accent_overrides(&self, cx: &App) -> Vec<SharedString> {
+ if !self.mode.is_full() {
+ return Vec::new();
+ }
+
+ theme::ThemeSettings::get_global(cx)
+ .theme_overrides
+ .get(cx.theme().name.as_ref())
+ .map(|theme_style| &theme_style.accents)
+ .into_iter()
+ .flatten()
+ .flat_map(|accent| accent.0.clone())
+ .collect()
+ }
+
+ fn fetch_applicable_language_settings(
+ &self,
+ cx: &App,
+ ) -> HashMap<Option<LanguageName>, LanguageSettings> {
+ if !self.mode.is_full() {
+ return HashMap::default();
+ }
+
+ self.buffer().read(cx).all_buffers().into_iter().fold(
+ HashMap::default(),
+ |mut acc, buffer| {
+ let buffer = buffer.read(cx);
+ let language = buffer.language().map(|language| language.name());
+ if let hash_map::Entry::Vacant(v) = acc.entry(language.clone()) {
+ let file = buffer.file();
+ v.insert(language_settings(language, file, cx).into_owned());
+ }
+ acc
+ },
+ )
+ }
+
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let new_language_settings = self.fetch_applicable_language_settings(cx);
+ let language_settings_changed = new_language_settings != self.applicable_language_settings;
+ self.applicable_language_settings = new_language_settings;
+
+ let new_accent_overrides = self.fetch_accent_overrides(cx);
+ let accent_overrides_changed = new_accent_overrides != self.accent_overrides;
+ self.accent_overrides = new_accent_overrides;
+
if self.diagnostics_enabled() {
let new_severity = EditorSettings::get_global(cx)
.diagnostics_max_severity
@@ -21547,15 +21619,19 @@ impl Editor {
})
}
}
- }
- if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
- colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
- }) {
- if !inlay_splice.is_empty() {
- self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
+ if language_settings_changed || accent_overrides_changed {
+ self.colorize_brackets(true, cx);
+ }
+
+ if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
+ colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
+ }) {
+ if !inlay_splice.is_empty() {
+ self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
+ }
+ self.refresh_colors_for_visible_range(None, window, cx);
}
- self.refresh_colors_for_visible_range(None, window, cx);
}
cx.notify();
@@ -22668,7 +22744,7 @@ fn insert_extra_newline_tree_sitter(
_ => return false,
};
let pair = {
- let mut result: Option<BracketMatch> = None;
+ let mut result: Option<BracketMatch<usize>> = None;
for pair in buffer
.all_bracket_ranges(range.start.0..range.end.0)
@@ -17565,7 +17565,9 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
- let mut assert = |before, after| {
+
+ #[track_caller]
+ fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
let _state_context = cx.set_state(before);
cx.run_until_parked();
cx.update_editor(|editor, window, cx| {
@@ -17573,30 +17575,33 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
});
cx.run_until_parked();
cx.assert_editor_state(after);
- };
+ }
// Outside bracket jumps to outside of matching bracket
- assert("console.logˇ(var);", "console.log(var)ˇ;");
- assert("console.log(var)ˇ;", "console.logˇ(var);");
+ assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
+ assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
// Inside bracket jumps to inside of matching bracket
- assert("console.log(ˇvar);", "console.log(varˇ);");
- assert("console.log(varˇ);", "console.log(ˇvar);");
+ assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
+ assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
// When outside a bracket and inside, favor jumping to the inside bracket
assert(
"console.log('foo', [1, 2, 3]ˇ);",
- "console.log(ˇ'foo', [1, 2, 3]);",
+ "console.log('foo', ˇ[1, 2, 3]);",
+ &mut cx,
);
assert(
"console.log(ˇ'foo', [1, 2, 3]);",
- "console.log('foo', [1, 2, 3]ˇ);",
+ "console.log('foo'ˇ, [1, 2, 3]);",
+ &mut cx,
);
// Bias forward if two options are equally likely
assert(
"let result = curried_fun()ˇ();",
"let result = curried_fun()()ˇ;",
+ &mut cx,
);
// If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
@@ -17609,6 +17614,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
function test() {
console.logˇ('test')
}"},
+ &mut cx,
);
}
@@ -500,6 +500,7 @@ impl Editor {
editor.register_visible_buffers(cx);
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
editor.update_lsp_data(None, window, cx);
+ editor.colorize_brackets(false, cx);
})
.ok();
});
@@ -1,9 +1,12 @@
+pub mod row_chunk;
+
use crate::{
DebuggerTextObject, LanguageScope, Outline, OutlineConfig, RunnableCapture, RunnableTag,
TextObject, TreeSitterOptions,
diagnostic_set::{DiagnosticEntry, DiagnosticEntryRef, DiagnosticGroup},
language_settings::{LanguageSettings, language_settings},
outline::OutlineItem,
+ row_chunk::RowChunks,
syntax_map::{
SyntaxLayer, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxMapMatch,
SyntaxMapMatches, SyntaxSnapshot, ToTreeSitterPoint,
@@ -18,9 +21,9 @@ pub use crate::{
proto,
};
use anyhow::{Context as _, Result};
-use clock::Lamport;
pub use clock::ReplicaId;
-use collections::HashMap;
+use clock::{Global, Lamport};
+use collections::{HashMap, HashSet};
use fs::MTime;
use futures::channel::oneshot;
use gpui::{
@@ -28,8 +31,9 @@ use gpui::{
Task, TaskLabel, TextStyle,
};
+use itertools::Itertools;
use lsp::{LanguageServerId, NumberOrString};
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RawMutex, lock_api::MutexGuard};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use settings::WorktreeId;
@@ -45,7 +49,7 @@ use std::{
iter::{self, Iterator, Peekable},
mem,
num::NonZeroU32,
- ops::{Deref, Range},
+ ops::{Deref, Not, Range},
path::PathBuf,
rc,
sync::{Arc, LazyLock},
@@ -126,6 +130,29 @@ pub struct Buffer {
has_unsaved_edits: Cell<(clock::Global, bool)>,
change_bits: Vec<rc::Weak<Cell<bool>>>,
_subscriptions: Vec<gpui::Subscription>,
+ tree_sitter_data: Arc<Mutex<TreeSitterData>>,
+}
+
+#[derive(Debug, Clone)]
+pub struct TreeSitterData {
+ chunks: RowChunks,
+ brackets_by_chunks: Vec<Option<Vec<BracketMatch<usize>>>>,
+}
+
+const MAX_ROWS_IN_A_CHUNK: u32 = 50;
+
+impl TreeSitterData {
+ fn clear(&mut self) {
+ self.brackets_by_chunks = vec![None; self.chunks.len()];
+ }
+
+ fn new(snapshot: text::BufferSnapshot) -> Self {
+ let chunks = RowChunks::new(snapshot, MAX_ROWS_IN_A_CHUNK);
+ Self {
+ brackets_by_chunks: vec![None; chunks.len()],
+ chunks,
+ }
+ }
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
@@ -149,6 +176,7 @@ pub struct BufferSnapshot {
remote_selections: TreeMap<ReplicaId, SelectionSet>,
language: Option<Arc<Language>>,
non_text_state_update_count: usize,
+ tree_sitter_data: Arc<Mutex<TreeSitterData>>,
}
/// The kind and amount of indentation in a particular line. For now,
@@ -819,11 +847,18 @@ impl EditPreview {
}
#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct BracketMatch {
- pub open_range: Range<usize>,
- pub close_range: Range<usize>,
+pub struct BracketMatch<T> {
+ pub open_range: Range<T>,
+ pub close_range: Range<T>,
pub newline_only: bool,
- pub depth: usize,
+ pub syntax_layer_depth: usize,
+ pub color_index: Option<usize>,
+}
+
+impl<T> BracketMatch<T> {
+ pub fn bracket_ranges(self) -> (Range<T>, Range<T>) {
+ (self.open_range, self.close_range)
+ }
}
impl Buffer {
@@ -974,8 +1009,10 @@ impl Buffer {
let saved_mtime = file.as_ref().and_then(|file| file.disk_state().mtime());
let snapshot = buffer.snapshot();
let syntax_map = Mutex::new(SyntaxMap::new(&snapshot));
+ let tree_sitter_data = TreeSitterData::new(snapshot);
Self {
saved_mtime,
+ tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
saved_version: buffer.version(),
preview_version: buffer.version(),
reload_task: None,
@@ -1025,12 +1062,14 @@ impl Buffer {
let language_registry = language_registry.clone();
syntax.reparse(&text, language_registry, language);
}
+ let tree_sitter_data = TreeSitterData::new(text.clone());
BufferSnapshot {
text,
syntax,
file: None,
diagnostics: Default::default(),
remote_selections: Default::default(),
+ tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
language,
non_text_state_update_count: 0,
}
@@ -1048,9 +1087,11 @@ impl Buffer {
)
.snapshot();
let syntax = SyntaxMap::new(&text).snapshot();
+ let tree_sitter_data = TreeSitterData::new(text.clone());
BufferSnapshot {
text,
syntax,
+ tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
file: None,
diagnostics: Default::default(),
remote_selections: Default::default(),
@@ -1075,9 +1116,11 @@ impl Buffer {
if let Some(language) = language.clone() {
syntax.reparse(&text, language_registry, language);
}
+ let tree_sitter_data = TreeSitterData::new(text.clone());
BufferSnapshot {
text,
syntax,
+ tree_sitter_data: Arc::new(Mutex::new(tree_sitter_data)),
file: None,
diagnostics: Default::default(),
remote_selections: Default::default(),
@@ -1097,6 +1140,7 @@ impl Buffer {
BufferSnapshot {
text,
syntax,
+ tree_sitter_data: self.tree_sitter_data.clone(),
file: self.file.clone(),
remote_selections: self.remote_selections.clone(),
diagnostics: self.diagnostics.clone(),
@@ -1611,6 +1655,7 @@ impl Buffer {
self.syntax_map.lock().did_parse(syntax_snapshot);
self.request_autoindent(cx);
self.parse_status.0.send(ParseStatus::Idle).unwrap();
+ self.tree_sitter_data.lock().clear();
cx.emit(BufferEvent::Reparsed);
cx.notify();
}
@@ -4120,61 +4165,166 @@ impl BufferSnapshot {
self.syntax.matches(range, self, query)
}
- pub fn all_bracket_ranges(
+ /// Finds all [`RowChunks`] applicable to the given range, then returns all bracket pairs that intersect with those chunks.
+ /// Hence, may return more bracket pairs than the range contains.
+ ///
+ /// Will omit known chunks.
+ /// The resulting bracket match collections are not ordered.
+ pub fn fetch_bracket_ranges(
&self,
range: Range<usize>,
- ) -> impl Iterator<Item = BracketMatch> + '_ {
- let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| {
- grammar.brackets_config.as_ref().map(|c| &c.query)
- });
- let configs = matches
- .grammars()
- .iter()
- .map(|grammar| grammar.brackets_config.as_ref().unwrap())
- .collect::<Vec<_>>();
-
- iter::from_fn(move || {
- while let Some(mat) = matches.peek() {
- let mut open = None;
- let mut close = None;
- let depth = mat.depth;
- let config = &configs[mat.grammar_index];
- let pattern = &config.patterns[mat.pattern_index];
- for capture in mat.captures {
- if capture.index == config.open_capture_ix {
- open = Some(capture.node.byte_range());
- } else if capture.index == config.close_capture_ix {
- close = Some(capture.node.byte_range());
- }
+ known_chunks: Option<(&Global, &HashSet<Range<BufferRow>>)>,
+ ) -> HashMap<Range<BufferRow>, Vec<BracketMatch<usize>>> {
+ let mut tree_sitter_data = self.latest_tree_sitter_data().clone();
+
+ let known_chunks = match known_chunks {
+ Some((known_version, known_chunks)) => {
+ if !tree_sitter_data
+ .chunks
+ .version()
+ .changed_since(known_version)
+ {
+ known_chunks.clone()
+ } else {
+ HashSet::default()
}
+ }
+ None => HashSet::default(),
+ };
- matches.advance();
+ let mut new_bracket_matches = HashMap::default();
+ let mut all_bracket_matches = HashMap::default();
- let Some((open_range, close_range)) = open.zip(close) else {
- continue;
- };
+ for chunk in tree_sitter_data
+ .chunks
+ .applicable_chunks(&[self.anchor_before(range.start)..self.anchor_after(range.end)])
+ {
+ if known_chunks.contains(&chunk.row_range()) {
+ continue;
+ }
+ let Some(chunk_range) = tree_sitter_data.chunks.chunk_range(chunk) else {
+ continue;
+ };
+ let chunk_range = chunk_range.to_offset(&tree_sitter_data.chunks.snapshot);
+
+ let bracket_matches = match tree_sitter_data.brackets_by_chunks[chunk.id].take() {
+ Some(cached_brackets) => cached_brackets,
+ None => {
+ let mut bracket_pairs_ends = Vec::new();
+ let mut matches =
+ self.syntax
+ .matches(chunk_range.clone(), &self.text, |grammar| {
+ grammar.brackets_config.as_ref().map(|c| &c.query)
+ });
+ let configs = matches
+ .grammars()
+ .iter()
+ .map(|grammar| grammar.brackets_config.as_ref().unwrap())
+ .collect::<Vec<_>>();
+
+ let chunk_range = chunk_range.clone();
+ let new_matches = iter::from_fn(move || {
+ while let Some(mat) = matches.peek() {
+ let mut open = None;
+ let mut close = None;
+ let depth = mat.depth;
+ let config = configs[mat.grammar_index];
+ let pattern = &config.patterns[mat.pattern_index];
+ for capture in mat.captures {
+ if capture.index == config.open_capture_ix {
+ open = Some(capture.node.byte_range());
+ } else if capture.index == config.close_capture_ix {
+ close = Some(capture.node.byte_range());
+ }
+ }
- let bracket_range = open_range.start..=close_range.end;
- if !bracket_range.overlaps(&range) {
- continue;
+ matches.advance();
+
+ let Some((open_range, close_range)) = open.zip(close) else {
+ continue;
+ };
+
+ let bracket_range = open_range.start..=close_range.end;
+ if !bracket_range.overlaps(&chunk_range) {
+ continue;
+ }
+
+ return Some((open_range, close_range, pattern, depth));
+ }
+ None
+ })
+ .sorted_by_key(|(open_range, _, _, _)| open_range.start)
+ .map(|(open_range, close_range, pattern, syntax_layer_depth)| {
+ while let Some(&last_bracket_end) = bracket_pairs_ends.last() {
+ if last_bracket_end <= open_range.start {
+ bracket_pairs_ends.pop();
+ } else {
+ break;
+ }
+ }
+
+ let bracket_depth = bracket_pairs_ends.len();
+ bracket_pairs_ends.push(close_range.end);
+
+ BracketMatch {
+ open_range,
+ close_range,
+ syntax_layer_depth,
+ newline_only: pattern.newline_only,
+ color_index: pattern.rainbow_exclude.not().then_some(bracket_depth),
+ }
+ })
+ .collect::<Vec<_>>();
+
+ new_bracket_matches.insert(chunk.id, new_matches.clone());
+ new_matches
}
+ };
+ all_bracket_matches.insert(chunk.row_range(), bracket_matches);
+ }
- return Some(BracketMatch {
- open_range,
- close_range,
- newline_only: pattern.newline_only,
- depth,
- });
+ let mut latest_tree_sitter_data = self.latest_tree_sitter_data();
+ if latest_tree_sitter_data.chunks.version() == &self.version {
+ for (chunk_id, new_matches) in new_bracket_matches {
+ let old_chunks = &mut latest_tree_sitter_data.brackets_by_chunks[chunk_id];
+ if old_chunks.is_none() {
+ *old_chunks = Some(new_matches);
+ }
}
- None
- })
+ }
+
+ all_bracket_matches
+ }
+
+ fn latest_tree_sitter_data(&self) -> MutexGuard<'_, RawMutex, TreeSitterData> {
+ let mut tree_sitter_data = self.tree_sitter_data.lock();
+ if self
+ .version
+ .changed_since(tree_sitter_data.chunks.version())
+ {
+ *tree_sitter_data = TreeSitterData::new(self.text.clone());
+ }
+ tree_sitter_data
+ }
+
+ pub fn all_bracket_ranges(
+ &self,
+ range: Range<usize>,
+ ) -> impl Iterator<Item = BracketMatch<usize>> {
+ self.fetch_bracket_ranges(range.clone(), None)
+ .into_values()
+ .flatten()
+ .filter(move |bracket_match| {
+ let bracket_range = bracket_match.open_range.start..bracket_match.close_range.end;
+ bracket_range.overlaps(&range)
+ })
}
/// Returns bracket range pairs overlapping or adjacent to `range`
pub fn bracket_ranges<T: ToOffset>(
&self,
range: Range<T>,
- ) -> impl Iterator<Item = BracketMatch> + '_ {
+ ) -> impl Iterator<Item = BracketMatch<usize>> + '_ {
// Find bracket pairs that *inclusively* contain the given range.
let range = range.start.to_previous_offset(self)..range.end.to_next_offset(self);
self.all_bracket_ranges(range)
@@ -4320,15 +4470,19 @@ impl BufferSnapshot {
pub fn enclosing_bracket_ranges<T: ToOffset>(
&self,
range: Range<T>,
- ) -> impl Iterator<Item = BracketMatch> + '_ {
+ ) -> impl Iterator<Item = BracketMatch<usize>> + '_ {
let range = range.start.to_offset(self)..range.end.to_offset(self);
let result: Vec<_> = self.bracket_ranges(range.clone()).collect();
- let max_depth = result.iter().map(|mat| mat.depth).max().unwrap_or(0);
+ let max_depth = result
+ .iter()
+ .map(|mat| mat.syntax_layer_depth)
+ .max()
+ .unwrap_or(0);
result.into_iter().filter(move |pair| {
pair.open_range.start <= range.start
&& pair.close_range.end >= range.end
- && pair.depth == max_depth
+ && pair.syntax_layer_depth == max_depth
})
}
@@ -4815,6 +4969,7 @@ impl Clone for BufferSnapshot {
remote_selections: self.remote_selections.clone(),
diagnostics: self.diagnostics.clone(),
language: self.language.clone(),
+ tree_sitter_data: self.tree_sitter_data.clone(),
non_text_state_update_count: self.non_text_state_update_count,
}
}
@@ -0,0 +1,121 @@
+//! A row chunk is an exclusive range of rows, [`BufferRow`] within a buffer of a certain version, [`Global`].
+//! All but the last chunk are of a constant, given size.
+
+use std::{ops::Range, sync::Arc};
+
+use clock::Global;
+use text::{Anchor, OffsetRangeExt as _, Point};
+use util::RangeExt;
+
+use crate::BufferRow;
+
+/// An range of rows, exclusive as [`lsp::Range`] and
+/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
+/// denote.
+///
+/// Represents an area in a text editor, adjacent to other ones.
+/// Together, chunks form entire document at a particular version [`Global`].
+/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
+/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
+#[derive(Clone)]
+pub struct RowChunks {
+ pub(crate) snapshot: text::BufferSnapshot,
+ chunks: Arc<[RowChunk]>,
+}
+
+impl std::fmt::Debug for RowChunks {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("RowChunks")
+ .field("version", self.snapshot.version())
+ .field("chunks", &self.chunks)
+ .finish()
+ }
+}
+
+impl RowChunks {
+ pub fn new(snapshot: text::BufferSnapshot, max_rows_per_chunk: u32) -> Self {
+ let buffer_point_range = (0..snapshot.len()).to_point(&snapshot);
+ let last_row = buffer_point_range.end.row;
+ let chunks = (buffer_point_range.start.row..=last_row)
+ .step_by(max_rows_per_chunk as usize)
+ .enumerate()
+ .map(|(id, chunk_start)| RowChunk {
+ id,
+ start: chunk_start,
+ end_exclusive: (chunk_start + max_rows_per_chunk).min(last_row),
+ })
+ .collect::<Vec<_>>();
+ Self {
+ snapshot,
+ chunks: Arc::from(chunks),
+ }
+ }
+
+ pub fn version(&self) -> &Global {
+ self.snapshot.version()
+ }
+
+ pub fn len(&self) -> usize {
+ self.chunks.len()
+ }
+
+ pub fn applicable_chunks(
+ &self,
+ ranges: &[Range<text::Anchor>],
+ ) -> impl Iterator<Item = RowChunk> {
+ let row_ranges = ranges
+ .iter()
+ .map(|range| range.to_point(&self.snapshot))
+ // Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
+ // This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
+ .map(|point_range| point_range.start.row..point_range.end.row + 1)
+ .collect::<Vec<_>>();
+ self.chunks
+ .iter()
+ .filter(move |chunk| -> bool {
+ let chunk_range = chunk.row_range().to_inclusive();
+ row_ranges
+ .iter()
+ .any(|row_range| chunk_range.overlaps(&row_range))
+ })
+ .copied()
+ }
+
+ pub fn chunk_range(&self, chunk: RowChunk) -> Option<Range<Anchor>> {
+ if !self.chunks.contains(&chunk) {
+ return None;
+ }
+
+ let start = Point::new(chunk.start, 0);
+ let end = if self.chunks.last() == Some(&chunk) {
+ Point::new(
+ chunk.end_exclusive,
+ self.snapshot.line_len(chunk.end_exclusive),
+ )
+ } else {
+ Point::new(chunk.end_exclusive, 0)
+ };
+ Some(self.snapshot.anchor_before(start)..self.snapshot.anchor_after(end))
+ }
+
+ pub fn previous_chunk(&self, chunk: RowChunk) -> Option<RowChunk> {
+ if chunk.id == 0 {
+ None
+ } else {
+ self.chunks.get(chunk.id - 1).copied()
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub struct RowChunk {
+ pub id: usize,
+ pub start: BufferRow,
+ pub end_exclusive: BufferRow,
+}
+
+impl RowChunk {
+ pub fn row_range(&self) -> Range<BufferRow> {
+ self.start..self.end_exclusive
+ }
+}
@@ -1111,9 +1111,10 @@ fn test_text_objects(cx: &mut App) {
#[gpui::test]
fn test_enclosing_bracket_ranges(cx: &mut App) {
- let mut assert = |selection_text, range_markers| {
+ #[track_caller]
+ fn assert(selection_text: &'static str, range_markers: Vec<&'static str>, cx: &mut App) {
assert_bracket_pairs(selection_text, range_markers, rust_lang(), cx)
- };
+ }
assert(
indoc! {"
@@ -1130,6 +1131,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
«}»
let foo = 1;"}],
+ cx,
);
assert(
@@ -1156,6 +1158,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
let foo = 1;"},
],
+ cx,
);
assert(
@@ -1182,6 +1185,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
let foo = 1;"},
],
+ cx,
);
assert(
@@ -1199,6 +1203,7 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
«}»
let foo = 1;"}],
+ cx,
);
assert(
@@ -1209,7 +1214,8 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
}
let fˇoo = 1;"},
- vec![],
+ Vec::new(),
+ cx,
);
// Regression test: avoid crash when querying at the end of the buffer.
@@ -1221,7 +1227,8 @@ fn test_enclosing_bracket_ranges(cx: &mut App) {
}
}
let foo = 1;ˇ"},
- vec![],
+ Vec::new(),
+ cx,
);
}
@@ -1323,6 +1323,7 @@ struct BracketsConfig {
#[derive(Clone, Debug, Default)]
struct BracketsPatternConfig {
newline_only: bool,
+ rainbow_exclude: bool,
}
pub struct DebugVariablesConfig {
@@ -1685,9 +1686,13 @@ impl Language {
.map(|ix| {
let mut config = BracketsPatternConfig::default();
for setting in query.property_settings(ix) {
- if setting.key.as_ref() == "newline.only" {
+ let setting_key = setting.key.as_ref();
+ if setting_key == "newline.only" {
config.newline_only = true
}
+ if setting_key == "rainbow.exclude" {
+ config.rainbow_exclude = true
+ }
}
config
})
@@ -2640,8 +2645,9 @@ pub fn rust_lang() -> Arc<Language> {
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
-("\"" @open "\"" @close)
-(closure_parameters "|" @open "|" @close)"#,
+(closure_parameters "|" @open "|" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))"#,
)),
text_objects: Some(Cow::from(
r#"
@@ -54,14 +54,14 @@ pub struct AllLanguageSettings {
pub(crate) file_types: FxHashMap<Arc<str>, GlobSet>,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub struct WhitespaceMap {
pub space: SharedString,
pub tab: SharedString,
}
/// The settings for a particular language.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub struct LanguageSettings {
/// How many columns a tab should occupy.
pub tab_size: NonZeroU32,
@@ -153,9 +153,11 @@ pub struct LanguageSettings {
pub completions: CompletionSettings,
/// Preferred debuggers for this language.
pub debuggers: Vec<String>,
+ /// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor.
+ pub colorize_brackets: bool,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub struct CompletionSettings {
/// Controls how words are completed.
/// For large documents, not all words may be fetched for completion.
@@ -207,7 +209,7 @@ pub struct IndentGuideSettings {
pub background_coloring: settings::IndentGuideBackgroundColoring,
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub struct LanguageTaskSettings {
/// Extra task variables to set for a particular language.
pub variables: HashMap<String, String>,
@@ -225,7 +227,7 @@ pub struct LanguageTaskSettings {
/// Allows to enable/disable formatting with Prettier
/// and configure default Prettier, used when no project-level Prettier installation is found.
/// Prettier formatting is disabled by default.
-#[derive(Debug, Clone)]
+#[derive(Debug, Clone, PartialEq)]
pub struct PrettierSettings {
/// Enables or disables formatting with Prettier for a given language.
pub allowed: bool,
@@ -584,6 +586,7 @@ impl settings::Settings for AllLanguageSettings {
},
show_completions_on_input: settings.show_completions_on_input.unwrap(),
show_completion_documentation: settings.show_completion_documentation.unwrap(),
+ colorize_brackets: settings.colorize_brackets.unwrap(),
completions: CompletionSettings {
words: completions.words.unwrap(),
words_min_length: completions.words_min_length.unwrap() as usize,
@@ -1,12 +1,12 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
-("`" @open "`" @close)
-(("do" @open "done" @close) (#set! newline.only))
-((case_statement ("in" @open "esac" @close)) (#set! newline.only))
-((if_statement (elif_clause ("then" @open)) (else_clause ("else" @close))) (#set! newline.only))
-((if_statement (else_clause ("else" @open)) "fi" @close) (#set! newline.only))
-((if_statement ("then" @open) (elif_clause ("elif" @close))) (#set! newline.only))
-((if_statement ("then" @open) (else_clause ("else" @close))) (#set! newline.only))
-((if_statement ("then" @open "fi" @close)) (#set! newline.only))
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("`" @open "`" @close) (#set! rainbow.exclude))
+(("do" @open "done" @close) (#set! newline.only) (#set! rainbow.exclude))
+((case_statement ("in" @open "esac" @close)) (#set! newline.only) (#set! rainbow.exclude))
+((if_statement (elif_clause ("then" @open)) (else_clause ("else" @close))) (#set! newline.only) (#set! rainbow.exclude))
+((if_statement (else_clause ("else" @open)) "fi" @close) (#set! newline.only) (#set! rainbow.exclude))
+((if_statement ("then" @open) (elif_clause ("elif" @close))) (#set! newline.only) (#set! rainbow.exclude))
+((if_statement ("then" @open) (else_clause ("else" @close))) (#set! newline.only) (#set! rainbow.exclude))
+((if_statement ("then" @open "fi" @close)) (#set! newline.only) (#set! rainbow.exclude))
@@ -1,5 +1,5 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
-("'" @open "'" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
@@ -1,5 +1,5 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
-("'" @open "'" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
@@ -1,5 +1,5 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
-("'" @open "'" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
@@ -1,6 +1,6 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
-("`" @open "`" @close)
-((rune_literal) @open @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("`" @open "`" @close) (#set! rainbow.exclude))
+((rune_literal) @open @close (#set! rainbow.exclude))
@@ -4,6 +4,6 @@
("<" @open ">" @close)
("<" @open "/>" @close)
("</" @open ">" @close)
-("\"" @open "\"" @close)
-("'" @open "'" @close)
-("`" @open "`" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
+(("`" @open "`" @close) (#set! rainbow.exclude))
@@ -1,3 +1,3 @@
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
@@ -1,3 +1,3 @@
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
@@ -1,7 +1,7 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
-("`" @open "`" @close)
-("'" @open "'" @close)
-((fenced_code_block_delimiter) @open (fenced_code_block_delimiter) @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("`" @open "`" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
+(((fenced_code_block_delimiter) @open (fenced_code_block_delimiter) @close) (#set! rainbow.exclude))
@@ -1,4 +1,4 @@
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
-((string_start) @open (string_end) @close)
+(((string_start) @open (string_end) @close) (#set! rainbow.exclude))
@@ -2,6 +2,6 @@
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
-("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)
-("'" @open "'" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
@@ -4,8 +4,8 @@
("<" @open ">" @close)
("<" @open "/>" @close)
("</" @open ">" @close)
-("\"" @open "\"" @close)
-("'" @open "'" @close)
-("`" @open "`" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
+(("`" @open "`" @close) (#set! rainbow.exclude))
-((jsx_element (jsx_opening_element) @open (jsx_closing_element) @close) (#set! newline.only))
+((jsx_element (jsx_opening_element) @open (jsx_closing_element) @close) (#set! newline.only) (#set! rainbow.exclude))
@@ -2,6 +2,6 @@
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
-("\"" @open "\"" @close)
-("'" @open "'" @close)
-("`" @open "`" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
+(("`" @open "`" @close) (#set! rainbow.exclude))
@@ -1,4 +1,4 @@
("[" @open "]" @close)
("{" @open "}" @close)
-("\"" @open "\"" @close)
-("'" @open "'" @close)
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+(("'" @open "'" @close) (#set! rainbow.exclude))
@@ -18,10 +18,10 @@ use collections::{BTreeMap, Bound, HashMap, HashSet};
use gpui::{App, Context, Entity, EntityId, EventEmitter};
use itertools::Itertools;
use language::{
- AutoindentMode, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, CharClassifier,
- CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, DiskState, File,
- IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, Outline,
- OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _,
+ AutoindentMode, BracketMatch, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability,
+ CharClassifier, CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, DiskState,
+ File, IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16,
+ Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _,
ToPoint as _, TransactionId, TreeSitterOptions, Unclipped,
language_settings::{LanguageSettings, language_settings},
};
@@ -5400,7 +5400,6 @@ impl MultiBufferSnapshot {
{
let range = range.start.to_offset(self)..range.end.to_offset(self);
let mut excerpt = self.excerpt_containing(range.clone())?;
-
Some(
excerpt
.buffer()
@@ -5410,15 +5409,17 @@ impl MultiBufferSnapshot {
BufferOffset(pair.open_range.start)..BufferOffset(pair.open_range.end);
let close_range =
BufferOffset(pair.close_range.start)..BufferOffset(pair.close_range.end);
- if excerpt.contains_buffer_range(open_range.start..close_range.end) {
- Some((
- excerpt.map_range_from_buffer(open_range),
- excerpt.map_range_from_buffer(close_range),
- ))
- } else {
- None
- }
- }),
+ excerpt
+ .contains_buffer_range(open_range.start..close_range.end)
+ .then(|| BracketMatch {
+ open_range: excerpt.map_range_from_buffer(open_range),
+ close_range: excerpt.map_range_from_buffer(close_range),
+ color_index: pair.color_index,
+ newline_only: pair.newline_only,
+ syntax_layer_depth: pair.syntax_layer_depth,
+ })
+ })
+ .map(BracketMatch::bracket_ranges),
)
}
@@ -29,7 +29,6 @@ use crate::{
lsp_command::{self, *},
lsp_store::{
self,
- inlay_hint_cache::BufferChunk,
log_store::{GlobalLogStore, LanguageServerKind},
},
manifest_tree::{
@@ -73,6 +72,7 @@ use language::{
serialize_lsp_edit, serialize_version,
},
range_from_lsp, range_to_lsp,
+ row_chunk::RowChunk,
};
use lsp::{
AdapterServerCapabilities, CodeActionKind, CompletionContext, CompletionOptions,
@@ -117,7 +117,7 @@ use std::{
time::{Duration, Instant},
};
use sum_tree::Dimensions;
-use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, Point, ToPoint as _};
+use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _};
use util::{
ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into,
@@ -3590,7 +3590,7 @@ pub struct BufferLspData {
code_lens: Option<CodeLensData>,
inlay_hints: BufferInlayHints,
lsp_requests: HashMap<LspKey, HashMap<LspRequestId, Task<()>>>,
- chunk_lsp_requests: HashMap<LspKey, HashMap<BufferChunk, LspRequestId>>,
+ chunk_lsp_requests: HashMap<LspKey, HashMap<RowChunk, LspRequestId>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -6706,7 +6706,7 @@ impl LspStore {
self.latest_lsp_data(buffer, cx)
.inlay_hints
.applicable_chunks(ranges)
- .map(|chunk| chunk.start..chunk.end)
+ .map(|chunk| chunk.row_range())
.collect()
}
@@ -6729,7 +6729,6 @@ impl LspStore {
known_chunks: Option<(clock::Global, HashSet<Range<BufferRow>>)>,
cx: &mut Context<Self>,
) -> HashMap<Range<BufferRow>, Task<Result<CacheInlayHints>>> {
- let buffer_snapshot = buffer.read(cx).snapshot();
let next_hint_id = self.next_hint_id.clone();
let lsp_data = self.latest_lsp_data(&buffer, cx);
let query_version = lsp_data.buffer_version.clone();
@@ -6758,14 +6757,12 @@ impl LspStore {
let mut ranges_to_query = None;
let applicable_chunks = existing_inlay_hints
.applicable_chunks(ranges.as_slice())
- .filter(|chunk| !known_chunks.contains(&(chunk.start..chunk.end)))
+ .filter(|chunk| !known_chunks.contains(&chunk.row_range()))
.collect::<Vec<_>>();
if applicable_chunks.is_empty() {
return HashMap::default();
}
- let last_chunk_number = existing_inlay_hints.buffer_chunks_len() - 1;
-
for row_chunk in applicable_chunks {
match (
existing_inlay_hints
@@ -6779,16 +6776,12 @@ impl LspStore {
.cloned(),
) {
(None, None) => {
- let end = if last_chunk_number == row_chunk.id {
- Point::new(row_chunk.end, buffer_snapshot.line_len(row_chunk.end))
- } else {
- Point::new(row_chunk.end, 0)
+ let Some(chunk_range) = existing_inlay_hints.chunk_range(row_chunk) else {
+ continue;
};
- ranges_to_query.get_or_insert_with(Vec::new).push((
- row_chunk,
- buffer_snapshot.anchor_before(Point::new(row_chunk.start, 0))
- ..buffer_snapshot.anchor_after(end),
- ));
+ ranges_to_query
+ .get_or_insert_with(Vec::new)
+ .push((row_chunk, chunk_range));
}
(None, Some(fetched_hints)) => hint_fetch_tasks.push((row_chunk, fetched_hints)),
(Some(cached_hints), None) => {
@@ -6796,7 +6789,7 @@ impl LspStore {
if for_server.is_none_or(|for_server| for_server == server_id) {
cached_inlay_hints
.get_or_insert_with(HashMap::default)
- .entry(row_chunk.start..row_chunk.end)
+ .entry(row_chunk.row_range())
.or_insert_with(HashMap::default)
.entry(server_id)
.or_insert_with(Vec::new)
@@ -6810,7 +6803,7 @@ impl LspStore {
if for_server.is_none_or(|for_server| for_server == server_id) {
cached_inlay_hints
.get_or_insert_with(HashMap::default)
- .entry(row_chunk.start..row_chunk.end)
+ .entry(row_chunk.row_range())
.or_insert_with(HashMap::default)
.entry(server_id)
.or_insert_with(Vec::new)
@@ -6896,7 +6889,7 @@ impl LspStore {
.map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints))))
.chain(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| {
(
- chunk.start..chunk.end,
+ chunk.row_range(),
cx.spawn(async move |_, _| {
hints_fetch.await.map_err(|e| {
if e.error_code() != ErrorCode::Internal {
@@ -3,10 +3,12 @@ use std::{collections::hash_map, ops::Range, sync::Arc};
use collections::HashMap;
use futures::future::Shared;
use gpui::{App, Entity, Task};
-use language::{Buffer, BufferRow, BufferSnapshot};
+use language::{
+ Buffer,
+ row_chunk::{RowChunk, RowChunks},
+};
use lsp::LanguageServerId;
-use text::OffsetRangeExt;
-use util::RangeExt as _;
+use text::Anchor;
use crate::{InlayHint, InlayId};
@@ -46,8 +48,7 @@ impl InvalidationStrategy {
}
pub struct BufferInlayHints {
- snapshot: BufferSnapshot,
- buffer_chunks: Vec<BufferChunk>,
+ chunks: RowChunks,
hints_by_chunks: Vec<Option<CacheInlayHints>>,
fetches_by_chunks: Vec<Option<CacheInlayHintsTask>>,
hints_by_id: HashMap<InlayId, HintForId>,
@@ -62,25 +63,10 @@ struct HintForId {
position: usize,
}
-/// An range of rows, exclusive as [`lsp::Range`] and
-/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#range>
-/// denote.
-///
-/// Represents an area in a text editor, adjacent to other ones.
-/// Together, chunks form entire document at a particular version [`clock::Global`].
-/// Each chunk is queried for inlays as `(start_row, 0)..(end_exclusive, 0)` via
-/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#inlayHintParams>
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub struct BufferChunk {
- pub id: usize,
- pub start: BufferRow,
- pub end: BufferRow,
-}
-
impl std::fmt::Debug for BufferInlayHints {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BufferInlayHints")
- .field("buffer_chunks", &self.buffer_chunks)
+ .field("buffer_chunks", &self.chunks)
.field("hints_by_chunks", &self.hints_by_chunks)
.field("fetches_by_chunks", &self.fetches_by_chunks)
.field("hints_by_id", &self.hints_by_id)
@@ -92,58 +78,30 @@ const MAX_ROWS_IN_A_CHUNK: u32 = 50;
impl BufferInlayHints {
pub fn new(buffer: &Entity<Buffer>, cx: &mut App) -> Self {
- let buffer = buffer.read(cx);
- let snapshot = buffer.snapshot();
- let buffer_point_range = (0..buffer.len()).to_point(&snapshot);
- let last_row = buffer_point_range.end.row;
- let buffer_chunks = (buffer_point_range.start.row..=last_row)
- .step_by(MAX_ROWS_IN_A_CHUNK as usize)
- .enumerate()
- .map(|(id, chunk_start)| BufferChunk {
- id,
- start: chunk_start,
- end: (chunk_start + MAX_ROWS_IN_A_CHUNK).min(last_row),
- })
- .collect::<Vec<_>>();
+ let chunks = RowChunks::new(buffer.read(cx).text_snapshot(), MAX_ROWS_IN_A_CHUNK);
Self {
- hints_by_chunks: vec![None; buffer_chunks.len()],
- fetches_by_chunks: vec![None; buffer_chunks.len()],
+ hints_by_chunks: vec![None; chunks.len()],
+ fetches_by_chunks: vec![None; chunks.len()],
latest_invalidation_requests: HashMap::default(),
hints_by_id: HashMap::default(),
hint_resolves: HashMap::default(),
- snapshot,
- buffer_chunks,
+ chunks,
}
}
pub fn applicable_chunks(
&self,
ranges: &[Range<text::Anchor>],
- ) -> impl Iterator<Item = BufferChunk> {
- let row_ranges = ranges
- .iter()
- .map(|range| range.to_point(&self.snapshot))
- // Be lenient and yield multiple chunks if they "touch" the exclusive part of the range.
- // This will result in LSP hints [re-]queried for more ranges, but also more hints already visible when scrolling around.
- .map(|point_range| point_range.start.row..point_range.end.row + 1)
- .collect::<Vec<_>>();
- self.buffer_chunks
- .iter()
- .filter(move |chunk| {
- let chunk_range = chunk.start..=chunk.end;
- row_ranges
- .iter()
- .any(|row_range| chunk_range.overlaps(&row_range))
- })
- .copied()
+ ) -> impl Iterator<Item = RowChunk> {
+ self.chunks.applicable_chunks(ranges)
}
- pub fn cached_hints(&mut self, chunk: &BufferChunk) -> Option<&CacheInlayHints> {
+ pub fn cached_hints(&mut self, chunk: &RowChunk) -> Option<&CacheInlayHints> {
self.hints_by_chunks[chunk.id].as_ref()
}
- pub fn fetched_hints(&mut self, chunk: &BufferChunk) -> &mut Option<CacheInlayHintsTask> {
+ pub fn fetched_hints(&mut self, chunk: &RowChunk) -> &mut Option<CacheInlayHintsTask> {
&mut self.fetches_by_chunks[chunk.id]
}
@@ -177,8 +135,8 @@ impl BufferInlayHints {
}
pub fn clear(&mut self) {
- self.hints_by_chunks = vec![None; self.buffer_chunks.len()];
- self.fetches_by_chunks = vec![None; self.buffer_chunks.len()];
+ self.hints_by_chunks = vec![None; self.chunks.len()];
+ self.fetches_by_chunks = vec![None; self.chunks.len()];
self.hints_by_id.clear();
self.hint_resolves.clear();
self.latest_invalidation_requests.clear();
@@ -186,7 +144,7 @@ impl BufferInlayHints {
pub fn insert_new_hints(
&mut self,
- chunk: BufferChunk,
+ chunk: RowChunk,
server_id: LanguageServerId,
new_hints: Vec<(InlayId, InlayHint)>,
) {
@@ -225,10 +183,6 @@ impl BufferInlayHints {
Some(hint)
}
- pub fn buffer_chunks_len(&self) -> usize {
- self.buffer_chunks.len()
- }
-
pub(crate) fn invalidate_for_server_refresh(
&mut self,
for_server: LanguageServerId,
@@ -263,7 +217,7 @@ impl BufferInlayHints {
true
}
- pub(crate) fn invalidate_for_chunk(&mut self, chunk: BufferChunk) {
+ pub(crate) fn invalidate_for_chunk(&mut self, chunk: RowChunk) {
self.fetches_by_chunks[chunk.id] = None;
if let Some(hints_by_server) = self.hints_by_chunks[chunk.id].take() {
for (hint_id, _) in hints_by_server.into_values().flatten() {
@@ -272,4 +226,8 @@ impl BufferInlayHints {
}
}
}
+
+ pub fn chunk_range(&self, chunk: RowChunk) -> Option<Range<Anchor>> {
+ self.chunks.chunk_range(chunk)
+ }
}
@@ -92,6 +92,7 @@ node_runtime = { workspace = true, features = ["test-support"] }
pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
remote = { workspace = true, features = ["test-support"] }
+theme = { workspace = true, features = ["test-support"] }
language_model = { workspace = true, features = ["test-support"] }
lsp = { workspace = true, features = ["test-support"] }
prompt_store.workspace = true
@@ -1499,6 +1499,14 @@ async fn test_remote_git_diffs_when_recv_update_repository_delay(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ theme::init(theme::LoadThemes::JustBase, cx);
+ release_channel::init(SemanticVersion::default(), cx);
+ editor::init(cx);
+ });
+
use editor::Editor;
use gpui::VisualContext;
let text_2 = "
@@ -49,5 +49,6 @@ editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
lsp.workspace = true
+pretty_assertions.workspace = true
unindent.workspace = true
workspace = { workspace = true, features = ["test-support"] }
@@ -2449,6 +2449,7 @@ pub mod tests {
use editor::{DisplayPoint, display_map::DisplayRow};
use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle};
use language::{FakeLspAdapter, rust_lang};
+ use pretty_assertions::assert_eq;
use project::FakeFs;
use serde_json::json;
use settings::{InlayHintSettingsContent, SettingsStore};
@@ -2507,10 +2508,6 @@ pub mod tests {
DisplayPoint::new(DisplayRow(2), 37)..DisplayPoint::new(DisplayRow(2), 40),
match_background_color
),
- (
- DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
- selection_background_color
- ),
(
DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 9),
match_background_color
@@ -412,6 +412,10 @@ pub struct LanguageSettingsContent {
///
/// Default: []
pub debuggers: Option<Vec<String>>,
+ /// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor.
+ ///
+ /// Default: false
+ pub colorize_brackets: Option<bool>,
}
/// Controls how whitespace should be displayedin the editor.
@@ -370,7 +370,7 @@ pub struct ThemeStyleContent {
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
-pub struct AccentContent(pub Option<String>);
+pub struct AccentContent(pub Option<SharedString>);
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)]
pub struct PlayerColorContent {
@@ -450,6 +450,7 @@ impl VsCodeSettings {
prettier: None,
remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"),
show_completion_documentation: None,
+ colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"),
show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"),
show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"),
show_whitespaces: self.read_enum("editor.renderWhitespace", |s| {
@@ -6991,6 +6991,25 @@ fn language_settings_data() -> Vec<SettingsPageItem> {
metadata: None,
files: USER | PROJECT,
}),
+ SettingsPageItem::SettingItem(SettingItem {
+ title: "Colorize brackets",
+ description: "Whether to colorize brackets in the editor.",
+ field: Box::new(SettingField {
+ json_path: Some("languages.$(language).colorize_brackets"),
+ pick: |settings_content| {
+ language_settings_field(settings_content, |language| {
+ language.colorize_brackets.as_ref()
+ })
+ },
+ write: |settings_content, value| {
+ language_settings_field_mut(settings_content, value, |language, value| {
+ language.colorize_brackets = value;
+ })
+ },
+ }),
+ metadata: None,
+ files: USER | PROJECT,
+ }),
]);
if current_language().is_none() {
@@ -196,6 +196,7 @@ You can also add agents through your `settings.json`, by specifying certain fiel
{
"agent_servers": {
"My Custom Agent": {
+ "type": "custom",
"command": "node",
"args": ["~/projects/agent/index.js", "--acp"],
"env": {}
@@ -58,6 +58,7 @@ You can customize a wide range of settings for each language, including:
- [`soft_wrap`](./configuring-zed.md#soft-wrap): How to wrap long lines of code
- [`show_completions_on_input`](./configuring-zed.md#show-completions-on-input): Whether or not to show completions as you type
- [`show_completion_documentation`](./configuring-zed.md#show-completion-documentation): Whether to display inline and alongside documentation for items in the completions menu
+- [`colorize_brackets`](./configuring-zed.md#colorize-brackets): Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor (also known as "rainbow brackets")
These settings allow you to maintain specific coding styles across different languages and projects.
@@ -4687,6 +4687,18 @@ See the [debugger page](./debugger.md) for more information about debugging supp
},
```
+## Colorize Brackets
+
+- Description: Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor (also known as "rainbow brackets").
+- Setting: `colorize_brackets`
+- Default: `false`
+
+**Options**
+
+`boolean` values
+
+The colors that are used for different indentation levels are defined in the theme (theme key: `accents`). They can be customized by using theme overrides.
+
## Unnecessary Code Fade
- Description: How much to fade out unused code.
@@ -154,6 +154,14 @@ This query identifies opening and closing brackets, braces, and quotation marks.
| @open | Captures opening brackets, braces, and quotes |
| @close | Captures closing brackets, braces, and quotes |
+Zed uses these to highlight matching brackets: painting each bracket pair with a different color ("rainbow brackets") and highlighting the brackets if the cursor is inside the bracket pair.
+
+To opt out of rainbow brackets colorization, add the following to the corresponding `brackets.scm` entry:
+
+```scheme
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+```
+
### Code outline/structure
The `outline.scm` file defines the structure for the code outline.
@@ -51,7 +51,15 @@ For example, add the following to your `settings.json` if you wish to override t
"comment.doc": {
"font_style": "italic"
}
- }
+ },
+ "accents": [
+ "#ff0000",
+ "#ff7f00",
+ "#ffff00",
+ "#00ff00",
+ "#0000ff",
+ "#8b00ff"
+ ]
}
}
}
@@ -374,6 +374,8 @@ TBD: Centered layout related settings
"lsp_document_colors": "inlay", // none, inlay, border, background
// When to show the scrollbar in the completion menu.
"completion_menu_scrollbar": "never", // auto, system, always, never
+ // Turn on colorization of brackets in editors (configurable per language)
+ "colorize_brackets": true,
```
### Edit Predictions {#editor-ai}
@@ -1,5 +1,5 @@
("<" @open "/>" @close)
("</" @open ">" @close)
("<" @open ">" @close)
-("\"" @open "\"" @close)
-((element (start_tag) @open (end_tag) @close) (#set! newline.only))
+(("\"" @open "\"" @close) (#set! rainbow.exclude))
+((element (start_tag) @open (end_tag) @close) (#set! newline.only) (#set! rainbow.exclude))