diff --git a/Cargo.lock b/Cargo.lock index a21c80c8b279206a791020231100abe6468ece6f..9cace971e3d9248361c7e97def9481206fa3cc1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/assets/settings/default.json b/assets/settings/default.json index 63ef8b51bb84c8f8dc5475dc172b82a78cee8eac..bd8a6e96dd3929c63f47b20f54d3422051363511 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -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: // diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index f72d92e038ce234327e29776f923c27d6592cf16..2939079f256d4c2742e514f002a4c9fe5e58b49a 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -242,6 +242,7 @@ impl Console { start_offset, vec![range], style, + false, cx, ); } diff --git a/crates/editor/src/bracket_colorization.rs b/crates/editor/src/bracket_colorization.rs new file mode 100644 index 0000000000000000000000000000000000000000..902ec2b7702b945bb482e4e4700cf37b36ae907b --- /dev/null +++ b/crates/editor/src/bracket_colorization.rs @@ -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) { + 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::>::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::(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::( + 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(a: HashMap>>) -> usize +where + T: Future>>>>, +{ + 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«11»«1(a: HashMap«24»>3»>2»)1» -> usize +where + T: Future«15»>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>, +} + +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«23»>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("Option1» «1{ + data: Vec«23»>2», +}1» + +fn process_data«1()1» «1{ + let map: 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«23»>2», +}1» + +fn process_data«1()1» «1{ + let map: Result2» +}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«23»>2», +}1» + +fn process_data«1()1» «1{ + let map: Result3»>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«23»>2», +}1» + +fn process_data«1()1» «1{ + let map: Result«24»>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> = None; + } +"#}, + indoc! {r#" + fn process_data_2() { + let map: Option> = 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«34»>3» = None; + }2» +"#}, + indoc! {r#" + fn process_data_2() { + let map: Option> = 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«34»>3» = None; + }2» +"#}, + indoc! {r#" + fn process_data_2«2()2» «2{ + let map: Option«34»>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> = None; + } +"#}, + indoc! {r#" + fn process_data_2«2()2» «2{ + let map: Option«34»>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«34»>3» = None; + }2» +"#}, + indoc! {r#" + fn process_data_2«2()2» «2{ + let map: Option«34»>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> = None; + } +"#}, + r#" fn process_data_2() { + let map: Option> = 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«34»>3» = None; + }2» +"#}, + r#" fn process_data_2() { + let map: Option> = 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, + config: &InlayHintsConfig, + ) -> Vec { + 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>, + extern_block_parent: Option, + } + + pub(crate) fn inlay_hints_resolve( + db: &RootDatabase, + file_id: FileId, + resolve_range: TextRange, + hash: u64, + config: &InlayHintsConfig, + hasher: impl Fn(&InlayHint) -> u64, + ) -> Option { + 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) -> Option { + 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, + 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::() + }); + + 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::() + }); + 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::() + }); + 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 = 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::(), + 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::(), + 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> = None; + // a + // b + // c + } + "#}, + indoc! {r#" + fn process_data_2() { + let other_map: Option> = 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«34»>3» = None; + // a + // b + + + fn process_data_2«2()2» «2{ + let other_map: Option«34»>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«34»>3» = None; + // a + // b + + + fn process_data_2«2()2» «2{ + let other_map: Option«34»>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«12»>1» = None; + // a + // b + + + fn process_data_2«2()2» «2{ + let other_map: Option«12»>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::(); + 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 + } +} diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 86aad01a2ea946057fe08876937853f5b84f00bf..0d051507b2582bb13b5d14f6ad5c693ffdb321a2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -483,8 +483,26 @@ impl DisplayMap { key: HighlightKey, ranges: Vec>, 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( + &self, + ) -> Vec<(gpui::Hsla, Range)> { + use itertools::Itertools; + + let required_type_id = TypeId::of::(); + 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::>() + }) + .sorted_by_key(|(_, range)| range.start) + .collect() + } + #[allow(unused)] #[cfg(any(test, feature = "test-support"))] pub(crate) fn inlay_highlights( @@ -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::()), highlighted_ranges @@ -2710,6 +2766,8 @@ pub mod tests { }) .collect(), style, + false, + cx, ); }); diff --git a/crates/editor/src/display_map/custom_highlights.rs b/crates/editor/src/display_map/custom_highlights.rs index e3ae7c99208cb4549a7538ac7f2abcc601c6e6d0..a40d1adc82f4bc79308eaec901586232e9e2e5c2 100644 --- a/crates/editor/src/display_map/custom_highlights.rs +++ b/crates/editor/src/display_map/custom_highlights.rs @@ -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() { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5d1c24bcad76333beee8941ee729b9578bb7ad65..e06ba62ce94c69828b3ab08465b4375b4c862343 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, pub lookup_key: Option>, + applicable_language_settings: HashMap, LanguageSettings>, + accent_overrides: Vec, + fetched_tree_sitter_chunks: HashMap>>, } fn debounce_value(debounce_ms: u64) -> Option { @@ -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>, style: HighlightStyle, + merge: bool, cx: &mut Context, ) { - self.display_map.update(cx, |map, _| { + self.display_map.update(cx, |map, cx| { map.highlight_text( HighlightKey::TypePlus(TypeId::of::(), key), ranges, style, + merge, + cx, ); }); cx.notify(); @@ -21159,8 +21174,14 @@ impl Editor { style: HighlightStyle, cx: &mut Context, ) { - self.display_map.update(cx, |map, _| { - map.highlight_text(HighlightKey::Type(TypeId::of::()), ranges, style) + self.display_map.update(cx, |map, cx| { + map.highlight_text( + HighlightKey::Type(TypeId::of::()), + 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 { + 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, 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) { + 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 = None; + let mut result: Option> = None; for pair in buffer .all_bracket_ranges(range.start.0..range.end.0) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9d567513b2a428a89b5a58ba75a1276411dce639..2bd1316371449d8f4b7e4c428e5e6c7c27f43457 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, ); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index d98dc89b6b0f1a5ab0ebd9a910db0fcb0db1f18c..a92735d18617057ddd10f049e5a22525827e1874 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -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(); }); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 95db651350a2e1c703ce0ab52c77f075a83a0500..fd5e6fcaf6435a2836ab1ad828933a9d0763f5b9 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -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>>, _subscriptions: Vec, + tree_sitter_data: Arc>, +} + +#[derive(Debug, Clone)] +pub struct TreeSitterData { + chunks: RowChunks, + brackets_by_chunks: Vec>>>, +} + +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, language: Option>, non_text_state_update_count: usize, + tree_sitter_data: Arc>, } /// 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, - pub close_range: Range, +pub struct BracketMatch { + pub open_range: Range, + pub close_range: Range, pub newline_only: bool, - pub depth: usize, + pub syntax_layer_depth: usize, + pub color_index: Option, +} + +impl BracketMatch { + pub fn bracket_ranges(self) -> (Range, Range) { + (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, - ) -> impl Iterator + '_ { - 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::>(); - - 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>)>, + ) -> HashMap, Vec>> { + 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::>(); + + 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::>(); + + 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, + ) -> impl Iterator> { + 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( &self, range: Range, - ) -> impl Iterator + '_ { + ) -> impl Iterator> + '_ { // 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( &self, range: Range, - ) -> impl Iterator + '_ { + ) -> impl Iterator> + '_ { 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, } } diff --git a/crates/language/src/buffer/row_chunk.rs b/crates/language/src/buffer/row_chunk.rs new file mode 100644 index 0000000000000000000000000000000000000000..7589c5ac078b9443c3dfd501abb0e6d79cb74581 --- /dev/null +++ b/crates/language/src/buffer/row_chunk.rs @@ -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 +/// +/// 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 +/// +#[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::>(); + 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], + ) -> impl Iterator { + 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::>(); + 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> { + 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 { + 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 { + self.start..self.end_exclusive + } +} diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 14475af5984d75de9e166dd1d8a0379c6a66f3fd..05402abcad478e2eedb17d31853ab0bc2bd3702c 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -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, ); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 82e0d69cefa94cc0e03a694eea0f29031d8fe156..7ce3986736cc0a1e8b8d21124ebe8c29ddc9214c 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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 { ("[" @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#" diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index f0235cb51b5fcbf3cdcc4a4bf46bd12adbfd674e..c5b2dc45e163e55c2427badd8d3f4a24dab64916 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -54,14 +54,14 @@ pub struct AllLanguageSettings { pub(crate) file_types: FxHashMap, 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, + /// 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, @@ -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, diff --git a/crates/languages/src/bash/brackets.scm b/crates/languages/src/bash/brackets.scm index 5ae73cdda76c3d0775ddb124c7b7343e5f2004de..88a2a1b67f602afb4e7de21a0ec0a523d33e37ee 100644 --- a/crates/languages/src/bash/brackets.scm +++ b/crates/languages/src/bash/brackets.scm @@ -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)) diff --git a/crates/languages/src/c/brackets.scm b/crates/languages/src/c/brackets.scm index 2f886c424022875118951191b381a203593183ad..2149bddc6c9a7ec04667d03da75580b676e12a28 100644 --- a/crates/languages/src/c/brackets.scm +++ b/crates/languages/src/c/brackets.scm @@ -1,5 +1,5 @@ ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) -("\"" @open "\"" @close) -("'" @open "'" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) +(("'" @open "'" @close) (#set! rainbow.exclude)) diff --git a/crates/languages/src/cpp/brackets.scm b/crates/languages/src/cpp/brackets.scm index 2f886c424022875118951191b381a203593183ad..2149bddc6c9a7ec04667d03da75580b676e12a28 100644 --- a/crates/languages/src/cpp/brackets.scm +++ b/crates/languages/src/cpp/brackets.scm @@ -1,5 +1,5 @@ ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) -("\"" @open "\"" @close) -("'" @open "'" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) +(("'" @open "'" @close) (#set! rainbow.exclude)) diff --git a/crates/languages/src/css/brackets.scm b/crates/languages/src/css/brackets.scm index 2f886c424022875118951191b381a203593183ad..2149bddc6c9a7ec04667d03da75580b676e12a28 100644 --- a/crates/languages/src/css/brackets.scm +++ b/crates/languages/src/css/brackets.scm @@ -1,5 +1,5 @@ ("(" @open ")" @close) ("[" @open "]" @close) ("{" @open "}" @close) -("\"" @open "\"" @close) -("'" @open "'" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) +(("'" @open "'" @close) (#set! rainbow.exclude)) diff --git a/crates/languages/src/go/brackets.scm b/crates/languages/src/go/brackets.scm index 0ced37682d4f08f705cd9a3665682b307e84af73..05fb1d7f9219889d652bbdbb294ca45e72cc9c05 100644 --- a/crates/languages/src/go/brackets.scm +++ b/crates/languages/src/go/brackets.scm @@ -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)) diff --git a/crates/languages/src/javascript/brackets.scm b/crates/languages/src/javascript/brackets.scm index 66bf14f137794b8a620b203c102ca3e3390fea20..a16a6432692ec7b9e0e3d24151cb814fc11bd83d 100644 --- a/crates/languages/src/javascript/brackets.scm +++ b/crates/languages/src/javascript/brackets.scm @@ -4,6 +4,6 @@ ("<" @open ">" @close) ("<" @open "/>" @close) ("" @close) -("\"" @open "\"" @close) -("'" @open "'" @close) -("`" @open "`" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) +(("'" @open "'" @close) (#set! rainbow.exclude)) +(("`" @open "`" @close) (#set! rainbow.exclude)) diff --git a/crates/languages/src/json/brackets.scm b/crates/languages/src/json/brackets.scm index 9e8c9cd93c30f7697ead2161295b4583ffdfb93b..cd5cdf328b3a04730d56ec0cb06c3802fe07c978 100644 --- a/crates/languages/src/json/brackets.scm +++ b/crates/languages/src/json/brackets.scm @@ -1,3 +1,3 @@ ("[" @open "]" @close) ("{" @open "}" @close) -("\"" @open "\"" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) diff --git a/crates/languages/src/jsonc/brackets.scm b/crates/languages/src/jsonc/brackets.scm index 9e8c9cd93c30f7697ead2161295b4583ffdfb93b..cd5cdf328b3a04730d56ec0cb06c3802fe07c978 100644 --- a/crates/languages/src/jsonc/brackets.scm +++ b/crates/languages/src/jsonc/brackets.scm @@ -1,3 +1,3 @@ ("[" @open "]" @close) ("{" @open "}" @close) -("\"" @open "\"" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) diff --git a/crates/languages/src/markdown/brackets.scm b/crates/languages/src/markdown/brackets.scm index 23f3e4d3d0155e1c68aa5c3f0ada4764fb693049..172a2e7f723e3a170d80d19fa2f78fa334258105 100644 --- a/crates/languages/src/markdown/brackets.scm +++ b/crates/languages/src/markdown/brackets.scm @@ -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)) diff --git a/crates/languages/src/python/brackets.scm b/crates/languages/src/python/brackets.scm index be6803358701ae6b43eb2aecb59a5a34f76d71b6..9e5b59788fc88fcb0830325417de50a9414828b8 100644 --- a/crates/languages/src/python/brackets.scm +++ b/crates/languages/src/python/brackets.scm @@ -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)) diff --git a/crates/languages/src/rust/brackets.scm b/crates/languages/src/rust/brackets.scm index 0bf19b8085fb035b28c8b19dc07ff0df191c9c26..7a35adb10021c83b8e08e888187ab133c5313ad9 100644 --- a/crates/languages/src/rust/brackets.scm +++ b/crates/languages/src/rust/brackets.scm @@ -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)) diff --git a/crates/languages/src/tsx/brackets.scm b/crates/languages/src/tsx/brackets.scm index 359ae87aa2d45a2241cb8f1579de14b312465baf..0e98b78036b4b19fd63d812fa92d2416788764f4 100644 --- a/crates/languages/src/tsx/brackets.scm +++ b/crates/languages/src/tsx/brackets.scm @@ -4,8 +4,8 @@ ("<" @open ">" @close) ("<" @open "/>" @close) ("" @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)) diff --git a/crates/languages/src/typescript/brackets.scm b/crates/languages/src/typescript/brackets.scm index 48afefeef07e9950cf6c8eba40b79def50c09c71..635233849142d8951edeca02ca0c79253aa91e80 100644 --- a/crates/languages/src/typescript/brackets.scm +++ b/crates/languages/src/typescript/brackets.scm @@ -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)) diff --git a/crates/languages/src/yaml/brackets.scm b/crates/languages/src/yaml/brackets.scm index 59cf45205f6819ac1e5076ba9b9d952c9b447b08..0cfc5072d4eeda19d75ce943481670a3ee8938b0 100644 --- a/crates/languages/src/yaml/brackets.scm +++ b/crates/languages/src/yaml/brackets.scm @@ -1,4 +1,4 @@ ("[" @open "]" @close) ("{" @open "}" @close) -("\"" @open "\"" @close) -("'" @open "'" @close) +(("\"" @open "\"" @close) (#set! rainbow.exclude)) +(("'" @open "'" @close) (#set! rainbow.exclude)) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 1ade63b5d17b9558c6686bc4f95bcd9193938f7d..7ecc09255b17ebbf2e68e21ab4c8d88f93d08d75 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -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), ) } diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 4c6ed0b7c535504de7ea63f8196e35553bd7d829..17f558d72d4854bb99676472100c442ad164f0a5 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -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, inlay_hints: BufferInlayHints, lsp_requests: HashMap>>, - chunk_lsp_requests: HashMap>, + chunk_lsp_requests: HashMap>, } #[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>)>, cx: &mut Context, ) -> HashMap, Task>> { - 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::>(); 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 { diff --git a/crates/project/src/lsp_store/inlay_hint_cache.rs b/crates/project/src/lsp_store/inlay_hint_cache.rs index cca9d66e8c330f1a4c723a84c4fb418b976f7c03..804552b52cee9f31799e12f3c42e0614291eeab9 100644 --- a/crates/project/src/lsp_store/inlay_hint_cache.rs +++ b/crates/project/src/lsp_store/inlay_hint_cache.rs @@ -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, + chunks: RowChunks, hints_by_chunks: Vec>, fetches_by_chunks: Vec>, hints_by_id: HashMap, @@ -62,25 +63,10 @@ struct HintForId { position: usize, } -/// An range of rows, exclusive as [`lsp::Range`] and -/// -/// 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 -/// -#[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, 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::>(); + 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], - ) -> impl Iterator { - 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::>(); - 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 { + 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 { + pub fn fetched_hints(&mut self, chunk: &RowChunk) -> &mut Option { &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> { + self.chunks.chunk_range(chunk) + } } diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml index e4c7932973741015066efbcd07d0d0c71212acb0..ba64f7aec9ee0a3759c2943e42b0f19742d905c1 100644 --- a/crates/remote_server/Cargo.toml +++ b/crates/remote_server/Cargo.toml @@ -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 diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 1cb63b8cd01e201c5fb2a212a2643cfdf481642a..4b931edb9e63443c6cf23756e737e015c291741c 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -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 = " diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 7d8efbb11a5f1461da5b63152e2277a38ad272b4..291257e74258359356203955cec3a0a6d065b3fa 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -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"] } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 68e3dca1ce07a1773856a3cacecf553d4c88f7e3..85656f179946393c3b15d8d57921ffa3847365cf 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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 diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs index ed70116862bbda6af22d4027a406535ae0c19d67..11eb87817d12517ecb2ef333eacc60d1c2f48330 100644 --- a/crates/settings/src/settings_content/language.rs +++ b/crates/settings/src/settings_content/language.rs @@ -412,6 +412,10 @@ pub struct LanguageSettingsContent { /// /// Default: [] pub debuggers: Option>, + /// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor. + /// + /// Default: false + pub colorize_brackets: Option, } /// Controls how whitespace should be displayedin the editor. diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index 8b87cc15196b7a562a794eb4a1effeb5cb102ef6..4cd1313633a1c32eaf2c0066b23ac3bd3e5bbe79 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -370,7 +370,7 @@ pub struct ThemeStyleContent { } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct AccentContent(pub Option); +pub struct AccentContent(pub Option); #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] pub struct PlayerColorContent { diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs index 5644cd7a1a8463a1f072d838a0a1b16bd7ad991b..f5df817dcd0f4ae02bea3934eaaaf042a02bdbc1 100644 --- a/crates/settings/src/vscode_import.rs +++ b/crates/settings/src/vscode_import.rs @@ -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| { diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index d5368e278914044196f55aaf852e2efefed07117..76874c2ad9594cd9955cbe759c458fe9cf007c2e 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -6991,6 +6991,25 @@ fn language_settings_data() -> Vec { 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() { diff --git a/docs/src/ai/external-agents.md b/docs/src/ai/external-agents.md index d396a9b72bf1b51e4fd3994805c7b0d5268a0cd0..696a60709cc3b6120af0c63fc01a79bd58134402 100644 --- a/docs/src/ai/external-agents.md +++ b/docs/src/ai/external-agents.md @@ -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": {} diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md index e04d63f5d16a83c84b933d9f59db901c276b7a6d..7b3456986e2766d134f3c1f15f94632feb067fb0 100644 --- a/docs/src/configuring-languages.md +++ b/docs/src/configuring-languages.md @@ -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. diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 13d42a5c4c99f3a4aba3709d829f289e9e9826f8..a3e24506c46054940dc13a52a4ba82cb233c6604 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -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. diff --git a/docs/src/extensions/languages.md b/docs/src/extensions/languages.md index 7eb6a355dbfcafaa01ca885789d41e28c474d2f4..f3ffcd71ba8122956636cd1d228f885383cb83e6 100644 --- a/docs/src/extensions/languages.md +++ b/docs/src/extensions/languages.md @@ -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. diff --git a/docs/src/themes.md b/docs/src/themes.md index 0bbea57ebfd7c9d55031c2ca9ff31b67b360bcdd..615cd2c7b38a734af071ef373b75350231f4a5fb 100644 --- a/docs/src/themes.md +++ b/docs/src/themes.md @@ -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" + ] } } } diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 3e4ff377f3cd54676f0b32f3f4853c9be6de706d..e5185719279dde488c40573d94fd842c06860f4d 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -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} diff --git a/extensions/html/languages/html/brackets.scm b/extensions/html/languages/html/brackets.scm index f9be89a2639d54c08fc2e4e9ce3f6ca3f93ba403..53d6a6bb234e28db21581906ea42e6384f872c9a 100644 --- a/extensions/html/languages/html/brackets.scm +++ b/extensions/html/languages/html/brackets.scm @@ -1,5 +1,5 @@ ("<" @open "/>" @close) ("" @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))