Support bracket colorization (rainbow brackets) (#43172)
Deals with https://github.com/zed-industries/zed/issues/5259 Highlights brackets with different colors based on their depth. Uses existing tree-sitter queries from brackets.scm to find brackets, uses theme's accents to color them. https://github.com/user-attachments/assets/cc5f3aba-22fa-446d-9af7-ba6e772029da 1. Adds `colorize_brackets` language setting that allows, per language or globally for all languages, to configure whether Zed should color the brackets for a particular language. Disabled for all languages by default. 2. Any given language can opt-out a certain bracket pair by amending the brackets.scm like `("\"" @open "\"" @close) ` -> `(("\"" @open "\"" @close) (#set! rainbow.exclude))` 3. Brackets are using colors from theme accents, which can be overridden as ```jsonc "theme_overrides": { "One Dark": { "accents": ["#ff69b4", "#7fff00", "#ff1493", "#00ffff", "#ff8c00", "#9400d3"] } }, ``` Release Notes: - Added bracket colorization (rainbow brackets) support. Use `colorize_brackets` language setting to enable. --------- Co-authored-by: MrSubidubi <dev@bahn.sh> Co-authored-by: Lukas Wirth <lukas@zed.dev> Co-authored-by: MrSubidubi <finn@zed.dev> Co-authored-by: Lukas Wirth <me@lukaswirth.dev> Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
co-authored by
MrSubidubi
Lukas Wirth
MrSubidubi
Lukas Wirth
Smit Barmase
parent
e6e5ccbf10
commit
7e341bcf94
File diff suppressed because it is too large
Load Diff
@@ -483,8 +483,26 @@ impl DisplayMap {
|
||||
key: HighlightKey,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
style: HighlightStyle,
|
||||
merge: bool,
|
||||
cx: &App,
|
||||
) {
|
||||
self.text_highlights.insert(key, Arc::new((style, ranges)));
|
||||
let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let to_insert = match self.text_highlights.remove(&key).filter(|_| merge) {
|
||||
Some(previous) => {
|
||||
let mut merged_ranges = previous.1.clone();
|
||||
for new_range in ranges {
|
||||
let i = merged_ranges
|
||||
.binary_search_by(|probe| {
|
||||
probe.start.cmp(&new_range.start, &multi_buffer_snapshot)
|
||||
})
|
||||
.unwrap_or_else(|i| i);
|
||||
merged_ranges.insert(i, new_range);
|
||||
}
|
||||
Arc::new((style, merged_ranges))
|
||||
}
|
||||
None => Arc::new((style, ranges)),
|
||||
};
|
||||
self.text_highlights.insert(key, to_insert);
|
||||
}
|
||||
|
||||
pub(crate) fn highlight_inlays(
|
||||
@@ -523,6 +541,15 @@ impl DisplayMap {
|
||||
.text_highlights
|
||||
.remove(&HighlightKey::Type(type_id))
|
||||
.is_some();
|
||||
self.text_highlights.retain(|key, _| {
|
||||
let retain = if let HighlightKey::TypePlus(key_type_id, _) = key {
|
||||
key_type_id != &type_id
|
||||
} else {
|
||||
true
|
||||
};
|
||||
cleared |= !retain;
|
||||
retain
|
||||
});
|
||||
cleared |= self.inlay_highlights.remove(&type_id).is_some();
|
||||
cleared
|
||||
}
|
||||
@@ -1382,6 +1409,33 @@ impl DisplaySnapshot {
|
||||
.cloned()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn all_text_highlight_ranges<Tag: ?Sized + 'static>(
|
||||
&self,
|
||||
) -> Vec<(gpui::Hsla, Range<Point>)> {
|
||||
use itertools::Itertools;
|
||||
|
||||
let required_type_id = TypeId::of::<Tag>();
|
||||
self.text_highlights
|
||||
.iter()
|
||||
.filter(|(key, _)| match key {
|
||||
HighlightKey::Type(type_id) => type_id == &required_type_id,
|
||||
HighlightKey::TypePlus(type_id, _) => type_id == &required_type_id,
|
||||
})
|
||||
.map(|(_, value)| value.clone())
|
||||
.flat_map(|ranges| {
|
||||
ranges
|
||||
.1
|
||||
.iter()
|
||||
.flat_map(|range| {
|
||||
Some((ranges.0.color?, range.to_point(self.buffer_snapshot())))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.sorted_by_key(|(_, range)| range.start)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn inlay_highlights<Tag: ?Sized + 'static>(
|
||||
@@ -2387,6 +2441,8 @@ pub mod tests {
|
||||
..buffer_snapshot.anchor_after(Point::new(3, 18)),
|
||||
],
|
||||
red.into(),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
map.insert_blocks(
|
||||
[BlockProperties {
|
||||
@@ -2698,7 +2754,7 @@ pub mod tests {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
map.update(cx, |map, _cx| {
|
||||
map.update(cx, |map, cx| {
|
||||
map.highlight_text(
|
||||
HighlightKey::Type(TypeId::of::<MyType>()),
|
||||
highlighted_ranges
|
||||
@@ -2710,6 +2766,8 @@ pub mod tests {
|
||||
})
|
||||
.collect(),
|
||||
style,
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -40,7 +40,6 @@ impl<'a> CustomHighlightsChunks<'a> {
|
||||
buffer_chunks: multibuffer_snapshot.chunks(range.clone(), language_aware),
|
||||
buffer_chunk: None,
|
||||
offset: range.start,
|
||||
|
||||
text_highlights,
|
||||
highlight_endpoints: create_highlight_endpoints(
|
||||
&range,
|
||||
@@ -75,16 +74,9 @@ fn create_highlight_endpoints(
|
||||
let style = text_highlights.0;
|
||||
let ranges = &text_highlights.1;
|
||||
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = probe.end.cmp(&start, buffer);
|
||||
if cmp.is_gt() {
|
||||
cmp::Ordering::Greater
|
||||
} else {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
let start_ix = ranges
|
||||
.binary_search_by(|probe| probe.end.cmp(&start, buffer).then(cmp::Ordering::Less))
|
||||
.unwrap_or_else(|i| i);
|
||||
|
||||
for range in &ranges[start_ix..] {
|
||||
if range.start.cmp(&end, buffer).is_ge() {
|
||||
|
||||
+92
-16
@@ -13,6 +13,7 @@
|
||||
//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior.
|
||||
pub mod actions;
|
||||
mod blink_manager;
|
||||
mod bracket_colorization;
|
||||
mod clangd_ext;
|
||||
pub mod code_context_menus;
|
||||
pub mod display_map;
|
||||
@@ -118,11 +119,11 @@ use language::{
|
||||
AutoindentMode, BlockCommentConfig, BracketMatch, BracketPair, Buffer, BufferRow,
|
||||
BufferSnapshot, Capability, CharClassifier, CharKind, CharScopeContext, CodeLabel, CursorShape,
|
||||
DiagnosticEntryRef, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, IndentKind,
|
||||
IndentSize, Language, LanguageRegistry, OffsetRangeExt, OutlineItem, Point, Runnable,
|
||||
Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
IndentSize, Language, LanguageName, LanguageRegistry, OffsetRangeExt, OutlineItem, Point,
|
||||
Runnable, Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery,
|
||||
language_settings::{
|
||||
self, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings,
|
||||
language_settings,
|
||||
self, LanguageSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode,
|
||||
all_language_settings, language_settings,
|
||||
},
|
||||
point_from_lsp, point_to_lsp, text_diff_with_options,
|
||||
};
|
||||
@@ -175,6 +176,7 @@ use std::{
|
||||
borrow::Cow,
|
||||
cell::{OnceCell, RefCell},
|
||||
cmp::{self, Ordering, Reverse},
|
||||
collections::hash_map,
|
||||
iter::{self, Peekable},
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
@@ -1193,6 +1195,9 @@ pub struct Editor {
|
||||
folding_newlines: Task<()>,
|
||||
select_next_is_case_sensitive: Option<bool>,
|
||||
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
|
||||
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
|
||||
accent_overrides: Vec<SharedString>,
|
||||
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
|
||||
}
|
||||
|
||||
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
|
||||
@@ -2333,12 +2338,18 @@ impl Editor {
|
||||
folding_newlines: Task::ready(()),
|
||||
lookup_key: None,
|
||||
select_next_is_case_sensitive: None,
|
||||
applicable_language_settings: HashMap::default(),
|
||||
accent_overrides: Vec::new(),
|
||||
fetched_tree_sitter_chunks: HashMap::default(),
|
||||
};
|
||||
|
||||
if is_minimap {
|
||||
return editor;
|
||||
}
|
||||
|
||||
editor.applicable_language_settings = editor.fetch_applicable_language_settings(cx);
|
||||
editor.accent_overrides = editor.fetch_accent_overrides(cx);
|
||||
|
||||
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
|
||||
editor
|
||||
._subscriptions
|
||||
@@ -2378,6 +2389,7 @@ impl Editor {
|
||||
InlayHintRefreshReason::NewLinesShown,
|
||||
cx,
|
||||
);
|
||||
editor.colorize_brackets(false, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
@@ -21141,13 +21153,16 @@ impl Editor {
|
||||
key: usize,
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
style: HighlightStyle,
|
||||
merge: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.display_map.update(cx, |map, _| {
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.highlight_text(
|
||||
HighlightKey::TypePlus(TypeId::of::<T>(), key),
|
||||
ranges,
|
||||
style,
|
||||
merge,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
cx.notify();
|
||||
@@ -21159,8 +21174,14 @@ impl Editor {
|
||||
style: HighlightStyle,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.display_map.update(cx, |map, _| {
|
||||
map.highlight_text(HighlightKey::Type(TypeId::of::<T>()), ranges, style)
|
||||
self.display_map.update(cx, |map, cx| {
|
||||
map.highlight_text(
|
||||
HighlightKey::Type(TypeId::of::<T>()),
|
||||
ranges,
|
||||
style,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@@ -21308,7 +21329,6 @@ impl Editor {
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
self.refresh_active_diagnostics(cx);
|
||||
self.refresh_code_actions(window, cx);
|
||||
self.refresh_selected_text_highlights(true, window, cx);
|
||||
self.refresh_single_line_folds(window, cx);
|
||||
self.refresh_matching_bracket_highlights(window, cx);
|
||||
if self.has_active_edit_prediction() {
|
||||
@@ -21364,6 +21384,7 @@ impl Editor {
|
||||
}
|
||||
self.update_lsp_data(Some(buffer_id), window, cx);
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
self.colorize_brackets(false, cx);
|
||||
cx.emit(EditorEvent::ExcerptsAdded {
|
||||
buffer: buffer.clone(),
|
||||
predecessor: *predecessor,
|
||||
@@ -21401,10 +21422,16 @@ impl Editor {
|
||||
multi_buffer::Event::ExcerptsExpanded { ids } => {
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
self.refresh_document_highlights(cx);
|
||||
for id in ids {
|
||||
self.fetched_tree_sitter_chunks.remove(id);
|
||||
}
|
||||
self.colorize_brackets(false, cx);
|
||||
cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() })
|
||||
}
|
||||
multi_buffer::Event::Reparsed(buffer_id) => {
|
||||
self.tasks_update_task = Some(self.refresh_runnables(window, cx));
|
||||
self.refresh_selected_text_highlights(true, window, cx);
|
||||
self.colorize_brackets(true, cx);
|
||||
jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx);
|
||||
|
||||
cx.emit(EditorEvent::Reparsed(*buffer_id));
|
||||
@@ -21475,7 +21502,52 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn fetch_accent_overrides(&self, cx: &App) -> Vec<SharedString> {
|
||||
if !self.mode.is_full() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
theme::ThemeSettings::get_global(cx)
|
||||
.theme_overrides
|
||||
.get(cx.theme().name.as_ref())
|
||||
.map(|theme_style| &theme_style.accents)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.flat_map(|accent| accent.0.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn fetch_applicable_language_settings(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> HashMap<Option<LanguageName>, LanguageSettings> {
|
||||
if !self.mode.is_full() {
|
||||
return HashMap::default();
|
||||
}
|
||||
|
||||
self.buffer().read(cx).all_buffers().into_iter().fold(
|
||||
HashMap::default(),
|
||||
|mut acc, buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
let language = buffer.language().map(|language| language.name());
|
||||
if let hash_map::Entry::Vacant(v) = acc.entry(language.clone()) {
|
||||
let file = buffer.file();
|
||||
v.insert(language_settings(language, file, cx).into_owned());
|
||||
}
|
||||
acc
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let new_language_settings = self.fetch_applicable_language_settings(cx);
|
||||
let language_settings_changed = new_language_settings != self.applicable_language_settings;
|
||||
self.applicable_language_settings = new_language_settings;
|
||||
|
||||
let new_accent_overrides = self.fetch_accent_overrides(cx);
|
||||
let accent_overrides_changed = new_accent_overrides != self.accent_overrides;
|
||||
self.accent_overrides = new_accent_overrides;
|
||||
|
||||
if self.diagnostics_enabled() {
|
||||
let new_severity = EditorSettings::get_global(cx)
|
||||
.diagnostics_max_severity
|
||||
@@ -21547,15 +21619,19 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
|
||||
colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
|
||||
}) {
|
||||
if !inlay_splice.is_empty() {
|
||||
self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
|
||||
if language_settings_changed || accent_overrides_changed {
|
||||
self.colorize_brackets(true, cx);
|
||||
}
|
||||
|
||||
if let Some(inlay_splice) = self.colors.as_mut().and_then(|colors| {
|
||||
colors.render_mode_updated(EditorSettings::get_global(cx).lsp_document_colors)
|
||||
}) {
|
||||
if !inlay_splice.is_empty() {
|
||||
self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx);
|
||||
}
|
||||
self.refresh_colors_for_visible_range(None, window, cx);
|
||||
}
|
||||
self.refresh_colors_for_visible_range(None, window, cx);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
@@ -22668,7 +22744,7 @@ fn insert_extra_newline_tree_sitter(
|
||||
_ => return false,
|
||||
};
|
||||
let pair = {
|
||||
let mut result: Option<BracketMatch> = None;
|
||||
let mut result: Option<BracketMatch<usize>> = None;
|
||||
|
||||
for pair in buffer
|
||||
.all_bracket_ranges(range.start.0..range.end.0)
|
||||
|
||||
@@ -17565,7 +17565,9 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
|
||||
let mut assert = |before, after| {
|
||||
|
||||
#[track_caller]
|
||||
fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
|
||||
let _state_context = cx.set_state(before);
|
||||
cx.run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
@@ -17573,30 +17575,33 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
});
|
||||
cx.run_until_parked();
|
||||
cx.assert_editor_state(after);
|
||||
};
|
||||
}
|
||||
|
||||
// Outside bracket jumps to outside of matching bracket
|
||||
assert("console.logˇ(var);", "console.log(var)ˇ;");
|
||||
assert("console.log(var)ˇ;", "console.logˇ(var);");
|
||||
assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
|
||||
assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
|
||||
|
||||
// Inside bracket jumps to inside of matching bracket
|
||||
assert("console.log(ˇvar);", "console.log(varˇ);");
|
||||
assert("console.log(varˇ);", "console.log(ˇvar);");
|
||||
assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
|
||||
assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
|
||||
|
||||
// When outside a bracket and inside, favor jumping to the inside bracket
|
||||
assert(
|
||||
"console.log('foo', [1, 2, 3]ˇ);",
|
||||
"console.log(ˇ'foo', [1, 2, 3]);",
|
||||
"console.log('foo', ˇ[1, 2, 3]);",
|
||||
&mut cx,
|
||||
);
|
||||
assert(
|
||||
"console.log(ˇ'foo', [1, 2, 3]);",
|
||||
"console.log('foo', [1, 2, 3]ˇ);",
|
||||
"console.log('foo'ˇ, [1, 2, 3]);",
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Bias forward if two options are equally likely
|
||||
assert(
|
||||
"let result = curried_fun()ˇ();",
|
||||
"let result = curried_fun()()ˇ;",
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
|
||||
@@ -17609,6 +17614,7 @@ async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
|
||||
function test() {
|
||||
console.logˇ('test')
|
||||
}"},
|
||||
&mut cx,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -500,6 +500,7 @@ impl Editor {
|
||||
editor.register_visible_buffers(cx);
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
editor.update_lsp_data(None, window, cx);
|
||||
editor.colorize_brackets(false, cx);
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user