mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-26 07:24:46 +00:00
We had a call to `BlockMap::unfold_intersecting` that ended up recomputing the entire block map for the RHS _without_ spacers, only to throw it away in favor of the version with spacers a few lines down. Now we only sync each block map once in `set_companion`. Release Notes: - Improved performance when toggling from the unified diff to the split diff. Co-authored-by: Jakub <jakub@zed.dev> Co-authored-by: Cameron McLoughlin <cameron.studdstreet@gmail.com>
4099 lines
148 KiB
Rust
4099 lines
148 KiB
Rust
//! This module defines where the text should be displayed in an [`Editor`][Editor].
|
||
//!
|
||
//! Not literally though - rendering, layout and all that jazz is a responsibility of [`EditorElement`][EditorElement].
|
||
//! Instead, [`DisplayMap`] decides where Inlays/Inlay hints are displayed, when
|
||
//! to apply a soft wrap, where to add fold indicators, whether there are any tabs in the buffer that
|
||
//! we display as spaces and where to display custom blocks (like diagnostics).
|
||
//! Seems like a lot? That's because it is. [`DisplayMap`] is conceptually made up
|
||
//! of several smaller structures that form a hierarchy (starting at the bottom):
|
||
//! - [`InlayMap`] that decides where the [`Inlay`]s should be displayed.
|
||
//! - [`FoldMap`] that decides where the fold indicators should be; it also tracks parts of a source file that are currently folded.
|
||
//! - [`TabMap`] that keeps track of hard tabs in a buffer.
|
||
//! - [`WrapMap`] that handles soft wrapping.
|
||
//! - [`BlockMap`] that tracks custom blocks such as diagnostics that should be displayed within buffer.
|
||
//! - [`DisplayMap`] that adds background highlights to the regions of text.
|
||
//! Each one of those builds on top of preceding map.
|
||
//!
|
||
//! ## Structure of the display map layers
|
||
//!
|
||
//! Each layer in the map (and the multibuffer itself to some extent) has a few
|
||
//! structures that are used to implement the public API available to the layer
|
||
//! above:
|
||
//! - a `Transform` type - this represents a region of text that the layer in
|
||
//! question is "managing", that it transforms into a more "processed" text
|
||
//! for the layer above. For example, the inlay map has an `enum Transform`
|
||
//! that has two variants:
|
||
//! - `Isomorphic`, representing a region of text that has no inlay hints (i.e.
|
||
//! is passed through the map transparently)
|
||
//! - `Inlay`, representing a location where an inlay hint is to be inserted.
|
||
//! - a `TransformSummary` type, which is usually a struct with two fields:
|
||
//! [`input: TextSummary`][`TextSummary`] and [`output: TextSummary`][`TextSummary`]. Here,
|
||
//! `input` corresponds to "text in the layer below", and `output` corresponds to the text
|
||
//! exposed to the layer above. So in the inlay map case, a `Transform::Isomorphic`'s summary is
|
||
//! just `input = output = summary`, where `summary` is the [`TextSummary`] stored in that
|
||
//! variant. Conversely, a `Transform::Inlay` always has an empty `input` summary, because it's
|
||
//! not "replacing" any text that exists on disk. The `output` is the summary of the inlay text
|
||
//! to be injected. - Various newtype wrappers for co-ordinate spaces (e.g. [`WrapRow`]
|
||
//! represents a row index, after soft-wrapping (and all lower layers)).
|
||
//! - A `Snapshot` type (e.g. [`InlaySnapshot`]) that captures the state of a layer at a specific
|
||
//! point in time.
|
||
//! - various APIs which drill through the layers below to work with the underlying text. Notably:
|
||
//! - `fn text_summary_for_offset()` returns a [`TextSummary`] for the range in the co-ordinate
|
||
//! space that the map in question is responsible for.
|
||
//! - `fn <A>_point_to_<B>_point()` converts a point in co-ordinate space `A` into co-ordinate
|
||
//! space `B`.
|
||
//! - A [`RowInfo`] iterator (e.g. [`InlayBufferRows`]) and a [`Chunk`] iterator
|
||
//! (e.g. [`InlayChunks`])
|
||
//! - A `sync` function (e.g. [`InlayMap::sync`]) that takes a snapshot and list of [`Edit<T>`]s,
|
||
//! and returns a new snapshot and a list of transformed [`Edit<S>`]s. Note that the generic
|
||
//! parameter on `Edit` changes, since these methods take in edits in the co-ordinate space of
|
||
//! the lower layer, and return edits in their own co-ordinate space. The term "edit" is
|
||
//! slightly misleading, since an [`Edit<T>`] doesn't tell you what changed - rather it can be
|
||
//! thought of as a "region to invalidate". In theory, it would be correct to always use a
|
||
//! single edit that covers the entire range. However, this would lead to lots of unnecessary
|
||
//! recalculation.
|
||
//!
|
||
//! See the docs for the [`inlay_map`] module for a more in-depth explanation of how a single layer
|
||
//! works.
|
||
//!
|
||
//! [Editor]: crate::Editor
|
||
//! [EditorElement]: crate::element::EditorElement
|
||
//! [`TextSummary`]: multi_buffer::MBTextSummary
|
||
//! [`WrapRow`]: wrap_map::WrapRow
|
||
//! [`InlayBufferRows`]: inlay_map::InlayBufferRows
|
||
//! [`InlayChunks`]: inlay_map::InlayChunks
|
||
//! [`Edit<T>`]: text::Edit
|
||
//! [`Edit<S>`]: text::Edit
|
||
//! [`Chunk`]: language::Chunk
|
||
|
||
#[macro_use]
|
||
mod dimensions;
|
||
|
||
mod block_map;
|
||
mod crease_map;
|
||
mod custom_highlights;
|
||
mod fold_map;
|
||
mod inlay_map;
|
||
mod invisibles;
|
||
mod tab_map;
|
||
mod wrap_map;
|
||
|
||
pub use crate::display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap};
|
||
pub use block_map::{
|
||
Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement,
|
||
BlockPoint, BlockProperties, BlockRows, BlockStyle, CompanionView, CompanionViewMut,
|
||
CustomBlockId, EditorMargins, RenderBlock, StickyHeaderExcerpt,
|
||
};
|
||
pub use crease_map::*;
|
||
pub use fold_map::{
|
||
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
|
||
};
|
||
pub use inlay_map::{InlayOffset, InlayPoint};
|
||
pub use invisibles::{is_invisible, replacement};
|
||
pub use wrap_map::{WrapPoint, WrapRow, WrapSnapshot};
|
||
|
||
use collections::{HashMap, HashSet, IndexSet};
|
||
use gpui::{
|
||
App, Context, Entity, EntityId, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle,
|
||
WeakEntity,
|
||
};
|
||
use language::{Point, Subscription as BufferSubscription, language_settings::language_settings};
|
||
use multi_buffer::{
|
||
Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16,
|
||
MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, RowInfo, ToOffset, ToPoint,
|
||
};
|
||
use project::project_settings::DiagnosticSeverity;
|
||
use project::{InlayId, lsp_store::LspFoldingRange, lsp_store::TokenType};
|
||
use serde::Deserialize;
|
||
use smallvec::SmallVec;
|
||
use sum_tree::{Bias, TreeMap};
|
||
use text::{BufferId, LineIndent, Patch, ToOffset as _};
|
||
use ui::{SharedString, px};
|
||
use unicode_segmentation::UnicodeSegmentation;
|
||
use ztracing::instrument;
|
||
|
||
use std::cell::RefCell;
|
||
use std::{
|
||
any::TypeId,
|
||
borrow::Cow,
|
||
fmt::Debug,
|
||
iter,
|
||
num::NonZeroU32,
|
||
ops::{self, Add, Bound, Range, Sub},
|
||
sync::Arc,
|
||
};
|
||
|
||
use crate::{
|
||
EditorStyle, RowExt, hover_links::InlayHighlight, inlays::Inlay, movement::TextLayoutDetails,
|
||
};
|
||
use block_map::{BlockRow, BlockSnapshot};
|
||
use fold_map::FoldSnapshot;
|
||
use inlay_map::InlaySnapshot;
|
||
use tab_map::TabSnapshot;
|
||
use wrap_map::{WrapMap, WrapPatch};
|
||
|
||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||
pub enum FoldStatus {
|
||
Folded,
|
||
Foldable,
|
||
}
|
||
|
||
/// Keys for tagging text highlights.
|
||
///
|
||
/// Note the order is important as it determines the priority of the highlights, lower means higher priority
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||
pub enum HighlightKey {
|
||
// Note we want semantic tokens > colorized brackets
|
||
// to allow language server highlights to work over brackets.
|
||
ColorizeBracket(usize),
|
||
SemanticToken,
|
||
// below is sorted lexicographically, as there is no relevant ordering for these aside from coming after the above
|
||
BufferSearchHighlights,
|
||
ConsoleAnsiHighlight(usize),
|
||
DebugStackFrameLine,
|
||
DocumentHighlightRead,
|
||
DocumentHighlightWrite,
|
||
EditPredictionHighlight,
|
||
Editor,
|
||
HighlightOnYank,
|
||
HighlightsTreeView(usize),
|
||
HoverState,
|
||
HoveredLinkState,
|
||
InlineAssist,
|
||
InputComposition,
|
||
MatchingBracket,
|
||
PendingInput,
|
||
ProjectSearchView,
|
||
Rename,
|
||
SearchWithinRange,
|
||
SelectedTextHighlight,
|
||
SyntaxTreeView(usize),
|
||
VimExchange,
|
||
}
|
||
|
||
pub trait ToDisplayPoint {
|
||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
|
||
}
|
||
|
||
type TextHighlights = TreeMap<HighlightKey, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
|
||
type SemanticTokensHighlights =
|
||
TreeMap<BufferId, (Arc<[SemanticTokenHighlight]>, Arc<HighlightStyleInterner>)>;
|
||
type InlayHighlights = TreeMap<HighlightKey, TreeMap<InlayId, (HighlightStyle, InlayHighlight)>>;
|
||
|
||
#[derive(Debug)]
|
||
pub struct CompanionExcerptPatch {
|
||
pub patch: Patch<MultiBufferPoint>,
|
||
pub edited_range: Range<MultiBufferPoint>,
|
||
pub source_excerpt_range: Range<MultiBufferPoint>,
|
||
pub target_excerpt_range: Range<MultiBufferPoint>,
|
||
}
|
||
|
||
pub type ConvertMultiBufferRows = fn(
|
||
&HashMap<ExcerptId, ExcerptId>,
|
||
&MultiBufferSnapshot,
|
||
&MultiBufferSnapshot,
|
||
(Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
|
||
) -> Vec<CompanionExcerptPatch>;
|
||
|
||
/// Decides how text in a [`MultiBuffer`] should be displayed in a buffer, handling inlay hints,
|
||
/// folding, hard tabs, soft wrapping, custom blocks (like diagnostics), and highlighting.
|
||
///
|
||
/// See the [module level documentation](self) for more information.
|
||
pub struct DisplayMap {
|
||
entity_id: EntityId,
|
||
/// The buffer that we are displaying.
|
||
buffer: Entity<MultiBuffer>,
|
||
buffer_subscription: BufferSubscription<MultiBufferOffset>,
|
||
/// Decides where the [`Inlay`]s should be displayed.
|
||
inlay_map: InlayMap,
|
||
/// Decides where the fold indicators should be and tracks parts of a source file that are currently folded.
|
||
fold_map: FoldMap,
|
||
/// Keeps track of hard tabs in a buffer.
|
||
tab_map: TabMap,
|
||
/// Handles soft wrapping.
|
||
wrap_map: Entity<WrapMap>,
|
||
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
|
||
block_map: BlockMap,
|
||
/// Regions of text that should be highlighted.
|
||
text_highlights: TextHighlights,
|
||
/// Regions of inlays that should be highlighted.
|
||
inlay_highlights: InlayHighlights,
|
||
/// The semantic tokens from the language server.
|
||
pub semantic_token_highlights: SemanticTokensHighlights,
|
||
/// A container for explicitly foldable ranges, which supersede indentation based fold range suggestions.
|
||
crease_map: CreaseMap,
|
||
pub(crate) fold_placeholder: FoldPlaceholder,
|
||
pub clip_at_line_ends: bool,
|
||
pub(crate) masked: bool,
|
||
pub(crate) diagnostics_max_severity: DiagnosticSeverity,
|
||
pub(crate) companion: Option<(WeakEntity<DisplayMap>, Entity<Companion>)>,
|
||
lsp_folding_crease_ids: HashMap<BufferId, Vec<CreaseId>>,
|
||
}
|
||
|
||
pub(crate) struct Companion {
|
||
rhs_display_map_id: EntityId,
|
||
rhs_buffer_to_lhs_buffer: HashMap<BufferId, BufferId>,
|
||
lhs_buffer_to_rhs_buffer: HashMap<BufferId, BufferId>,
|
||
rhs_excerpt_to_lhs_excerpt: HashMap<ExcerptId, ExcerptId>,
|
||
lhs_excerpt_to_rhs_excerpt: HashMap<ExcerptId, ExcerptId>,
|
||
rhs_rows_to_lhs_rows: ConvertMultiBufferRows,
|
||
lhs_rows_to_rhs_rows: ConvertMultiBufferRows,
|
||
rhs_custom_block_to_balancing_block: RefCell<HashMap<CustomBlockId, CustomBlockId>>,
|
||
lhs_custom_block_to_balancing_block: RefCell<HashMap<CustomBlockId, CustomBlockId>>,
|
||
}
|
||
|
||
impl Companion {
|
||
pub(crate) fn new(
|
||
rhs_display_map_id: EntityId,
|
||
rhs_rows_to_lhs_rows: ConvertMultiBufferRows,
|
||
lhs_rows_to_rhs_rows: ConvertMultiBufferRows,
|
||
) -> Self {
|
||
Self {
|
||
rhs_display_map_id,
|
||
rhs_buffer_to_lhs_buffer: Default::default(),
|
||
lhs_buffer_to_rhs_buffer: Default::default(),
|
||
rhs_excerpt_to_lhs_excerpt: Default::default(),
|
||
lhs_excerpt_to_rhs_excerpt: Default::default(),
|
||
rhs_rows_to_lhs_rows,
|
||
lhs_rows_to_rhs_rows,
|
||
rhs_custom_block_to_balancing_block: Default::default(),
|
||
lhs_custom_block_to_balancing_block: Default::default(),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn is_rhs(&self, display_map_id: EntityId) -> bool {
|
||
self.rhs_display_map_id == display_map_id
|
||
}
|
||
|
||
pub(crate) fn custom_block_to_balancing_block(
|
||
&self,
|
||
display_map_id: EntityId,
|
||
) -> &RefCell<HashMap<CustomBlockId, CustomBlockId>> {
|
||
if self.is_rhs(display_map_id) {
|
||
&self.rhs_custom_block_to_balancing_block
|
||
} else {
|
||
&self.lhs_custom_block_to_balancing_block
|
||
}
|
||
}
|
||
|
||
pub(crate) fn convert_rows_to_companion(
|
||
&self,
|
||
display_map_id: EntityId,
|
||
companion_snapshot: &MultiBufferSnapshot,
|
||
our_snapshot: &MultiBufferSnapshot,
|
||
bounds: (Bound<MultiBufferPoint>, Bound<MultiBufferPoint>),
|
||
) -> Vec<CompanionExcerptPatch> {
|
||
let (excerpt_map, convert_fn) = if self.is_rhs(display_map_id) {
|
||
(&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows)
|
||
} else {
|
||
(&self.lhs_excerpt_to_rhs_excerpt, self.lhs_rows_to_rhs_rows)
|
||
};
|
||
convert_fn(excerpt_map, companion_snapshot, our_snapshot, bounds)
|
||
}
|
||
|
||
pub(crate) fn convert_point_from_companion(
|
||
&self,
|
||
display_map_id: EntityId,
|
||
our_snapshot: &MultiBufferSnapshot,
|
||
companion_snapshot: &MultiBufferSnapshot,
|
||
point: MultiBufferPoint,
|
||
) -> Range<MultiBufferPoint> {
|
||
let (excerpt_map, convert_fn) = if self.is_rhs(display_map_id) {
|
||
(&self.lhs_excerpt_to_rhs_excerpt, self.lhs_rows_to_rhs_rows)
|
||
} else {
|
||
(&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows)
|
||
};
|
||
|
||
let excerpt = convert_fn(
|
||
excerpt_map,
|
||
our_snapshot,
|
||
companion_snapshot,
|
||
(Bound::Included(point), Bound::Included(point)),
|
||
)
|
||
.into_iter()
|
||
.next();
|
||
|
||
let Some(excerpt) = excerpt else {
|
||
return Point::zero()..our_snapshot.max_point();
|
||
};
|
||
excerpt.patch.edit_for_old_position(point).new
|
||
}
|
||
|
||
pub(crate) fn convert_point_to_companion(
|
||
&self,
|
||
display_map_id: EntityId,
|
||
our_snapshot: &MultiBufferSnapshot,
|
||
companion_snapshot: &MultiBufferSnapshot,
|
||
point: MultiBufferPoint,
|
||
) -> Range<MultiBufferPoint> {
|
||
let (excerpt_map, convert_fn) = if self.is_rhs(display_map_id) {
|
||
(&self.rhs_excerpt_to_lhs_excerpt, self.rhs_rows_to_lhs_rows)
|
||
} else {
|
||
(&self.lhs_excerpt_to_rhs_excerpt, self.lhs_rows_to_rhs_rows)
|
||
};
|
||
|
||
let excerpt = convert_fn(
|
||
excerpt_map,
|
||
companion_snapshot,
|
||
our_snapshot,
|
||
(Bound::Included(point), Bound::Included(point)),
|
||
)
|
||
.into_iter()
|
||
.next();
|
||
|
||
let Some(excerpt) = excerpt else {
|
||
return Point::zero()..companion_snapshot.max_point();
|
||
};
|
||
excerpt.patch.edit_for_old_position(point).new
|
||
}
|
||
|
||
pub(crate) fn companion_excerpt_to_excerpt(
|
||
&self,
|
||
display_map_id: EntityId,
|
||
) -> &HashMap<ExcerptId, ExcerptId> {
|
||
if self.is_rhs(display_map_id) {
|
||
&self.lhs_excerpt_to_rhs_excerpt
|
||
} else {
|
||
&self.rhs_excerpt_to_lhs_excerpt
|
||
}
|
||
}
|
||
|
||
fn buffer_to_companion_buffer(&self, display_map_id: EntityId) -> &HashMap<BufferId, BufferId> {
|
||
if self.is_rhs(display_map_id) {
|
||
&self.rhs_buffer_to_lhs_buffer
|
||
} else {
|
||
&self.lhs_buffer_to_rhs_buffer
|
||
}
|
||
}
|
||
|
||
pub(crate) fn add_excerpt_mapping(&mut self, lhs_id: ExcerptId, rhs_id: ExcerptId) {
|
||
self.lhs_excerpt_to_rhs_excerpt.insert(lhs_id, rhs_id);
|
||
self.rhs_excerpt_to_lhs_excerpt.insert(rhs_id, lhs_id);
|
||
}
|
||
|
||
pub(crate) fn remove_excerpt_mappings(
|
||
&mut self,
|
||
lhs_ids: impl IntoIterator<Item = ExcerptId>,
|
||
rhs_ids: impl IntoIterator<Item = ExcerptId>,
|
||
) {
|
||
for id in lhs_ids {
|
||
self.lhs_excerpt_to_rhs_excerpt.remove(&id);
|
||
}
|
||
for id in rhs_ids {
|
||
self.rhs_excerpt_to_lhs_excerpt.remove(&id);
|
||
}
|
||
}
|
||
|
||
pub(crate) fn lhs_to_rhs_buffer(&self, lhs_buffer_id: BufferId) -> Option<BufferId> {
|
||
self.lhs_buffer_to_rhs_buffer.get(&lhs_buffer_id).copied()
|
||
}
|
||
|
||
pub(crate) fn add_buffer_mapping(&mut self, lhs_buffer: BufferId, rhs_buffer: BufferId) {
|
||
self.lhs_buffer_to_rhs_buffer.insert(lhs_buffer, rhs_buffer);
|
||
self.rhs_buffer_to_lhs_buffer.insert(rhs_buffer, lhs_buffer);
|
||
}
|
||
}
|
||
|
||
#[derive(Default, Debug)]
|
||
pub struct HighlightStyleInterner {
|
||
styles: IndexSet<HighlightStyle>,
|
||
}
|
||
|
||
impl HighlightStyleInterner {
|
||
pub(crate) fn intern(&mut self, style: HighlightStyle) -> HighlightStyleId {
|
||
HighlightStyleId(self.styles.insert_full(style).0 as u32)
|
||
}
|
||
}
|
||
|
||
impl ops::Index<HighlightStyleId> for HighlightStyleInterner {
|
||
type Output = HighlightStyle;
|
||
|
||
fn index(&self, index: HighlightStyleId) -> &Self::Output {
|
||
&self.styles[index.0 as usize]
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||
pub struct HighlightStyleId(u32);
|
||
|
||
/// A `SemanticToken`, but positioned to an offset in a buffer, and stylized.
|
||
#[derive(Debug, Clone)]
|
||
pub struct SemanticTokenHighlight {
|
||
pub range: Range<Anchor>,
|
||
pub style: HighlightStyleId,
|
||
pub token_type: TokenType,
|
||
pub token_modifiers: u32,
|
||
pub server_id: lsp::LanguageServerId,
|
||
}
|
||
|
||
impl DisplayMap {
|
||
pub fn new(
|
||
buffer: Entity<MultiBuffer>,
|
||
font: Font,
|
||
font_size: Pixels,
|
||
wrap_width: Option<Pixels>,
|
||
buffer_header_height: u32,
|
||
excerpt_header_height: u32,
|
||
fold_placeholder: FoldPlaceholder,
|
||
diagnostics_max_severity: DiagnosticSeverity,
|
||
cx: &mut Context<Self>,
|
||
) -> Self {
|
||
let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||
|
||
let tab_size = Self::tab_size(&buffer, cx);
|
||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||
let crease_map = CreaseMap::new(&buffer_snapshot);
|
||
let (inlay_map, snapshot) = InlayMap::new(buffer_snapshot);
|
||
let (fold_map, snapshot) = FoldMap::new(snapshot);
|
||
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
|
||
let (wrap_map, snapshot) = WrapMap::new(snapshot, font, font_size, wrap_width, cx);
|
||
let block_map = BlockMap::new(snapshot, buffer_header_height, excerpt_header_height);
|
||
|
||
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
|
||
|
||
DisplayMap {
|
||
entity_id: cx.entity_id(),
|
||
buffer,
|
||
buffer_subscription,
|
||
fold_map,
|
||
inlay_map,
|
||
tab_map,
|
||
wrap_map,
|
||
block_map,
|
||
crease_map,
|
||
fold_placeholder,
|
||
diagnostics_max_severity,
|
||
text_highlights: Default::default(),
|
||
inlay_highlights: Default::default(),
|
||
semantic_token_highlights: TreeMap::default(),
|
||
clip_at_line_ends: false,
|
||
masked: false,
|
||
companion: None,
|
||
lsp_folding_crease_ids: HashMap::default(),
|
||
}
|
||
}
|
||
|
||
// TODO(split-diff) figure out how to free the LHS from having to build a block map before this is called
|
||
pub(crate) fn set_companion(
|
||
&mut self,
|
||
companion: Option<(Entity<DisplayMap>, Entity<Companion>)>,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
let this = cx.weak_entity();
|
||
// Reverting to no companion, recompute the block map to clear spacers
|
||
// and balancing blocks.
|
||
let Some((companion_display_map, companion)) = companion else {
|
||
let Some((_, companion)) = self.companion.take() else {
|
||
return;
|
||
};
|
||
let (snapshot, edits) = self.sync_through_wrap(cx);
|
||
let edits = edits.compose([text::Edit {
|
||
old: WrapRow(0)..snapshot.max_point().row(),
|
||
new: WrapRow(0)..snapshot.max_point().row(),
|
||
}]);
|
||
self.block_map.write(snapshot, edits, None).remove(
|
||
companion
|
||
.read(cx)
|
||
.lhs_custom_block_to_balancing_block
|
||
.borrow()
|
||
.values()
|
||
.copied()
|
||
.collect(),
|
||
);
|
||
return;
|
||
};
|
||
assert_eq!(self.entity_id, companion.read(cx).rhs_display_map_id);
|
||
|
||
// Note, throwing away the wrap edits because we're going to recompute the maximal range for the block map regardless
|
||
let old_max_row = self.block_map.wrap_snapshot.borrow().max_point().row();
|
||
let snapshot = {
|
||
let edits = self.buffer_subscription.consume();
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits.into_inner());
|
||
let (mut writer, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (_snapshot, _edits) = self
|
||
.wrap_map
|
||
.update(cx, |wrap_map, cx| wrap_map.sync(snapshot, edits, cx));
|
||
|
||
let (snapshot, edits) =
|
||
writer.unfold_intersecting([Anchor::min()..Anchor::max()], true);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (snapshot, _edits) = self
|
||
.wrap_map
|
||
.update(cx, |wrap_map, cx| wrap_map.sync(snapshot, edits, cx));
|
||
|
||
self.block_map
|
||
.retain_blocks_raw(|block| !matches!(block.placement, BlockPlacement::Replace(_)));
|
||
snapshot
|
||
};
|
||
|
||
let (companion_wrap_snapshot, companion_wrap_edits) =
|
||
companion_display_map.update(cx, |dm, cx| dm.sync_through_wrap(cx));
|
||
|
||
let edits = Patch::new(
|
||
[text::Edit {
|
||
old: WrapRow(0)..old_max_row,
|
||
new: WrapRow(0)..snapshot.max_point().row(),
|
||
}]
|
||
.into_iter()
|
||
.collect(),
|
||
);
|
||
|
||
let reader = self.block_map.read(
|
||
snapshot.clone(),
|
||
edits.clone(),
|
||
Some(CompanionView::new(
|
||
self.entity_id,
|
||
&companion_wrap_snapshot,
|
||
&companion_wrap_edits,
|
||
companion.read(cx),
|
||
)),
|
||
);
|
||
|
||
companion_display_map.update(cx, |companion_display_map, cx| {
|
||
for my_buffer in self.folded_buffers() {
|
||
let their_buffer = companion
|
||
.read(cx)
|
||
.rhs_buffer_to_lhs_buffer
|
||
.get(my_buffer)
|
||
.unwrap();
|
||
companion_display_map
|
||
.block_map
|
||
.folded_buffers
|
||
.insert(*their_buffer);
|
||
}
|
||
for block in reader.blocks {
|
||
let Some(their_block) = block_map::balancing_block(
|
||
&block.properties(),
|
||
snapshot.buffer(),
|
||
companion_wrap_snapshot.buffer(),
|
||
self.entity_id,
|
||
companion.read(cx),
|
||
) else {
|
||
continue;
|
||
};
|
||
let their_id = companion_display_map
|
||
.block_map
|
||
.insert_block_raw(their_block, companion_wrap_snapshot.buffer());
|
||
companion.update(cx, |companion, _cx| {
|
||
companion
|
||
.custom_block_to_balancing_block(self.entity_id)
|
||
.borrow_mut()
|
||
.insert(block.id, their_id);
|
||
});
|
||
}
|
||
companion_display_map.block_map.read(
|
||
companion_wrap_snapshot,
|
||
companion_wrap_edits,
|
||
Some(CompanionView::new(
|
||
companion_display_map.entity_id,
|
||
&snapshot,
|
||
&edits,
|
||
companion.read(cx),
|
||
)),
|
||
);
|
||
companion_display_map.companion = Some((this, companion.clone()));
|
||
});
|
||
|
||
self.companion = Some((companion_display_map.downgrade(), companion));
|
||
}
|
||
|
||
pub(crate) fn companion(&self) -> Option<&Entity<Companion>> {
|
||
self.companion.as_ref().map(|(_, c)| c)
|
||
}
|
||
|
||
pub(crate) fn companion_excerpt_to_my_excerpt(
|
||
&self,
|
||
their_id: ExcerptId,
|
||
cx: &App,
|
||
) -> Option<ExcerptId> {
|
||
let (_, companion) = self.companion.as_ref()?;
|
||
let c = companion.read(cx);
|
||
c.companion_excerpt_to_excerpt(self.entity_id)
|
||
.get(&their_id)
|
||
.copied()
|
||
}
|
||
|
||
fn sync_through_wrap(&mut self, cx: &mut App) -> (WrapSnapshot, WrapPatch) {
|
||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||
let edits = self.buffer_subscription.consume().into_inner();
|
||
|
||
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
|
||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
self.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx))
|
||
}
|
||
|
||
fn with_synced_companion_mut<R>(
|
||
display_map_id: EntityId,
|
||
companion: &Option<(WeakEntity<DisplayMap>, Entity<Companion>)>,
|
||
cx: &mut App,
|
||
callback: impl FnOnce(Option<CompanionViewMut<'_>>, &mut App) -> R,
|
||
) -> R {
|
||
let Some((companion_display_map, companion)) = companion else {
|
||
return callback(None, cx);
|
||
};
|
||
let Some(companion_display_map) = companion_display_map.upgrade() else {
|
||
return callback(None, cx);
|
||
};
|
||
companion_display_map.update(cx, |companion_display_map, cx| {
|
||
let (companion_wrap_snapshot, companion_wrap_edits) =
|
||
companion_display_map.sync_through_wrap(cx);
|
||
companion_display_map
|
||
.buffer
|
||
.update(cx, |companion_multibuffer, cx| {
|
||
companion.update(cx, |companion, cx| {
|
||
let companion_view = CompanionViewMut::new(
|
||
display_map_id,
|
||
companion_display_map.entity_id,
|
||
&companion_wrap_snapshot,
|
||
&companion_wrap_edits,
|
||
companion_multibuffer,
|
||
companion,
|
||
&mut companion_display_map.block_map,
|
||
);
|
||
callback(Some(companion_view), cx)
|
||
})
|
||
})
|
||
})
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn snapshot(&mut self, cx: &mut Context<Self>) -> DisplaySnapshot {
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
let companion_wrap_data = self.companion.as_ref().and_then(|(companion_dm, _)| {
|
||
companion_dm
|
||
.update(cx, |dm, cx| dm.sync_through_wrap(cx))
|
||
.ok()
|
||
});
|
||
let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
|
||
let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
|
||
|((snapshot, edits), companion)| {
|
||
CompanionView::new(self.entity_id, snapshot, edits, companion)
|
||
},
|
||
);
|
||
|
||
let block_snapshot = self
|
||
.block_map
|
||
.read(
|
||
self_wrap_snapshot.clone(),
|
||
self_wrap_edits.clone(),
|
||
companion_view,
|
||
)
|
||
.snapshot;
|
||
|
||
if let Some((companion_dm, _)) = &self.companion {
|
||
let _ = companion_dm.update(cx, |dm, _cx| {
|
||
if let Some((companion_snapshot, companion_edits)) = companion_wrap_data {
|
||
let their_companion_ref = dm.companion.as_ref().map(|(_, c)| c.read(_cx));
|
||
dm.block_map.read(
|
||
companion_snapshot,
|
||
companion_edits,
|
||
their_companion_ref.map(|c| {
|
||
CompanionView::new(
|
||
dm.entity_id,
|
||
&self_wrap_snapshot,
|
||
&self_wrap_edits,
|
||
c,
|
||
)
|
||
}),
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
let companion_display_snapshot = self.companion.as_ref().and_then(|(companion_dm, _)| {
|
||
companion_dm
|
||
.update(cx, |dm, cx| Arc::new(dm.snapshot_simple(cx)))
|
||
.ok()
|
||
});
|
||
|
||
DisplaySnapshot {
|
||
display_map_id: self.entity_id,
|
||
companion_display_snapshot,
|
||
block_snapshot,
|
||
diagnostics_max_severity: self.diagnostics_max_severity,
|
||
crease_snapshot: self.crease_map.snapshot(),
|
||
text_highlights: self.text_highlights.clone(),
|
||
inlay_highlights: self.inlay_highlights.clone(),
|
||
semantic_token_highlights: self.semantic_token_highlights.clone(),
|
||
clip_at_line_ends: self.clip_at_line_ends,
|
||
masked: self.masked,
|
||
use_lsp_folding_ranges: !self.lsp_folding_crease_ids.is_empty(),
|
||
fold_placeholder: self.fold_placeholder.clone(),
|
||
}
|
||
}
|
||
|
||
fn snapshot_simple(&mut self, cx: &mut Context<Self>) -> DisplaySnapshot {
|
||
let (wrap_snapshot, wrap_edits) = self.sync_through_wrap(cx);
|
||
|
||
let block_snapshot = self
|
||
.block_map
|
||
.read(wrap_snapshot, wrap_edits, None)
|
||
.snapshot;
|
||
|
||
DisplaySnapshot {
|
||
display_map_id: self.entity_id,
|
||
companion_display_snapshot: None,
|
||
block_snapshot,
|
||
diagnostics_max_severity: self.diagnostics_max_severity,
|
||
crease_snapshot: self.crease_map.snapshot(),
|
||
text_highlights: self.text_highlights.clone(),
|
||
inlay_highlights: self.inlay_highlights.clone(),
|
||
semantic_token_highlights: self.semantic_token_highlights.clone(),
|
||
clip_at_line_ends: self.clip_at_line_ends,
|
||
masked: self.masked,
|
||
use_lsp_folding_ranges: !self.lsp_folding_crease_ids.is_empty(),
|
||
fold_placeholder: self.fold_placeholder.clone(),
|
||
}
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut Context<Self>) {
|
||
self.fold(
|
||
other
|
||
.folds_in_range(MultiBufferOffset(0)..other.buffer_snapshot().len())
|
||
.map(|fold| {
|
||
Crease::simple(
|
||
fold.range.to_offset(other.buffer_snapshot()),
|
||
fold.placeholder.clone(),
|
||
)
|
||
})
|
||
.collect(),
|
||
cx,
|
||
);
|
||
}
|
||
|
||
/// Creates folds for the given creases.
|
||
#[instrument(skip_all)]
|
||
pub fn fold<T: Clone + ToOffset>(&mut self, creases: Vec<Crease<T>>, cx: &mut Context<Self>) {
|
||
if self.companion().is_some() {
|
||
return;
|
||
}
|
||
|
||
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||
let edits = self.buffer_subscription.consume().into_inner();
|
||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||
|
||
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot.clone(), edits);
|
||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (snapshot, edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
self.block_map.read(snapshot, edits, None);
|
||
|
||
let inline = creases.iter().filter_map(|crease| {
|
||
if let Crease::Inline {
|
||
range, placeholder, ..
|
||
} = crease
|
||
{
|
||
Some((range.clone(), placeholder.clone()))
|
||
} else {
|
||
None
|
||
}
|
||
});
|
||
let (snapshot, edits) = fold_map.fold(inline);
|
||
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (snapshot, edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
|
||
let blocks = creases
|
||
.into_iter()
|
||
.filter_map(|crease| {
|
||
if let Crease::Block {
|
||
range,
|
||
block_height,
|
||
render_block,
|
||
block_style,
|
||
block_priority,
|
||
..
|
||
} = crease
|
||
{
|
||
Some((
|
||
range,
|
||
render_block,
|
||
block_height,
|
||
block_style,
|
||
block_priority,
|
||
))
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.map(|(range, render, height, style, priority)| {
|
||
let start = buffer_snapshot.anchor_before(range.start);
|
||
let end = buffer_snapshot.anchor_after(range.end);
|
||
BlockProperties {
|
||
placement: BlockPlacement::Replace(start..=end),
|
||
render,
|
||
height: Some(height),
|
||
style,
|
||
priority,
|
||
}
|
||
});
|
||
|
||
self.block_map.write(snapshot, edits, None).insert(blocks);
|
||
}
|
||
|
||
/// Removes any folds with the given ranges.
|
||
#[instrument(skip_all)]
|
||
pub fn remove_folds_with_type<T: ToOffset>(
|
||
&mut self,
|
||
ranges: impl IntoIterator<Item = Range<T>>,
|
||
type_id: TypeId,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
let edits = self.buffer_subscription.consume().into_inner();
|
||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||
|
||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (snapshot, edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
self.block_map.read(snapshot, edits, None);
|
||
|
||
let (snapshot, edits) = fold_map.remove_folds(ranges, type_id);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (self_new_wrap_snapshot, self_new_wrap_edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
|
||
self.block_map
|
||
.write(self_new_wrap_snapshot, self_new_wrap_edits, None);
|
||
}
|
||
|
||
/// Removes any folds whose ranges intersect any of the given ranges.
|
||
#[instrument(skip_all)]
|
||
pub fn unfold_intersecting<T: ToOffset>(
|
||
&mut self,
|
||
ranges: impl IntoIterator<Item = Range<T>>,
|
||
inclusive: bool,
|
||
cx: &mut Context<Self>,
|
||
) -> WrapSnapshot {
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
let offset_ranges = ranges
|
||
.into_iter()
|
||
.map(|range| range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot))
|
||
.collect::<Vec<_>>();
|
||
let edits = self.buffer_subscription.consume().into_inner();
|
||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||
|
||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (snapshot, edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
self.block_map.read(snapshot, edits, None);
|
||
|
||
let (snapshot, edits) =
|
||
fold_map.unfold_intersecting(offset_ranges.iter().cloned(), inclusive);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (self_new_wrap_snapshot, self_new_wrap_edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
|
||
self.block_map
|
||
.write(self_new_wrap_snapshot.clone(), self_new_wrap_edits, None)
|
||
.remove_intersecting_replace_blocks(offset_ranges, inclusive);
|
||
|
||
self_new_wrap_snapshot
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
self.block_map
|
||
.write(self_wrap_snapshot, self_wrap_edits, None)
|
||
.disable_header_for_buffer(buffer_id);
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn fold_buffers(
|
||
&mut self,
|
||
buffer_ids: impl IntoIterator<Item = language::BufferId>,
|
||
cx: &mut App,
|
||
) {
|
||
let buffer_ids: Vec<_> = buffer_ids.into_iter().collect();
|
||
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
|
||
Self::with_synced_companion_mut(
|
||
self.entity_id,
|
||
&self.companion,
|
||
cx,
|
||
|companion_view, cx| {
|
||
self.block_map
|
||
.write(
|
||
self_wrap_snapshot.clone(),
|
||
self_wrap_edits.clone(),
|
||
companion_view,
|
||
)
|
||
.fold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
|
||
},
|
||
)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn unfold_buffers(
|
||
&mut self,
|
||
buffer_ids: impl IntoIterator<Item = language::BufferId>,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
let buffer_ids: Vec<_> = buffer_ids.into_iter().collect();
|
||
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
|
||
Self::with_synced_companion_mut(
|
||
self.entity_id,
|
||
&self.companion,
|
||
cx,
|
||
|companion_view, cx| {
|
||
self.block_map
|
||
.write(
|
||
self_wrap_snapshot.clone(),
|
||
self_wrap_edits.clone(),
|
||
companion_view,
|
||
)
|
||
.unfold_buffers(buffer_ids.iter().copied(), self.buffer.read(cx), cx);
|
||
},
|
||
)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub(crate) fn is_buffer_folded(&self, buffer_id: language::BufferId) -> bool {
|
||
self.block_map.folded_buffers.contains(&buffer_id)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub(crate) fn folded_buffers(&self) -> &HashSet<BufferId> {
|
||
&self.block_map.folded_buffers
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn insert_creases(
|
||
&mut self,
|
||
creases: impl IntoIterator<Item = Crease<Anchor>>,
|
||
cx: &mut Context<Self>,
|
||
) -> Vec<CreaseId> {
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
self.crease_map.insert(creases, &snapshot)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn remove_creases(
|
||
&mut self,
|
||
crease_ids: impl IntoIterator<Item = CreaseId>,
|
||
cx: &mut Context<Self>,
|
||
) -> Vec<(CreaseId, Range<Anchor>)> {
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
self.crease_map.remove(crease_ids, &snapshot)
|
||
}
|
||
|
||
/// Replaces the LSP folding-range creases for a single buffer.
|
||
/// Converts the supplied buffer-anchor ranges into multi-buffer creases
|
||
/// by mapping them through the appropriate excerpts.
|
||
pub(super) fn set_lsp_folding_ranges(
|
||
&mut self,
|
||
buffer_id: BufferId,
|
||
ranges: Vec<LspFoldingRange>,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
|
||
let old_ids = self
|
||
.lsp_folding_crease_ids
|
||
.remove(&buffer_id)
|
||
.unwrap_or_default();
|
||
if !old_ids.is_empty() {
|
||
self.crease_map.remove(old_ids, &snapshot);
|
||
}
|
||
|
||
if ranges.is_empty() {
|
||
return;
|
||
}
|
||
|
||
let excerpt_ids = snapshot
|
||
.excerpts()
|
||
.filter(|(_, buf, _)| buf.remote_id() == buffer_id)
|
||
.map(|(id, _, _)| id)
|
||
.collect::<Vec<_>>();
|
||
|
||
let base_placeholder = self.fold_placeholder.clone();
|
||
let creases = ranges.into_iter().filter_map(|folding_range| {
|
||
let mb_range = excerpt_ids.iter().find_map(|&id| {
|
||
snapshot.anchor_range_in_excerpt(id, folding_range.range.clone())
|
||
})?;
|
||
let placeholder = if let Some(collapsed_text) = folding_range.collapsed_text {
|
||
FoldPlaceholder {
|
||
render: Arc::new({
|
||
let collapsed_text = collapsed_text.clone();
|
||
move |fold_id, _fold_range, cx: &mut gpui::App| {
|
||
use gpui::{Element as _, ParentElement as _};
|
||
FoldPlaceholder::fold_element(fold_id, cx)
|
||
.child(collapsed_text.clone())
|
||
.into_any()
|
||
}
|
||
}),
|
||
constrain_width: false,
|
||
merge_adjacent: base_placeholder.merge_adjacent,
|
||
type_tag: base_placeholder.type_tag,
|
||
collapsed_text: Some(collapsed_text),
|
||
}
|
||
} else {
|
||
base_placeholder.clone()
|
||
};
|
||
Some(Crease::simple(mb_range, placeholder))
|
||
});
|
||
|
||
let new_ids = self.crease_map.insert(creases, &snapshot);
|
||
if !new_ids.is_empty() {
|
||
self.lsp_folding_crease_ids.insert(buffer_id, new_ids);
|
||
}
|
||
}
|
||
|
||
/// Removes all LSP folding-range creases for a single buffer.
|
||
pub(super) fn clear_lsp_folding_ranges(&mut self, buffer_id: BufferId, cx: &mut Context<Self>) {
|
||
if let Some(old_ids) = self.lsp_folding_crease_ids.remove(&buffer_id) {
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
self.crease_map.remove(old_ids, &snapshot);
|
||
}
|
||
}
|
||
|
||
/// Returns `true` when at least one buffer has LSP folding-range creases.
|
||
pub(super) fn has_lsp_folding_ranges(&self) -> bool {
|
||
!self.lsp_folding_crease_ids.is_empty()
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn insert_blocks(
|
||
&mut self,
|
||
blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
|
||
cx: &mut Context<Self>,
|
||
) -> Vec<CustomBlockId> {
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
Self::with_synced_companion_mut(
|
||
self.entity_id,
|
||
&self.companion,
|
||
cx,
|
||
|companion_view, _cx| {
|
||
self.block_map
|
||
.write(
|
||
self_wrap_snapshot.clone(),
|
||
self_wrap_edits.clone(),
|
||
companion_view,
|
||
)
|
||
.insert(blocks)
|
||
},
|
||
)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn resize_blocks(&mut self, heights: HashMap<CustomBlockId, u32>, cx: &mut Context<Self>) {
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
|
||
Self::with_synced_companion_mut(
|
||
self.entity_id,
|
||
&self.companion,
|
||
cx,
|
||
|companion_view, _cx| {
|
||
self.block_map
|
||
.write(
|
||
self_wrap_snapshot.clone(),
|
||
self_wrap_edits.clone(),
|
||
companion_view,
|
||
)
|
||
.resize(heights);
|
||
},
|
||
)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn replace_blocks(&mut self, renderers: HashMap<CustomBlockId, RenderBlock>) {
|
||
self.block_map.replace_blocks(renderers);
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn remove_blocks(&mut self, ids: HashSet<CustomBlockId>, cx: &mut Context<Self>) {
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
|
||
Self::with_synced_companion_mut(
|
||
self.entity_id,
|
||
&self.companion,
|
||
cx,
|
||
|companion_view, _cx| {
|
||
self.block_map
|
||
.write(
|
||
self_wrap_snapshot.clone(),
|
||
self_wrap_edits.clone(),
|
||
companion_view,
|
||
)
|
||
.remove(ids);
|
||
},
|
||
)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn row_for_block(
|
||
&mut self,
|
||
block_id: CustomBlockId,
|
||
cx: &mut Context<Self>,
|
||
) -> Option<DisplayRow> {
|
||
let (self_wrap_snapshot, self_wrap_edits) = self.sync_through_wrap(cx);
|
||
|
||
let companion_wrap_data = self.companion.as_ref().and_then(|(companion_dm, _)| {
|
||
companion_dm
|
||
.update(cx, |dm, cx| dm.sync_through_wrap(cx))
|
||
.ok()
|
||
});
|
||
|
||
let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
|
||
let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
|
||
|((snapshot, edits), companion)| {
|
||
CompanionView::new(self.entity_id, snapshot, edits, companion)
|
||
},
|
||
);
|
||
|
||
let block_map = self.block_map.read(
|
||
self_wrap_snapshot.clone(),
|
||
self_wrap_edits.clone(),
|
||
companion_view,
|
||
);
|
||
let block_row = block_map.row_for_block(block_id)?;
|
||
|
||
if let Some((companion_dm, _)) = &self.companion {
|
||
let _ = companion_dm.update(cx, |dm, cx| {
|
||
if let Some((companion_snapshot, companion_edits)) = companion_wrap_data {
|
||
let their_companion_ref = dm.companion.as_ref().map(|(_, c)| c.read(cx));
|
||
dm.block_map.read(
|
||
companion_snapshot,
|
||
companion_edits,
|
||
their_companion_ref.map(|c| {
|
||
CompanionView::new(
|
||
dm.entity_id,
|
||
&self_wrap_snapshot,
|
||
&self_wrap_edits,
|
||
c,
|
||
)
|
||
}),
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
Some(DisplayRow(block_row.0))
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn highlight_text(
|
||
&mut self,
|
||
key: HighlightKey,
|
||
ranges: Vec<Range<Anchor>>,
|
||
style: HighlightStyle,
|
||
merge: bool,
|
||
cx: &App,
|
||
) {
|
||
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);
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub(crate) fn highlight_inlays(
|
||
&mut self,
|
||
key: HighlightKey,
|
||
highlights: Vec<InlayHighlight>,
|
||
style: HighlightStyle,
|
||
) {
|
||
for highlight in highlights {
|
||
let update = self.inlay_highlights.update(&key, |highlights| {
|
||
highlights.insert(highlight.inlay, (style, highlight.clone()))
|
||
});
|
||
if update.is_none() {
|
||
self.inlay_highlights.insert(
|
||
key,
|
||
TreeMap::from_ordered_entries([(highlight.inlay, (style, highlight))]),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn text_highlights(&self, key: HighlightKey) -> Option<(HighlightStyle, &[Range<Anchor>])> {
|
||
let highlights = self.text_highlights.get(&key)?;
|
||
Some((highlights.0, &highlights.1))
|
||
}
|
||
|
||
pub fn all_text_highlights(
|
||
&self,
|
||
) -> impl Iterator<Item = (&HighlightKey, &Arc<(HighlightStyle, Vec<Range<Anchor>>)>)> {
|
||
self.text_highlights.iter()
|
||
}
|
||
|
||
pub fn all_semantic_token_highlights(
|
||
&self,
|
||
) -> impl Iterator<
|
||
Item = (
|
||
&BufferId,
|
||
&(Arc<[SemanticTokenHighlight]>, Arc<HighlightStyleInterner>),
|
||
),
|
||
> {
|
||
self.semantic_token_highlights.iter()
|
||
}
|
||
|
||
pub fn clear_highlights(&mut self, key: HighlightKey) -> bool {
|
||
let mut cleared = self.text_highlights.remove(&key).is_some();
|
||
cleared |= self.inlay_highlights.remove(&key).is_some();
|
||
cleared
|
||
}
|
||
|
||
pub fn clear_highlights_with(&mut self, mut f: impl FnMut(&HighlightKey) -> bool) -> bool {
|
||
let mut cleared = false;
|
||
self.text_highlights.retain(|k, _| {
|
||
let b = !f(k);
|
||
cleared |= b;
|
||
b
|
||
});
|
||
self.inlay_highlights.retain(|k, _| {
|
||
let b = !f(k);
|
||
cleared |= b;
|
||
b
|
||
});
|
||
cleared
|
||
}
|
||
|
||
pub fn set_font(&self, font: Font, font_size: Pixels, cx: &mut Context<Self>) -> bool {
|
||
self.wrap_map
|
||
.update(cx, |map, cx| map.set_font_with_size(font, font_size, cx))
|
||
}
|
||
|
||
pub fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut Context<Self>) -> bool {
|
||
self.wrap_map
|
||
.update(cx, |map, cx| map.set_wrap_width(width, cx))
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn update_fold_widths(
|
||
&mut self,
|
||
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
|
||
cx: &mut Context<Self>,
|
||
) -> bool {
|
||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||
let edits = self.buffer_subscription.consume().into_inner();
|
||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||
|
||
let (snapshot, edits) = self.inlay_map.sync(snapshot, edits);
|
||
let (mut fold_map, snapshot, edits) = self.fold_map.write(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (snapshot, edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
self.block_map.read(snapshot, edits, None);
|
||
|
||
let (snapshot, edits) = fold_map.update_fold_widths(widths);
|
||
let widths_changed = !edits.is_empty();
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (self_new_wrap_snapshot, self_new_wrap_edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
|
||
self.block_map
|
||
.read(self_new_wrap_snapshot, self_new_wrap_edits, None);
|
||
|
||
widths_changed
|
||
}
|
||
|
||
pub(crate) fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
|
||
self.inlay_map.current_inlays()
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub(crate) fn splice_inlays(
|
||
&mut self,
|
||
to_remove: &[InlayId],
|
||
to_insert: Vec<Inlay>,
|
||
cx: &mut Context<Self>,
|
||
) {
|
||
if to_remove.is_empty() && to_insert.is_empty() {
|
||
return;
|
||
}
|
||
let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
|
||
let edits = self.buffer_subscription.consume().into_inner();
|
||
let tab_size = Self::tab_size(&self.buffer, cx);
|
||
|
||
let companion_wrap_data = self.companion.as_ref().and_then(|(companion_dm, _)| {
|
||
companion_dm
|
||
.update(cx, |dm, cx| dm.sync_through_wrap(cx))
|
||
.ok()
|
||
});
|
||
|
||
let (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
|
||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (snapshot, edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
|
||
{
|
||
let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
|
||
let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
|
||
|((snapshot, edits), companion)| {
|
||
CompanionView::new(self.entity_id, snapshot, edits, companion)
|
||
},
|
||
);
|
||
self.block_map.read(snapshot, edits, companion_view);
|
||
}
|
||
|
||
let (snapshot, edits) = self.inlay_map.splice(to_remove, to_insert);
|
||
let (snapshot, edits) = self.fold_map.read(snapshot, edits);
|
||
let (snapshot, edits) = self.tab_map.sync(snapshot, edits, tab_size);
|
||
let (self_new_wrap_snapshot, self_new_wrap_edits) = self
|
||
.wrap_map
|
||
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
|
||
|
||
let (self_wrap_snapshot, self_wrap_edits) =
|
||
(self_new_wrap_snapshot.clone(), self_new_wrap_edits.clone());
|
||
|
||
{
|
||
let companion_ref = self.companion.as_ref().map(|(_, c)| c.read(cx));
|
||
let companion_view = companion_wrap_data.as_ref().zip(companion_ref).map(
|
||
|((snapshot, edits), companion)| {
|
||
CompanionView::new(self.entity_id, snapshot, edits, companion)
|
||
},
|
||
);
|
||
self.block_map
|
||
.read(self_new_wrap_snapshot, self_new_wrap_edits, companion_view);
|
||
}
|
||
|
||
if let Some((companion_dm, _)) = &self.companion {
|
||
let _ = companion_dm.update(cx, |dm, cx| {
|
||
if let Some((companion_snapshot, companion_edits)) = companion_wrap_data {
|
||
let their_companion_ref = dm.companion.as_ref().map(|(_, c)| c.read(cx));
|
||
dm.block_map.read(
|
||
companion_snapshot,
|
||
companion_edits,
|
||
their_companion_ref.map(|c| {
|
||
CompanionView::new(
|
||
dm.entity_id,
|
||
&self_wrap_snapshot,
|
||
&self_wrap_edits,
|
||
c,
|
||
)
|
||
}),
|
||
);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
fn tab_size(buffer: &Entity<MultiBuffer>, cx: &App) -> NonZeroU32 {
|
||
let buffer = buffer.read(cx).as_singleton().map(|buffer| buffer.read(cx));
|
||
let language = buffer
|
||
.and_then(|buffer| buffer.language())
|
||
.map(|l| l.name());
|
||
let file = buffer.and_then(|buffer| buffer.file());
|
||
language_settings(language, file, cx).tab_size
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub fn is_rewrapping(&self, cx: &gpui::App) -> bool {
|
||
self.wrap_map.read(cx).is_rewrapping()
|
||
}
|
||
|
||
pub fn invalidate_semantic_highlights(&mut self, buffer_id: BufferId) {
|
||
self.semantic_token_highlights.remove(&buffer_id);
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Default)]
|
||
pub(crate) struct Highlights<'a> {
|
||
pub text_highlights: Option<&'a TextHighlights>,
|
||
pub inlay_highlights: Option<&'a InlayHighlights>,
|
||
pub semantic_token_highlights: Option<&'a SemanticTokensHighlights>,
|
||
pub styles: HighlightStyles,
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct EditPredictionStyles {
|
||
pub insertion: HighlightStyle,
|
||
pub whitespace: HighlightStyle,
|
||
}
|
||
|
||
#[derive(Default, Debug, Clone, Copy)]
|
||
pub struct HighlightStyles {
|
||
pub inlay_hint: Option<HighlightStyle>,
|
||
pub edit_prediction: Option<EditPredictionStyles>,
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub enum ChunkReplacement {
|
||
Renderer(ChunkRenderer),
|
||
Str(SharedString),
|
||
}
|
||
|
||
pub struct HighlightedChunk<'a> {
|
||
pub text: &'a str,
|
||
pub style: Option<HighlightStyle>,
|
||
pub is_tab: bool,
|
||
pub is_inlay: bool,
|
||
pub replacement: Option<ChunkReplacement>,
|
||
}
|
||
|
||
impl<'a> HighlightedChunk<'a> {
|
||
#[instrument(skip_all)]
|
||
fn highlight_invisibles(
|
||
self,
|
||
editor_style: &'a EditorStyle,
|
||
) -> impl Iterator<Item = Self> + 'a {
|
||
let mut chars = self.text.chars().peekable();
|
||
let mut text = self.text;
|
||
let style = self.style;
|
||
let is_tab = self.is_tab;
|
||
let renderer = self.replacement;
|
||
let is_inlay = self.is_inlay;
|
||
iter::from_fn(move || {
|
||
let mut prefix_len = 0;
|
||
while let Some(&ch) = chars.peek() {
|
||
if !is_invisible(ch) {
|
||
prefix_len += ch.len_utf8();
|
||
chars.next();
|
||
continue;
|
||
}
|
||
if prefix_len > 0 {
|
||
let (prefix, suffix) = text.split_at(prefix_len);
|
||
text = suffix;
|
||
return Some(HighlightedChunk {
|
||
text: prefix,
|
||
style,
|
||
is_tab,
|
||
is_inlay,
|
||
replacement: renderer.clone(),
|
||
});
|
||
}
|
||
chars.next();
|
||
let (prefix, suffix) = text.split_at(ch.len_utf8());
|
||
text = suffix;
|
||
if let Some(replacement) = replacement(ch) {
|
||
let invisible_highlight = HighlightStyle {
|
||
background_color: Some(editor_style.status.hint_background),
|
||
underline: Some(UnderlineStyle {
|
||
color: Some(editor_style.status.hint),
|
||
thickness: px(1.),
|
||
wavy: false,
|
||
}),
|
||
..Default::default()
|
||
};
|
||
let invisible_style = if let Some(style) = style {
|
||
style.highlight(invisible_highlight)
|
||
} else {
|
||
invisible_highlight
|
||
};
|
||
return Some(HighlightedChunk {
|
||
text: prefix,
|
||
style: Some(invisible_style),
|
||
is_tab: false,
|
||
is_inlay,
|
||
replacement: Some(ChunkReplacement::Str(replacement.into())),
|
||
});
|
||
} else {
|
||
let invisible_highlight = HighlightStyle {
|
||
background_color: Some(editor_style.status.hint_background),
|
||
underline: Some(UnderlineStyle {
|
||
color: Some(editor_style.status.hint),
|
||
thickness: px(1.),
|
||
wavy: false,
|
||
}),
|
||
..Default::default()
|
||
};
|
||
let invisible_style = if let Some(style) = style {
|
||
style.highlight(invisible_highlight)
|
||
} else {
|
||
invisible_highlight
|
||
};
|
||
|
||
return Some(HighlightedChunk {
|
||
text: prefix,
|
||
style: Some(invisible_style),
|
||
is_tab: false,
|
||
is_inlay,
|
||
replacement: renderer.clone(),
|
||
});
|
||
}
|
||
}
|
||
|
||
if !text.is_empty() {
|
||
let remainder = text;
|
||
text = "";
|
||
Some(HighlightedChunk {
|
||
text: remainder,
|
||
style,
|
||
is_tab,
|
||
is_inlay,
|
||
replacement: renderer.clone(),
|
||
})
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
#[derive(Clone)]
|
||
pub struct DisplaySnapshot {
|
||
pub display_map_id: EntityId,
|
||
pub companion_display_snapshot: Option<Arc<DisplaySnapshot>>,
|
||
pub crease_snapshot: CreaseSnapshot,
|
||
block_snapshot: BlockSnapshot,
|
||
text_highlights: TextHighlights,
|
||
inlay_highlights: InlayHighlights,
|
||
semantic_token_highlights: SemanticTokensHighlights,
|
||
clip_at_line_ends: bool,
|
||
masked: bool,
|
||
diagnostics_max_severity: DiagnosticSeverity,
|
||
pub(crate) fold_placeholder: FoldPlaceholder,
|
||
/// When true, LSP folding ranges are used via the crease map and the
|
||
/// indent-based fallback in `crease_for_buffer_row` is skipped.
|
||
pub(crate) use_lsp_folding_ranges: bool,
|
||
}
|
||
|
||
impl DisplaySnapshot {
|
||
pub fn companion_snapshot(&self) -> Option<&DisplaySnapshot> {
|
||
self.companion_display_snapshot.as_deref()
|
||
}
|
||
|
||
pub fn wrap_snapshot(&self) -> &WrapSnapshot {
|
||
&self.block_snapshot.wrap_snapshot
|
||
}
|
||
pub fn tab_snapshot(&self) -> &TabSnapshot {
|
||
&self.block_snapshot.wrap_snapshot.tab_snapshot
|
||
}
|
||
|
||
pub fn fold_snapshot(&self) -> &FoldSnapshot {
|
||
&self.block_snapshot.wrap_snapshot.tab_snapshot.fold_snapshot
|
||
}
|
||
|
||
pub fn inlay_snapshot(&self) -> &InlaySnapshot {
|
||
&self
|
||
.block_snapshot
|
||
.wrap_snapshot
|
||
.tab_snapshot
|
||
.fold_snapshot
|
||
.inlay_snapshot
|
||
}
|
||
|
||
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
|
||
&self
|
||
.block_snapshot
|
||
.wrap_snapshot
|
||
.tab_snapshot
|
||
.fold_snapshot
|
||
.inlay_snapshot
|
||
.buffer
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub fn fold_count(&self) -> usize {
|
||
self.fold_snapshot().fold_count()
|
||
}
|
||
|
||
pub fn is_empty(&self) -> bool {
|
||
self.buffer_snapshot().len() == MultiBufferOffset(0)
|
||
}
|
||
|
||
/// Returns whether tree-sitter syntax highlighting should be used.
|
||
/// Returns `false` if any buffer with semantic token highlights has the "full" mode setting,
|
||
/// meaning LSP semantic tokens should replace tree-sitter highlighting.
|
||
pub fn use_tree_sitter_for_syntax(&self, position: DisplayRow, cx: &App) -> bool {
|
||
let position = DisplayPoint::new(position, 0);
|
||
let Some((buffer_snapshot, ..)) = self.point_to_buffer_point(position.to_point(self))
|
||
else {
|
||
return false;
|
||
};
|
||
let settings = language_settings(
|
||
buffer_snapshot.language().map(|l| l.name()),
|
||
buffer_snapshot.file(),
|
||
cx,
|
||
);
|
||
settings.semantic_tokens.use_tree_sitter()
|
||
}
|
||
|
||
pub fn row_infos(&self, start_row: DisplayRow) -> impl Iterator<Item = RowInfo> + '_ {
|
||
self.block_snapshot.row_infos(BlockRow(start_row.0))
|
||
}
|
||
|
||
pub fn widest_line_number(&self) -> u32 {
|
||
self.buffer_snapshot().widest_line_number()
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn prev_line_boundary(&self, mut point: MultiBufferPoint) -> (Point, DisplayPoint) {
|
||
loop {
|
||
let mut inlay_point = self.inlay_snapshot().to_inlay_point(point);
|
||
let mut fold_point = self.fold_snapshot().to_fold_point(inlay_point, Bias::Left);
|
||
fold_point.0.column = 0;
|
||
inlay_point = fold_point.to_inlay_point(self.fold_snapshot());
|
||
point = self.inlay_snapshot().to_buffer_point(inlay_point);
|
||
|
||
let mut display_point = self.point_to_display_point(point, Bias::Left);
|
||
*display_point.column_mut() = 0;
|
||
let next_point = self.display_point_to_point(display_point, Bias::Left);
|
||
if next_point == point {
|
||
return (point, display_point);
|
||
}
|
||
point = next_point;
|
||
}
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn next_line_boundary(
|
||
&self,
|
||
mut point: MultiBufferPoint,
|
||
) -> (MultiBufferPoint, DisplayPoint) {
|
||
let original_point = point;
|
||
loop {
|
||
let mut inlay_point = self.inlay_snapshot().to_inlay_point(point);
|
||
let mut fold_point = self.fold_snapshot().to_fold_point(inlay_point, Bias::Right);
|
||
fold_point.0.column = self.fold_snapshot().line_len(fold_point.row());
|
||
inlay_point = fold_point.to_inlay_point(self.fold_snapshot());
|
||
point = self.inlay_snapshot().to_buffer_point(inlay_point);
|
||
|
||
let mut display_point = self.point_to_display_point(point, Bias::Right);
|
||
*display_point.column_mut() = self.line_len(display_point.row());
|
||
let next_point = self.display_point_to_point(display_point, Bias::Right);
|
||
if next_point == point || original_point == point || original_point == next_point {
|
||
return (point, display_point);
|
||
}
|
||
point = next_point;
|
||
}
|
||
}
|
||
|
||
// used by line_mode selections and tries to match vim behavior
|
||
pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
|
||
let new_start = MultiBufferPoint::new(range.start.row, 0);
|
||
let new_end = if range.end.column > 0 {
|
||
MultiBufferPoint::new(
|
||
range.end.row,
|
||
self.buffer_snapshot()
|
||
.line_len(MultiBufferRow(range.end.row)),
|
||
)
|
||
} else {
|
||
range.end
|
||
};
|
||
|
||
new_start..new_end
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn point_to_display_point(&self, point: MultiBufferPoint, bias: Bias) -> DisplayPoint {
|
||
let inlay_point = self.inlay_snapshot().to_inlay_point(point);
|
||
let fold_point = self.fold_snapshot().to_fold_point(inlay_point, bias);
|
||
let tab_point = self.tab_snapshot().fold_point_to_tab_point(fold_point);
|
||
let wrap_point = self.wrap_snapshot().tab_point_to_wrap_point(tab_point);
|
||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||
DisplayPoint(block_point)
|
||
}
|
||
|
||
/// Converts a buffer offset range into one or more `DisplayPoint` ranges
|
||
/// that cover only actual buffer text, excluding any inlay hint text that
|
||
/// falls within the range.
|
||
pub fn isomorphic_display_point_ranges_for_buffer_range(
|
||
&self,
|
||
range: Range<MultiBufferOffset>,
|
||
) -> SmallVec<[Range<DisplayPoint>; 1]> {
|
||
let inlay_snapshot = self.inlay_snapshot();
|
||
inlay_snapshot
|
||
.buffer_offset_to_inlay_ranges(range)
|
||
.map(|inlay_range| {
|
||
let inlay_point_to_display_point = |inlay_point: InlayPoint, bias: Bias| {
|
||
let fold_point = self.fold_snapshot().to_fold_point(inlay_point, bias);
|
||
let tab_point = self.tab_snapshot().fold_point_to_tab_point(fold_point);
|
||
let wrap_point = self.wrap_snapshot().tab_point_to_wrap_point(tab_point);
|
||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||
DisplayPoint(block_point)
|
||
};
|
||
|
||
let start = inlay_point_to_display_point(
|
||
inlay_snapshot.to_point(inlay_range.start),
|
||
Bias::Left,
|
||
);
|
||
let end = inlay_point_to_display_point(
|
||
inlay_snapshot.to_point(inlay_range.end),
|
||
Bias::Left,
|
||
);
|
||
start..end
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
pub fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
|
||
self.inlay_snapshot()
|
||
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
|
||
}
|
||
|
||
pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset {
|
||
self.inlay_snapshot()
|
||
.to_offset(self.display_point_to_inlay_point(point, bias))
|
||
}
|
||
|
||
pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
|
||
self.inlay_snapshot()
|
||
.to_inlay_offset(anchor.to_offset(self.buffer_snapshot()))
|
||
}
|
||
|
||
pub fn display_point_to_anchor(&self, point: DisplayPoint, bias: Bias) -> Anchor {
|
||
self.buffer_snapshot()
|
||
.anchor_at(point.to_offset(self, bias), bias)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint {
|
||
let block_point = point.0;
|
||
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
|
||
let tab_point = self.wrap_snapshot().to_tab_point(wrap_point);
|
||
let fold_point = self
|
||
.tab_snapshot()
|
||
.tab_point_to_fold_point(tab_point, bias)
|
||
.0;
|
||
fold_point.to_inlay_point(self.fold_snapshot())
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint {
|
||
let block_point = point.0;
|
||
let wrap_point = self.block_snapshot.to_wrap_point(block_point, bias);
|
||
let tab_point = self.wrap_snapshot().to_tab_point(wrap_point);
|
||
self.tab_snapshot()
|
||
.tab_point_to_fold_point(tab_point, bias)
|
||
.0
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
|
||
let tab_point = self.tab_snapshot().fold_point_to_tab_point(fold_point);
|
||
let wrap_point = self.wrap_snapshot().tab_point_to_wrap_point(tab_point);
|
||
let block_point = self.block_snapshot.to_block_point(wrap_point);
|
||
DisplayPoint(block_point)
|
||
}
|
||
|
||
pub fn max_point(&self) -> DisplayPoint {
|
||
DisplayPoint(self.block_snapshot.max_point())
|
||
}
|
||
|
||
/// Returns text chunks starting at the given display row until the end of the file
|
||
#[instrument(skip_all)]
|
||
pub fn text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
|
||
self.block_snapshot
|
||
.chunks(
|
||
BlockRow(display_row.0)..BlockRow(self.max_point().row().next_row().0),
|
||
false,
|
||
self.masked,
|
||
Highlights::default(),
|
||
)
|
||
.map(|h| h.text)
|
||
}
|
||
|
||
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||
#[instrument(skip_all)]
|
||
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
|
||
(0..=display_row.0).rev().flat_map(move |row| {
|
||
self.block_snapshot
|
||
.chunks(
|
||
BlockRow(row)..BlockRow(row + 1),
|
||
false,
|
||
self.masked,
|
||
Highlights::default(),
|
||
)
|
||
.map(|h| h.text)
|
||
.collect::<Vec<_>>()
|
||
.into_iter()
|
||
.rev()
|
||
})
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn chunks(
|
||
&self,
|
||
display_rows: Range<DisplayRow>,
|
||
language_aware: bool,
|
||
highlight_styles: HighlightStyles,
|
||
) -> DisplayChunks<'_> {
|
||
self.block_snapshot.chunks(
|
||
BlockRow(display_rows.start.0)..BlockRow(display_rows.end.0),
|
||
language_aware,
|
||
self.masked,
|
||
Highlights {
|
||
text_highlights: Some(&self.text_highlights),
|
||
inlay_highlights: Some(&self.inlay_highlights),
|
||
semantic_token_highlights: Some(&self.semantic_token_highlights),
|
||
styles: highlight_styles,
|
||
},
|
||
)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn highlighted_chunks<'a>(
|
||
&'a self,
|
||
display_rows: Range<DisplayRow>,
|
||
language_aware: bool,
|
||
editor_style: &'a EditorStyle,
|
||
) -> impl Iterator<Item = HighlightedChunk<'a>> {
|
||
self.chunks(
|
||
display_rows,
|
||
language_aware,
|
||
HighlightStyles {
|
||
inlay_hint: Some(editor_style.inlay_hints_style),
|
||
edit_prediction: Some(editor_style.edit_prediction_styles),
|
||
},
|
||
)
|
||
.flat_map(|chunk| {
|
||
let syntax_highlight_style = chunk
|
||
.syntax_highlight_id
|
||
.and_then(|id| id.style(&editor_style.syntax));
|
||
|
||
let chunk_highlight = chunk.highlight_style.map(|chunk_highlight| {
|
||
HighlightStyle {
|
||
// For color inlays, blend the color with the editor background
|
||
// if the color has transparency (alpha < 1.0)
|
||
color: chunk_highlight.color.map(|color| {
|
||
if chunk.is_inlay && !color.is_opaque() {
|
||
editor_style.background.blend(color)
|
||
} else {
|
||
color
|
||
}
|
||
}),
|
||
..chunk_highlight
|
||
}
|
||
});
|
||
|
||
let diagnostic_highlight = chunk
|
||
.diagnostic_severity
|
||
.filter(|severity| {
|
||
self.diagnostics_max_severity
|
||
.into_lsp()
|
||
.is_some_and(|max_severity| severity <= &max_severity)
|
||
})
|
||
.map(|severity| HighlightStyle {
|
||
fade_out: chunk
|
||
.is_unnecessary
|
||
.then_some(editor_style.unnecessary_code_fade),
|
||
underline: (chunk.underline
|
||
&& editor_style.show_underlines
|
||
&& !(chunk.is_unnecessary && severity > lsp::DiagnosticSeverity::WARNING))
|
||
.then(|| {
|
||
let diagnostic_color =
|
||
super::diagnostic_style(severity, &editor_style.status);
|
||
UnderlineStyle {
|
||
color: Some(diagnostic_color),
|
||
thickness: 1.0.into(),
|
||
wavy: true,
|
||
}
|
||
}),
|
||
..Default::default()
|
||
});
|
||
|
||
let style = [
|
||
syntax_highlight_style,
|
||
chunk_highlight,
|
||
diagnostic_highlight,
|
||
]
|
||
.into_iter()
|
||
.flatten()
|
||
.reduce(|acc, highlight| acc.highlight(highlight));
|
||
|
||
HighlightedChunk {
|
||
text: chunk.text,
|
||
style,
|
||
is_tab: chunk.is_tab,
|
||
is_inlay: chunk.is_inlay,
|
||
replacement: chunk.renderer.map(ChunkReplacement::Renderer),
|
||
}
|
||
.highlight_invisibles(editor_style)
|
||
})
|
||
}
|
||
|
||
/// Returns combined highlight styles (tree-sitter syntax + semantic tokens)
|
||
/// for a byte range within the specified buffer.
|
||
/// Returned ranges are 0-based relative to `buffer_range.start`.
|
||
pub(super) fn combined_highlights(
|
||
&self,
|
||
buffer_id: BufferId,
|
||
buffer_range: Range<usize>,
|
||
syntax_theme: &theme::SyntaxTheme,
|
||
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||
let multibuffer = self.buffer_snapshot();
|
||
|
||
let multibuffer_range = multibuffer
|
||
.excerpts()
|
||
.find_map(|(excerpt_id, buffer, range)| {
|
||
if buffer.remote_id() != buffer_id {
|
||
return None;
|
||
}
|
||
let context_start = range.context.start.to_offset(buffer);
|
||
let context_end = range.context.end.to_offset(buffer);
|
||
if buffer_range.start < context_start || buffer_range.end > context_end {
|
||
return None;
|
||
}
|
||
let start_anchor = buffer.anchor_before(buffer_range.start);
|
||
let end_anchor = buffer.anchor_after(buffer_range.end);
|
||
let mb_range =
|
||
multibuffer.anchor_range_in_excerpt(excerpt_id, start_anchor..end_anchor)?;
|
||
Some(mb_range.start.to_offset(multibuffer)..mb_range.end.to_offset(multibuffer))
|
||
});
|
||
|
||
let Some(multibuffer_range) = multibuffer_range else {
|
||
// Range is outside all excerpts (e.g. symbol name not in a
|
||
// multi-buffer excerpt). Fall back to buffer-level syntax highlights.
|
||
let buffer_snapshot = multibuffer.excerpts().find_map(|(_, buffer, _)| {
|
||
(buffer.remote_id() == buffer_id).then(|| buffer.clone())
|
||
});
|
||
let Some(buffer_snapshot) = buffer_snapshot else {
|
||
return Vec::new();
|
||
};
|
||
let mut highlights = Vec::new();
|
||
let mut offset = 0usize;
|
||
for chunk in buffer_snapshot.chunks(buffer_range, true) {
|
||
let chunk_len = chunk.text.len();
|
||
if chunk_len == 0 {
|
||
continue;
|
||
}
|
||
if let Some(style) = chunk
|
||
.syntax_highlight_id
|
||
.and_then(|id| id.style(syntax_theme))
|
||
{
|
||
highlights.push((offset..offset + chunk_len, style));
|
||
}
|
||
offset += chunk_len;
|
||
}
|
||
return highlights;
|
||
};
|
||
|
||
let chunks = custom_highlights::CustomHighlightsChunks::new(
|
||
multibuffer_range,
|
||
true,
|
||
None,
|
||
Some(&self.semantic_token_highlights),
|
||
multibuffer,
|
||
);
|
||
|
||
let mut highlights = Vec::new();
|
||
let mut offset = 0usize;
|
||
for chunk in chunks {
|
||
let chunk_len = chunk.text.len();
|
||
if chunk_len == 0 {
|
||
continue;
|
||
}
|
||
|
||
let syntax_style = chunk
|
||
.syntax_highlight_id
|
||
.and_then(|id| id.style(syntax_theme));
|
||
let overlay_style = chunk.highlight_style;
|
||
|
||
let combined = match (syntax_style, overlay_style) {
|
||
(Some(syntax), Some(overlay)) => Some(syntax.highlight(overlay)),
|
||
(some @ Some(_), None) | (None, some @ Some(_)) => some,
|
||
(None, None) => None,
|
||
};
|
||
|
||
if let Some(style) = combined {
|
||
highlights.push((offset..offset + chunk_len, style));
|
||
}
|
||
offset += chunk_len;
|
||
}
|
||
highlights
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn layout_row(
|
||
&self,
|
||
display_row: DisplayRow,
|
||
TextLayoutDetails {
|
||
text_system,
|
||
editor_style,
|
||
rem_size,
|
||
scroll_anchor: _,
|
||
visible_rows: _,
|
||
vertical_scroll_margin: _,
|
||
}: &TextLayoutDetails,
|
||
) -> Arc<LineLayout> {
|
||
let mut runs = Vec::new();
|
||
let mut line = String::new();
|
||
|
||
let range = display_row..display_row.next_row();
|
||
for chunk in self.highlighted_chunks(range, false, editor_style) {
|
||
line.push_str(chunk.text);
|
||
|
||
let text_style = if let Some(style) = chunk.style {
|
||
Cow::Owned(editor_style.text.clone().highlight(style))
|
||
} else {
|
||
Cow::Borrowed(&editor_style.text)
|
||
};
|
||
|
||
runs.push(text_style.to_run(chunk.text.len()))
|
||
}
|
||
|
||
if line.ends_with('\n') {
|
||
line.pop();
|
||
if let Some(last_run) = runs.last_mut() {
|
||
last_run.len -= 1;
|
||
if last_run.len == 0 {
|
||
runs.pop();
|
||
}
|
||
}
|
||
}
|
||
|
||
let font_size = editor_style.text.font_size.to_pixels(*rem_size);
|
||
text_system.layout_line(&line, font_size, &runs, None)
|
||
}
|
||
|
||
pub fn x_for_display_point(
|
||
&self,
|
||
display_point: DisplayPoint,
|
||
text_layout_details: &TextLayoutDetails,
|
||
) -> Pixels {
|
||
let line = self.layout_row(display_point.row(), text_layout_details);
|
||
line.x_for_index(display_point.column() as usize)
|
||
}
|
||
|
||
pub fn display_column_for_x(
|
||
&self,
|
||
display_row: DisplayRow,
|
||
x: Pixels,
|
||
details: &TextLayoutDetails,
|
||
) -> u32 {
|
||
let layout_line = self.layout_row(display_row, details);
|
||
layout_line.closest_index_for_x(x) as u32
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn grapheme_at(&self, mut point: DisplayPoint) -> Option<SharedString> {
|
||
point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
|
||
let chars = self
|
||
.text_chunks(point.row())
|
||
.flat_map(str::chars)
|
||
.skip_while({
|
||
let mut column = 0;
|
||
move |char| {
|
||
let at_point = column >= point.column();
|
||
column += char.len_utf8() as u32;
|
||
!at_point
|
||
}
|
||
})
|
||
.take_while({
|
||
let mut prev = false;
|
||
move |char| {
|
||
let now = char.is_ascii();
|
||
let end = char.is_ascii() && (char.is_ascii_whitespace() || prev);
|
||
prev = now;
|
||
!end
|
||
}
|
||
});
|
||
chars.collect::<String>().graphemes(true).next().map(|s| {
|
||
if let Some(invisible) = s.chars().next().filter(|&c| is_invisible(c)) {
|
||
replacement(invisible).unwrap_or(s).to_owned().into()
|
||
} else if s == "\n" {
|
||
" ".into()
|
||
} else {
|
||
s.to_owned().into()
|
||
}
|
||
})
|
||
}
|
||
|
||
pub fn buffer_chars_at(
|
||
&self,
|
||
mut offset: MultiBufferOffset,
|
||
) -> impl Iterator<Item = (char, MultiBufferOffset)> + '_ {
|
||
self.buffer_snapshot().chars_at(offset).map(move |ch| {
|
||
let ret = (ch, offset);
|
||
offset += ch.len_utf8();
|
||
ret
|
||
})
|
||
}
|
||
|
||
pub fn reverse_buffer_chars_at(
|
||
&self,
|
||
mut offset: MultiBufferOffset,
|
||
) -> impl Iterator<Item = (char, MultiBufferOffset)> + '_ {
|
||
self.buffer_snapshot()
|
||
.reversed_chars_at(offset)
|
||
.map(move |ch| {
|
||
offset -= ch.len_utf8();
|
||
(ch, offset)
|
||
})
|
||
}
|
||
|
||
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||
let mut clipped = self.block_snapshot.clip_point(point.0, bias);
|
||
if self.clip_at_line_ends {
|
||
clipped = self.clip_at_line_end(DisplayPoint(clipped)).0
|
||
}
|
||
DisplayPoint(clipped)
|
||
}
|
||
|
||
pub fn clip_ignoring_line_ends(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
|
||
DisplayPoint(self.block_snapshot.clip_point(point.0, bias))
|
||
}
|
||
|
||
pub fn clip_at_line_end(&self, display_point: DisplayPoint) -> DisplayPoint {
|
||
let mut point = self.display_point_to_point(display_point, Bias::Left);
|
||
|
||
if point.column != self.buffer_snapshot().line_len(MultiBufferRow(point.row)) {
|
||
return display_point;
|
||
}
|
||
point.column = point.column.saturating_sub(1);
|
||
point = self.buffer_snapshot().clip_point(point, Bias::Left);
|
||
self.point_to_display_point(point, Bias::Left)
|
||
}
|
||
|
||
pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Fold>
|
||
where
|
||
T: ToOffset,
|
||
{
|
||
self.fold_snapshot().folds_in_range(range)
|
||
}
|
||
|
||
pub fn blocks_in_range(
|
||
&self,
|
||
rows: Range<DisplayRow>,
|
||
) -> impl Iterator<Item = (DisplayRow, &Block)> {
|
||
self.block_snapshot
|
||
.blocks_in_range(BlockRow(rows.start.0)..BlockRow(rows.end.0))
|
||
.map(|(row, block)| (DisplayRow(row.0), block))
|
||
}
|
||
|
||
pub fn sticky_header_excerpt(&self, row: f64) -> Option<StickyHeaderExcerpt<'_>> {
|
||
self.block_snapshot.sticky_header_excerpt(row)
|
||
}
|
||
|
||
pub fn block_for_id(&self, id: BlockId) -> Option<Block> {
|
||
self.block_snapshot.block_for_id(id)
|
||
}
|
||
|
||
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
|
||
self.fold_snapshot().intersects_fold(offset)
|
||
}
|
||
|
||
pub fn is_line_folded(&self, buffer_row: MultiBufferRow) -> bool {
|
||
self.block_snapshot.is_line_replaced(buffer_row)
|
||
|| self.fold_snapshot().is_line_folded(buffer_row)
|
||
}
|
||
|
||
pub fn is_block_line(&self, display_row: DisplayRow) -> bool {
|
||
self.block_snapshot.is_block_line(BlockRow(display_row.0))
|
||
}
|
||
|
||
pub fn is_folded_buffer_header(&self, display_row: DisplayRow) -> bool {
|
||
self.block_snapshot
|
||
.is_folded_buffer_header(BlockRow(display_row.0))
|
||
}
|
||
|
||
pub fn soft_wrap_indent(&self, display_row: DisplayRow) -> Option<u32> {
|
||
let wrap_row = self
|
||
.block_snapshot
|
||
.to_wrap_point(BlockPoint::new(BlockRow(display_row.0), 0), Bias::Left)
|
||
.row();
|
||
self.wrap_snapshot().soft_wrap_indent(wrap_row)
|
||
}
|
||
|
||
pub fn text(&self) -> String {
|
||
self.text_chunks(DisplayRow(0)).collect()
|
||
}
|
||
|
||
pub fn line(&self, display_row: DisplayRow) -> String {
|
||
let mut result = String::new();
|
||
for chunk in self.text_chunks(display_row) {
|
||
if let Some(ix) = chunk.find('\n') {
|
||
result.push_str(&chunk[0..ix]);
|
||
break;
|
||
} else {
|
||
result.push_str(chunk);
|
||
}
|
||
}
|
||
result
|
||
}
|
||
|
||
pub fn line_indent_for_buffer_row(&self, buffer_row: MultiBufferRow) -> LineIndent {
|
||
self.buffer_snapshot().line_indent_for_row(buffer_row)
|
||
}
|
||
|
||
pub fn line_len(&self, row: DisplayRow) -> u32 {
|
||
self.block_snapshot.line_len(BlockRow(row.0))
|
||
}
|
||
|
||
pub fn longest_row(&self) -> DisplayRow {
|
||
DisplayRow(self.block_snapshot.longest_row().0)
|
||
}
|
||
|
||
pub fn longest_row_in_range(&self, range: Range<DisplayRow>) -> DisplayRow {
|
||
let block_range = BlockRow(range.start.0)..BlockRow(range.end.0);
|
||
let longest_row = self.block_snapshot.longest_row_in_range(block_range);
|
||
DisplayRow(longest_row.0)
|
||
}
|
||
|
||
pub fn starts_indent(&self, buffer_row: MultiBufferRow) -> bool {
|
||
let max_row = self.buffer_snapshot().max_row();
|
||
if buffer_row >= max_row {
|
||
return false;
|
||
}
|
||
|
||
let line_indent = self.line_indent_for_buffer_row(buffer_row);
|
||
if line_indent.is_line_blank() {
|
||
return false;
|
||
}
|
||
|
||
(buffer_row.0 + 1..=max_row.0)
|
||
.find_map(|next_row| {
|
||
let next_line_indent = self.line_indent_for_buffer_row(MultiBufferRow(next_row));
|
||
if next_line_indent.raw_len() > line_indent.raw_len() {
|
||
Some(true)
|
||
} else if !next_line_indent.is_line_blank() {
|
||
Some(false)
|
||
} else {
|
||
None
|
||
}
|
||
})
|
||
.unwrap_or(false)
|
||
}
|
||
|
||
#[instrument(skip_all)]
|
||
pub fn crease_for_buffer_row(&self, buffer_row: MultiBufferRow) -> Option<Crease<Point>> {
|
||
let start =
|
||
MultiBufferPoint::new(buffer_row.0, self.buffer_snapshot().line_len(buffer_row));
|
||
if let Some(crease) = self
|
||
.crease_snapshot
|
||
.query_row(buffer_row, self.buffer_snapshot())
|
||
{
|
||
match crease {
|
||
Crease::Inline {
|
||
range,
|
||
placeholder,
|
||
render_toggle,
|
||
render_trailer,
|
||
metadata,
|
||
} => Some(Crease::Inline {
|
||
range: range.to_point(self.buffer_snapshot()),
|
||
placeholder: placeholder.clone(),
|
||
render_toggle: render_toggle.clone(),
|
||
render_trailer: render_trailer.clone(),
|
||
metadata: metadata.clone(),
|
||
}),
|
||
Crease::Block {
|
||
range,
|
||
block_height,
|
||
block_style,
|
||
render_block,
|
||
block_priority,
|
||
render_toggle,
|
||
} => Some(Crease::Block {
|
||
range: range.to_point(self.buffer_snapshot()),
|
||
block_height: *block_height,
|
||
block_style: *block_style,
|
||
render_block: render_block.clone(),
|
||
block_priority: *block_priority,
|
||
render_toggle: render_toggle.clone(),
|
||
}),
|
||
}
|
||
} else if !self.use_lsp_folding_ranges
|
||
&& self.starts_indent(MultiBufferRow(start.row))
|
||
&& !self.is_line_folded(MultiBufferRow(start.row))
|
||
{
|
||
let start_line_indent = self.line_indent_for_buffer_row(buffer_row);
|
||
let max_point = self.buffer_snapshot().max_point();
|
||
let mut end = None;
|
||
|
||
for row in (buffer_row.0 + 1)..=max_point.row {
|
||
let line_indent = self.line_indent_for_buffer_row(MultiBufferRow(row));
|
||
if !line_indent.is_line_blank()
|
||
&& line_indent.raw_len() <= start_line_indent.raw_len()
|
||
{
|
||
let prev_row = row - 1;
|
||
end = Some(Point::new(
|
||
prev_row,
|
||
self.buffer_snapshot().line_len(MultiBufferRow(prev_row)),
|
||
));
|
||
break;
|
||
}
|
||
}
|
||
|
||
let mut row_before_line_breaks = end.unwrap_or(max_point);
|
||
while row_before_line_breaks.row > start.row
|
||
&& self
|
||
.buffer_snapshot()
|
||
.is_line_blank(MultiBufferRow(row_before_line_breaks.row))
|
||
{
|
||
row_before_line_breaks.row -= 1;
|
||
}
|
||
|
||
row_before_line_breaks = Point::new(
|
||
row_before_line_breaks.row,
|
||
self.buffer_snapshot()
|
||
.line_len(MultiBufferRow(row_before_line_breaks.row)),
|
||
);
|
||
|
||
Some(Crease::Inline {
|
||
range: start..row_before_line_breaks,
|
||
placeholder: self.fold_placeholder.clone(),
|
||
render_toggle: None,
|
||
render_trailer: None,
|
||
metadata: None,
|
||
})
|
||
} else {
|
||
None
|
||
}
|
||
}
|
||
|
||
#[cfg(any(test, feature = "test-support"))]
|
||
#[instrument(skip_all)]
|
||
pub fn text_highlight_ranges(
|
||
&self,
|
||
key: HighlightKey,
|
||
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
|
||
self.text_highlights.get(&key).cloned()
|
||
}
|
||
|
||
#[cfg(any(test, feature = "test-support"))]
|
||
#[instrument(skip_all)]
|
||
pub fn all_text_highlight_ranges(
|
||
&self,
|
||
f: impl Fn(&HighlightKey) -> bool,
|
||
) -> Vec<(gpui::Hsla, Range<Point>)> {
|
||
use itertools::Itertools;
|
||
|
||
self.text_highlights
|
||
.iter()
|
||
.filter(|(key, _)| f(key))
|
||
.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(
|
||
&self,
|
||
key: HighlightKey,
|
||
) -> Option<&TreeMap<InlayId, (HighlightStyle, InlayHighlight)>> {
|
||
self.inlay_highlights.get(&key)
|
||
}
|
||
|
||
pub fn buffer_header_height(&self) -> u32 {
|
||
self.block_snapshot.buffer_header_height
|
||
}
|
||
|
||
pub fn excerpt_header_height(&self) -> u32 {
|
||
self.block_snapshot.excerpt_header_height
|
||
}
|
||
|
||
/// Given a `DisplayPoint`, returns another `DisplayPoint` corresponding to
|
||
/// the start of the buffer row that is a given number of buffer rows away
|
||
/// from the provided point.
|
||
///
|
||
/// This moves by buffer rows instead of display rows, a distinction that is
|
||
/// important when soft wrapping is enabled.
|
||
#[instrument(skip_all)]
|
||
pub fn start_of_relative_buffer_row(&self, point: DisplayPoint, times: isize) -> DisplayPoint {
|
||
let start = self.display_point_to_fold_point(point, Bias::Left);
|
||
let target = start.row() as isize + times;
|
||
let new_row = (target.max(0) as u32).min(self.fold_snapshot().max_point().row());
|
||
|
||
self.clip_point(
|
||
self.fold_point_to_display_point(
|
||
self.fold_snapshot()
|
||
.clip_point(FoldPoint::new(new_row, 0), Bias::Right),
|
||
),
|
||
Bias::Right,
|
||
)
|
||
}
|
||
}
|
||
|
||
impl std::ops::Deref for DisplaySnapshot {
|
||
type Target = BlockSnapshot;
|
||
|
||
fn deref(&self) -> &Self::Target {
|
||
&self.block_snapshot
|
||
}
|
||
}
|
||
|
||
/// A zero-indexed point in a text buffer consisting of a row and column adjusted for inserted blocks.
|
||
#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)]
|
||
pub struct DisplayPoint(BlockPoint);
|
||
|
||
impl Debug for DisplayPoint {
|
||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||
f.write_fmt(format_args!(
|
||
"DisplayPoint({}, {})",
|
||
self.row().0,
|
||
self.column()
|
||
))
|
||
}
|
||
}
|
||
|
||
impl Add for DisplayPoint {
|
||
type Output = Self;
|
||
|
||
fn add(self, other: Self) -> Self::Output {
|
||
DisplayPoint(BlockPoint(self.0.0 + other.0.0))
|
||
}
|
||
}
|
||
|
||
impl Sub for DisplayPoint {
|
||
type Output = Self;
|
||
|
||
fn sub(self, other: Self) -> Self::Output {
|
||
DisplayPoint(BlockPoint(self.0.0 - other.0.0))
|
||
}
|
||
}
|
||
|
||
#[derive(Debug, Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq, Deserialize, Hash)]
|
||
#[serde(transparent)]
|
||
pub struct DisplayRow(pub u32);
|
||
|
||
impl DisplayRow {
|
||
pub(crate) fn as_display_point(&self) -> DisplayPoint {
|
||
DisplayPoint::new(*self, 0)
|
||
}
|
||
}
|
||
|
||
impl Add<DisplayRow> for DisplayRow {
|
||
type Output = Self;
|
||
|
||
fn add(self, other: Self) -> Self::Output {
|
||
DisplayRow(self.0 + other.0)
|
||
}
|
||
}
|
||
|
||
impl Add<u32> for DisplayRow {
|
||
type Output = Self;
|
||
|
||
fn add(self, other: u32) -> Self::Output {
|
||
DisplayRow(self.0 + other)
|
||
}
|
||
}
|
||
|
||
impl Sub<DisplayRow> for DisplayRow {
|
||
type Output = Self;
|
||
|
||
fn sub(self, other: Self) -> Self::Output {
|
||
DisplayRow(self.0 - other.0)
|
||
}
|
||
}
|
||
|
||
impl Sub<u32> for DisplayRow {
|
||
type Output = Self;
|
||
|
||
fn sub(self, other: u32) -> Self::Output {
|
||
DisplayRow(self.0 - other)
|
||
}
|
||
}
|
||
|
||
impl DisplayPoint {
|
||
pub fn new(row: DisplayRow, column: u32) -> Self {
|
||
Self(BlockPoint(Point::new(row.0, column)))
|
||
}
|
||
|
||
pub fn zero() -> Self {
|
||
Self::new(DisplayRow(0), 0)
|
||
}
|
||
|
||
pub fn is_zero(&self) -> bool {
|
||
self.0.is_zero()
|
||
}
|
||
|
||
pub fn row(self) -> DisplayRow {
|
||
DisplayRow(self.0.row)
|
||
}
|
||
|
||
pub fn column(self) -> u32 {
|
||
self.0.column
|
||
}
|
||
|
||
pub fn row_mut(&mut self) -> &mut u32 {
|
||
&mut self.0.row
|
||
}
|
||
|
||
pub fn column_mut(&mut self) -> &mut u32 {
|
||
&mut self.0.column
|
||
}
|
||
|
||
pub fn to_point(self, map: &DisplaySnapshot) -> Point {
|
||
map.display_point_to_point(self, Bias::Left)
|
||
}
|
||
|
||
pub fn to_offset(self, map: &DisplaySnapshot, bias: Bias) -> MultiBufferOffset {
|
||
let wrap_point = map.block_snapshot.to_wrap_point(self.0, bias);
|
||
let tab_point = map.wrap_snapshot().to_tab_point(wrap_point);
|
||
let fold_point = map
|
||
.tab_snapshot()
|
||
.tab_point_to_fold_point(tab_point, bias)
|
||
.0;
|
||
let inlay_point = fold_point.to_inlay_point(map.fold_snapshot());
|
||
map.inlay_snapshot()
|
||
.to_buffer_offset(map.inlay_snapshot().to_offset(inlay_point))
|
||
}
|
||
}
|
||
|
||
impl ToDisplayPoint for MultiBufferOffset {
|
||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
|
||
map.point_to_display_point(self.to_point(map.buffer_snapshot()), Bias::Left)
|
||
}
|
||
}
|
||
|
||
impl ToDisplayPoint for MultiBufferOffsetUtf16 {
|
||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
|
||
self.to_offset(map.buffer_snapshot()).to_display_point(map)
|
||
}
|
||
}
|
||
|
||
impl ToDisplayPoint for Point {
|
||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
|
||
map.point_to_display_point(*self, Bias::Left)
|
||
}
|
||
}
|
||
|
||
impl ToDisplayPoint for Anchor {
|
||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
|
||
self.to_point(map.buffer_snapshot()).to_display_point(map)
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub mod tests {
|
||
use super::*;
|
||
use crate::{
|
||
movement,
|
||
test::{marked_display_snapshot, test_font},
|
||
};
|
||
use Bias::*;
|
||
use block_map::BlockPlacement;
|
||
use gpui::{
|
||
App, AppContext as _, BorrowAppContext, Element, Hsla, Rgba, div, font, observe, px,
|
||
};
|
||
use language::{
|
||
Buffer, Diagnostic, DiagnosticEntry, DiagnosticSet, Language, LanguageConfig,
|
||
LanguageMatcher,
|
||
};
|
||
use lsp::LanguageServerId;
|
||
|
||
use rand::{Rng, prelude::*};
|
||
use settings::{SettingsContent, SettingsStore};
|
||
use smol::stream::StreamExt;
|
||
use std::{env, sync::Arc};
|
||
use text::PointUtf16;
|
||
use theme::{LoadThemes, SyntaxTheme};
|
||
use unindent::Unindent as _;
|
||
use util::test::{marked_text_ranges, sample_text};
|
||
|
||
#[gpui::test(iterations = 100)]
|
||
async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
|
||
cx.background_executor.set_block_on_ticks(0..=50);
|
||
let operations = env::var("OPERATIONS")
|
||
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
|
||
.unwrap_or(10);
|
||
|
||
let mut tab_size = rng.random_range(1..=4);
|
||
let buffer_start_excerpt_header_height = rng.random_range(1..=5);
|
||
let excerpt_header_height = rng.random_range(1..=5);
|
||
let font_size = px(14.0);
|
||
let max_wrap_width = 300.0;
|
||
let mut wrap_width = if rng.random_bool(0.1) {
|
||
None
|
||
} else {
|
||
Some(px(rng.random_range(0.0..=max_wrap_width)))
|
||
};
|
||
|
||
log::info!("tab size: {}", tab_size);
|
||
log::info!("wrap width: {:?}", wrap_width);
|
||
|
||
cx.update(|cx| {
|
||
init_test(cx, |s| {
|
||
s.project.all_languages.defaults.tab_size = NonZeroU32::new(tab_size)
|
||
});
|
||
});
|
||
|
||
let buffer = cx.update(|cx| {
|
||
if rng.random() {
|
||
let len = rng.random_range(0..10);
|
||
let text = util::RandomCharIter::new(&mut rng)
|
||
.take(len)
|
||
.collect::<String>();
|
||
MultiBuffer::build_simple(&text, cx)
|
||
} else {
|
||
MultiBuffer::build_random(&mut rng, cx)
|
||
}
|
||
});
|
||
|
||
let font = test_font();
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font,
|
||
font_size,
|
||
wrap_width,
|
||
buffer_start_excerpt_header_height,
|
||
excerpt_header_height,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
let mut notifications = observe(&map, cx);
|
||
let mut fold_count = 0;
|
||
let mut blocks = Vec::new();
|
||
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot().text());
|
||
log::info!("fold text: {:?}", snapshot.fold_snapshot().text());
|
||
log::info!("tab text: {:?}", snapshot.tab_snapshot().text());
|
||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot().text());
|
||
log::info!("block text: {:?}", snapshot.block_snapshot.text());
|
||
log::info!("display text: {:?}", snapshot.text());
|
||
|
||
for _i in 0..operations {
|
||
match rng.random_range(0..100) {
|
||
0..=19 => {
|
||
wrap_width = if rng.random_bool(0.2) {
|
||
None
|
||
} else {
|
||
Some(px(rng.random_range(0.0..=max_wrap_width)))
|
||
};
|
||
log::info!("setting wrap width to {:?}", wrap_width);
|
||
map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
|
||
}
|
||
20..=29 => {
|
||
let mut tab_sizes = vec![1, 2, 3, 4];
|
||
tab_sizes.remove((tab_size - 1) as usize);
|
||
tab_size = *tab_sizes.choose(&mut rng).unwrap();
|
||
log::info!("setting tab size to {:?}", tab_size);
|
||
cx.update(|cx| {
|
||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||
store.update_user_settings(cx, |s| {
|
||
s.project.all_languages.defaults.tab_size =
|
||
NonZeroU32::new(tab_size);
|
||
});
|
||
});
|
||
});
|
||
}
|
||
30..=44 => {
|
||
map.update(cx, |map, cx| {
|
||
if rng.random() || blocks.is_empty() {
|
||
let snapshot = map.snapshot(cx);
|
||
let buffer = snapshot.buffer_snapshot();
|
||
let block_properties = (0..rng.random_range(1..=1))
|
||
.map(|_| {
|
||
let position = buffer.anchor_after(buffer.clip_offset(
|
||
rng.random_range(MultiBufferOffset(0)..=buffer.len()),
|
||
Bias::Left,
|
||
));
|
||
|
||
let placement = if rng.random() {
|
||
BlockPlacement::Above(position)
|
||
} else {
|
||
BlockPlacement::Below(position)
|
||
};
|
||
let height = rng.random_range(1..5);
|
||
log::info!(
|
||
"inserting block {:?} with height {}",
|
||
placement.as_ref().map(|p| p.to_point(&buffer)),
|
||
height
|
||
);
|
||
let priority = rng.random_range(1..100);
|
||
BlockProperties {
|
||
placement,
|
||
style: BlockStyle::Fixed,
|
||
height: Some(height),
|
||
render: Arc::new(|_| div().into_any()),
|
||
priority,
|
||
}
|
||
})
|
||
.collect::<Vec<_>>();
|
||
blocks.extend(map.insert_blocks(block_properties, cx));
|
||
} else {
|
||
blocks.shuffle(&mut rng);
|
||
let remove_count = rng.random_range(1..=4.min(blocks.len()));
|
||
let block_ids_to_remove = (0..remove_count)
|
||
.map(|_| blocks.remove(rng.random_range(0..blocks.len())))
|
||
.collect();
|
||
log::info!("removing block ids {:?}", block_ids_to_remove);
|
||
map.remove_blocks(block_ids_to_remove, cx);
|
||
}
|
||
});
|
||
}
|
||
45..=79 => {
|
||
let mut ranges = Vec::new();
|
||
for _ in 0..rng.random_range(1..=3) {
|
||
buffer.read_with(cx, |buffer, cx| {
|
||
let buffer = buffer.read(cx);
|
||
let end = buffer.clip_offset(
|
||
rng.random_range(MultiBufferOffset(0)..=buffer.len()),
|
||
Right,
|
||
);
|
||
let start = buffer
|
||
.clip_offset(rng.random_range(MultiBufferOffset(0)..=end), Left);
|
||
ranges.push(start..end);
|
||
});
|
||
}
|
||
|
||
if rng.random() && fold_count > 0 {
|
||
log::info!("unfolding ranges: {:?}", ranges);
|
||
map.update(cx, |map, cx| {
|
||
map.unfold_intersecting(ranges, true, cx);
|
||
});
|
||
} else {
|
||
log::info!("folding ranges: {:?}", ranges);
|
||
map.update(cx, |map, cx| {
|
||
map.fold(
|
||
ranges
|
||
.into_iter()
|
||
.map(|range| Crease::simple(range, FoldPlaceholder::test()))
|
||
.collect(),
|
||
cx,
|
||
);
|
||
});
|
||
}
|
||
}
|
||
_ => {
|
||
buffer.update(cx, |buffer, cx| buffer.randomly_mutate(&mut rng, 5, cx));
|
||
}
|
||
}
|
||
|
||
if map.read_with(cx, |map, cx| map.is_rewrapping(cx)) {
|
||
notifications.next().await.unwrap();
|
||
}
|
||
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
fold_count = snapshot.fold_count();
|
||
log::info!("buffer text: {:?}", snapshot.buffer_snapshot().text());
|
||
log::info!("fold text: {:?}", snapshot.fold_snapshot().text());
|
||
log::info!("tab text: {:?}", snapshot.tab_snapshot().text());
|
||
log::info!("wrap text: {:?}", snapshot.wrap_snapshot().text());
|
||
log::info!("block text: {:?}", snapshot.block_snapshot.text());
|
||
log::info!("display text: {:?}", snapshot.text());
|
||
|
||
// Line boundaries
|
||
let buffer = snapshot.buffer_snapshot();
|
||
for _ in 0..5 {
|
||
let row = rng.random_range(0..=buffer.max_point().row);
|
||
let column = rng.random_range(0..=buffer.line_len(MultiBufferRow(row)));
|
||
let point = buffer.clip_point(Point::new(row, column), Left);
|
||
|
||
let (prev_buffer_bound, prev_display_bound) = snapshot.prev_line_boundary(point);
|
||
let (next_buffer_bound, next_display_bound) = snapshot.next_line_boundary(point);
|
||
|
||
assert!(prev_buffer_bound <= point);
|
||
assert!(next_buffer_bound >= point);
|
||
assert_eq!(prev_buffer_bound.column, 0);
|
||
assert_eq!(prev_display_bound.column(), 0);
|
||
if next_buffer_bound < buffer.max_point() {
|
||
assert_eq!(buffer.chars_at(next_buffer_bound).next(), Some('\n'));
|
||
}
|
||
|
||
assert_eq!(
|
||
prev_display_bound,
|
||
prev_buffer_bound.to_display_point(&snapshot),
|
||
"row boundary before {:?}. reported buffer row boundary: {:?}",
|
||
point,
|
||
prev_buffer_bound
|
||
);
|
||
assert_eq!(
|
||
next_display_bound,
|
||
next_buffer_bound.to_display_point(&snapshot),
|
||
"display row boundary after {:?}. reported buffer row boundary: {:?}",
|
||
point,
|
||
next_buffer_bound
|
||
);
|
||
assert_eq!(
|
||
prev_buffer_bound,
|
||
prev_display_bound.to_point(&snapshot),
|
||
"row boundary before {:?}. reported display row boundary: {:?}",
|
||
point,
|
||
prev_display_bound
|
||
);
|
||
assert_eq!(
|
||
next_buffer_bound,
|
||
next_display_bound.to_point(&snapshot),
|
||
"row boundary after {:?}. reported display row boundary: {:?}",
|
||
point,
|
||
next_display_bound
|
||
);
|
||
}
|
||
|
||
// Movement
|
||
let min_point = snapshot.clip_point(DisplayPoint::new(DisplayRow(0), 0), Left);
|
||
let max_point = snapshot.clip_point(snapshot.max_point(), Right);
|
||
for _ in 0..5 {
|
||
let row = rng.random_range(0..=snapshot.max_point().row().0);
|
||
let column = rng.random_range(0..=snapshot.line_len(DisplayRow(row)));
|
||
let point = snapshot.clip_point(DisplayPoint::new(DisplayRow(row), column), Left);
|
||
|
||
log::info!("Moving from point {:?}", point);
|
||
|
||
let moved_right = movement::right(&snapshot, point);
|
||
log::info!("Right {:?}", moved_right);
|
||
if point < max_point {
|
||
assert!(moved_right > point);
|
||
if point.column() == snapshot.line_len(point.row())
|
||
|| snapshot.soft_wrap_indent(point.row()).is_some()
|
||
&& point.column() == snapshot.line_len(point.row()) - 1
|
||
{
|
||
assert!(moved_right.row() > point.row());
|
||
}
|
||
} else {
|
||
assert_eq!(moved_right, point);
|
||
}
|
||
|
||
let moved_left = movement::left(&snapshot, point);
|
||
log::info!("Left {:?}", moved_left);
|
||
if point > min_point {
|
||
assert!(moved_left < point);
|
||
if point.column() == 0 {
|
||
assert!(moved_left.row() < point.row());
|
||
}
|
||
} else {
|
||
assert_eq!(moved_left, point);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[gpui::test(retries = 5)]
|
||
async fn test_soft_wraps(cx: &mut gpui::TestAppContext) {
|
||
cx.background_executor
|
||
.set_block_on_ticks(usize::MAX..=usize::MAX);
|
||
cx.update(|cx| {
|
||
init_test(cx, |_| {});
|
||
});
|
||
|
||
let mut cx = crate::test::editor_test_context::EditorTestContext::new(cx).await;
|
||
let editor = cx.editor.clone();
|
||
let window = cx.window;
|
||
|
||
_ = cx.update_window(window, |_, window, cx| {
|
||
let text_layout_details =
|
||
editor.update(cx, |editor, cx| editor.text_layout_details(window, cx));
|
||
|
||
let font_size = px(12.0);
|
||
let wrap_width = Some(px(96.));
|
||
|
||
let text = "one two three four five\nsix seven eight";
|
||
let buffer = MultiBuffer::build_simple(text, cx);
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Helvetica"),
|
||
font_size,
|
||
wrap_width,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
assert_eq!(
|
||
snapshot.text_chunks(DisplayRow(0)).collect::<String>(),
|
||
"one two \nthree four \nfive\nsix seven \neight"
|
||
);
|
||
assert_eq!(
|
||
snapshot.clip_point(DisplayPoint::new(DisplayRow(0), 8), Bias::Left),
|
||
DisplayPoint::new(DisplayRow(0), 7)
|
||
);
|
||
assert_eq!(
|
||
snapshot.clip_point(DisplayPoint::new(DisplayRow(0), 8), Bias::Right),
|
||
DisplayPoint::new(DisplayRow(1), 0)
|
||
);
|
||
assert_eq!(
|
||
movement::right(&snapshot, DisplayPoint::new(DisplayRow(0), 7)),
|
||
DisplayPoint::new(DisplayRow(1), 0)
|
||
);
|
||
assert_eq!(
|
||
movement::left(&snapshot, DisplayPoint::new(DisplayRow(1), 0)),
|
||
DisplayPoint::new(DisplayRow(0), 7)
|
||
);
|
||
|
||
let x = snapshot
|
||
.x_for_display_point(DisplayPoint::new(DisplayRow(1), 10), &text_layout_details);
|
||
assert_eq!(
|
||
movement::up(
|
||
&snapshot,
|
||
DisplayPoint::new(DisplayRow(1), 10),
|
||
language::SelectionGoal::None,
|
||
false,
|
||
&text_layout_details,
|
||
),
|
||
(
|
||
DisplayPoint::new(DisplayRow(0), 7),
|
||
language::SelectionGoal::HorizontalPosition(f64::from(x))
|
||
)
|
||
);
|
||
assert_eq!(
|
||
movement::down(
|
||
&snapshot,
|
||
DisplayPoint::new(DisplayRow(0), 7),
|
||
language::SelectionGoal::HorizontalPosition(f64::from(x)),
|
||
false,
|
||
&text_layout_details
|
||
),
|
||
(
|
||
DisplayPoint::new(DisplayRow(1), 10),
|
||
language::SelectionGoal::HorizontalPosition(f64::from(x))
|
||
)
|
||
);
|
||
assert_eq!(
|
||
movement::down(
|
||
&snapshot,
|
||
DisplayPoint::new(DisplayRow(1), 10),
|
||
language::SelectionGoal::HorizontalPosition(f64::from(x)),
|
||
false,
|
||
&text_layout_details
|
||
),
|
||
(
|
||
DisplayPoint::new(DisplayRow(2), 4),
|
||
language::SelectionGoal::HorizontalPosition(f64::from(x))
|
||
)
|
||
);
|
||
|
||
let ix = MultiBufferOffset(snapshot.buffer_snapshot().text().find("seven").unwrap());
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit([(ix..ix, "and ")], None, cx);
|
||
});
|
||
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
assert_eq!(
|
||
snapshot.text_chunks(DisplayRow(1)).collect::<String>(),
|
||
"three four \nfive\nsix and \nseven eight"
|
||
);
|
||
|
||
// Re-wrap on font size changes
|
||
map.update(cx, |map, cx| {
|
||
map.set_font(font("Helvetica"), font_size + Pixels::from(3.), cx)
|
||
});
|
||
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
assert_eq!(
|
||
snapshot.text_chunks(DisplayRow(1)).collect::<String>(),
|
||
"three \nfour five\nsix and \nseven \neight"
|
||
)
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_text_chunks(cx: &mut gpui::App) {
|
||
init_test(cx, |_| {});
|
||
|
||
let text = sample_text(6, 6, 'a');
|
||
let buffer = MultiBuffer::build_simple(&text, cx);
|
||
|
||
let font_size = px(14.0);
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Helvetica"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit(
|
||
vec![
|
||
(
|
||
MultiBufferPoint::new(1, 0)..MultiBufferPoint::new(1, 0),
|
||
"\t",
|
||
),
|
||
(
|
||
MultiBufferPoint::new(1, 1)..MultiBufferPoint::new(1, 1),
|
||
"\t",
|
||
),
|
||
(
|
||
MultiBufferPoint::new(2, 1)..MultiBufferPoint::new(2, 1),
|
||
"\t",
|
||
),
|
||
],
|
||
None,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
assert_eq!(
|
||
map.update(cx, |map, cx| map.snapshot(cx))
|
||
.text_chunks(DisplayRow(1))
|
||
.collect::<String>()
|
||
.lines()
|
||
.next(),
|
||
Some(" b bbbbb")
|
||
);
|
||
assert_eq!(
|
||
map.update(cx, |map, cx| map.snapshot(cx))
|
||
.text_chunks(DisplayRow(2))
|
||
.collect::<String>()
|
||
.lines()
|
||
.next(),
|
||
Some("c ccccc")
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_inlays_with_newlines_after_blocks(cx: &mut gpui::TestAppContext) {
|
||
cx.update(|cx| init_test(cx, |_| {}));
|
||
|
||
let buffer = cx.new(|cx| Buffer::local("a", cx));
|
||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||
|
||
let font_size = px(14.0);
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Helvetica"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
map.update(cx, |map, cx| {
|
||
map.insert_blocks(
|
||
[BlockProperties {
|
||
placement: BlockPlacement::Above(
|
||
buffer_snapshot.anchor_before(Point::new(0, 0)),
|
||
),
|
||
height: Some(2),
|
||
style: BlockStyle::Sticky,
|
||
render: Arc::new(|_| div().into_any()),
|
||
priority: 0,
|
||
}],
|
||
cx,
|
||
);
|
||
});
|
||
map.update(cx, |m, cx| assert_eq!(m.snapshot(cx).text(), "\n\na"));
|
||
|
||
map.update(cx, |map, cx| {
|
||
map.splice_inlays(
|
||
&[],
|
||
vec![Inlay::edit_prediction(
|
||
0,
|
||
buffer_snapshot.anchor_after(MultiBufferOffset(0)),
|
||
"\n",
|
||
)],
|
||
cx,
|
||
);
|
||
});
|
||
map.update(cx, |m, cx| assert_eq!(m.snapshot(cx).text(), "\n\n\na"));
|
||
|
||
// Regression test: updating the display map does not crash when a
|
||
// block is immediately followed by a multi-line inlay.
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.edit(
|
||
[(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
|
||
None,
|
||
cx,
|
||
);
|
||
});
|
||
map.update(cx, |m, cx| assert_eq!(m.snapshot(cx).text(), "\n\n\nab"));
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_chunks(cx: &mut gpui::TestAppContext) {
|
||
let text = r#"
|
||
fn outer() {}
|
||
|
||
mod module {
|
||
fn inner() {}
|
||
}"#
|
||
.unindent();
|
||
|
||
let theme =
|
||
SyntaxTheme::new_test(vec![("mod.body", Hsla::red()), ("fn.name", Hsla::blue())]);
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
name: "Test".into(),
|
||
matcher: LanguageMatcher {
|
||
path_suffixes: vec![".test".to_string()],
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||
)
|
||
.with_highlights_query(
|
||
r#"
|
||
(mod_item name: (identifier) body: _ @mod.body)
|
||
(function_item name: (identifier) @fn.name)
|
||
"#,
|
||
)
|
||
.unwrap(),
|
||
);
|
||
language.set_theme(&theme);
|
||
|
||
cx.update(|cx| {
|
||
init_test(cx, |s| {
|
||
s.project.all_languages.defaults.tab_size = Some(2.try_into().unwrap())
|
||
})
|
||
});
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
|
||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
|
||
let font_size = px(14.0);
|
||
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer,
|
||
font("Helvetica"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
assert_eq!(
|
||
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)),
|
||
vec![
|
||
("fn ".to_string(), None),
|
||
("outer".to_string(), Some(Hsla::blue())),
|
||
("() {}\n\nmod module ".to_string(), None),
|
||
("{\n fn ".to_string(), Some(Hsla::red())),
|
||
("inner".to_string(), Some(Hsla::blue())),
|
||
("() {}\n}".to_string(), Some(Hsla::red())),
|
||
]
|
||
);
|
||
assert_eq!(
|
||
cx.update(|cx| syntax_chunks(DisplayRow(3)..DisplayRow(5), &map, &theme, cx)),
|
||
vec![
|
||
(" fn ".to_string(), Some(Hsla::red())),
|
||
("inner".to_string(), Some(Hsla::blue())),
|
||
("() {}\n}".to_string(), Some(Hsla::red())),
|
||
]
|
||
);
|
||
|
||
map.update(cx, |map, cx| {
|
||
map.fold(
|
||
vec![Crease::simple(
|
||
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
|
||
FoldPlaceholder::test(),
|
||
)],
|
||
cx,
|
||
)
|
||
});
|
||
assert_eq!(
|
||
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(2), &map, &theme, cx)),
|
||
vec![
|
||
("fn ".to_string(), None),
|
||
("out".to_string(), Some(Hsla::blue())),
|
||
("⋯".to_string(), None),
|
||
(" fn ".to_string(), Some(Hsla::red())),
|
||
("inner".to_string(), Some(Hsla::blue())),
|
||
("() {}\n}".to_string(), Some(Hsla::red())),
|
||
]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_chunks_with_syntax_highlighting_across_blocks(cx: &mut gpui::TestAppContext) {
|
||
cx.background_executor
|
||
.set_block_on_ticks(usize::MAX..=usize::MAX);
|
||
|
||
let text = r#"
|
||
const A: &str = "
|
||
one
|
||
two
|
||
three
|
||
";
|
||
const B: &str = "four";
|
||
"#
|
||
.unindent();
|
||
|
||
let theme = SyntaxTheme::new_test(vec![
|
||
("string", Hsla::red()),
|
||
("punctuation", Hsla::blue()),
|
||
("keyword", Hsla::green()),
|
||
]);
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
name: "Rust".into(),
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||
)
|
||
.with_highlights_query(
|
||
r#"
|
||
(string_literal) @string
|
||
"const" @keyword
|
||
[":" ";"] @punctuation
|
||
"#,
|
||
)
|
||
.unwrap(),
|
||
);
|
||
language.set_theme(&theme);
|
||
|
||
cx.update(|cx| init_test(cx, |_| {}));
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
|
||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer,
|
||
font("Courier"),
|
||
px(16.0),
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
// Insert two blocks in the middle of a multi-line string literal.
|
||
// The second block has zero height.
|
||
map.update(cx, |map, cx| {
|
||
map.insert_blocks(
|
||
[
|
||
BlockProperties {
|
||
placement: BlockPlacement::Below(
|
||
buffer_snapshot.anchor_before(Point::new(1, 0)),
|
||
),
|
||
height: Some(1),
|
||
style: BlockStyle::Sticky,
|
||
render: Arc::new(|_| div().into_any()),
|
||
priority: 0,
|
||
},
|
||
BlockProperties {
|
||
placement: BlockPlacement::Below(
|
||
buffer_snapshot.anchor_before(Point::new(2, 0)),
|
||
),
|
||
height: None,
|
||
style: BlockStyle::Sticky,
|
||
render: Arc::new(|_| div().into_any()),
|
||
priority: 0,
|
||
},
|
||
],
|
||
cx,
|
||
)
|
||
});
|
||
|
||
pretty_assertions::assert_eq!(
|
||
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(7), &map, &theme, cx)),
|
||
[
|
||
("const".into(), Some(Hsla::green())),
|
||
(" A".into(), None),
|
||
(":".into(), Some(Hsla::blue())),
|
||
(" &str = ".into(), None),
|
||
("\"\n one\n".into(), Some(Hsla::red())),
|
||
("\n".into(), None),
|
||
(" two\n three\n\"".into(), Some(Hsla::red())),
|
||
(";".into(), Some(Hsla::blue())),
|
||
("\n".into(), None),
|
||
("const".into(), Some(Hsla::green())),
|
||
(" B".into(), None),
|
||
(":".into(), Some(Hsla::blue())),
|
||
(" &str = ".into(), None),
|
||
("\"four\"".into(), Some(Hsla::red())),
|
||
(";".into(), Some(Hsla::blue())),
|
||
("\n".into(), None),
|
||
]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_chunks_with_diagnostics_across_blocks(cx: &mut gpui::TestAppContext) {
|
||
cx.background_executor
|
||
.set_block_on_ticks(usize::MAX..=usize::MAX);
|
||
|
||
let text = r#"
|
||
struct A {
|
||
b: usize;
|
||
}
|
||
const c: usize = 1;
|
||
"#
|
||
.unindent();
|
||
|
||
cx.update(|cx| init_test(cx, |_| {}));
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx));
|
||
|
||
buffer.update(cx, |buffer, cx| {
|
||
buffer.update_diagnostics(
|
||
LanguageServerId(0),
|
||
DiagnosticSet::new(
|
||
[DiagnosticEntry {
|
||
range: PointUtf16::new(0, 0)..PointUtf16::new(2, 1),
|
||
diagnostic: Diagnostic {
|
||
severity: lsp::DiagnosticSeverity::ERROR,
|
||
group_id: 1,
|
||
message: "hi".into(),
|
||
..Default::default()
|
||
},
|
||
}],
|
||
buffer,
|
||
),
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer,
|
||
font("Courier"),
|
||
px(16.0),
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let black = gpui::black().to_rgb();
|
||
let red = gpui::red().to_rgb();
|
||
|
||
// Insert a block in the middle of a multi-line diagnostic.
|
||
map.update(cx, |map, cx| {
|
||
map.highlight_text(
|
||
HighlightKey::Editor,
|
||
vec![
|
||
buffer_snapshot.anchor_before(Point::new(3, 9))
|
||
..buffer_snapshot.anchor_after(Point::new(3, 14)),
|
||
buffer_snapshot.anchor_before(Point::new(3, 17))
|
||
..buffer_snapshot.anchor_after(Point::new(3, 18)),
|
||
],
|
||
red.into(),
|
||
false,
|
||
cx,
|
||
);
|
||
map.insert_blocks(
|
||
[BlockProperties {
|
||
placement: BlockPlacement::Below(
|
||
buffer_snapshot.anchor_before(Point::new(1, 0)),
|
||
),
|
||
height: Some(1),
|
||
style: BlockStyle::Sticky,
|
||
render: Arc::new(|_| div().into_any()),
|
||
priority: 0,
|
||
}],
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
let mut chunks = Vec::<(String, Option<lsp::DiagnosticSeverity>, Rgba)>::new();
|
||
for chunk in snapshot.chunks(DisplayRow(0)..DisplayRow(5), true, Default::default()) {
|
||
let color = chunk
|
||
.highlight_style
|
||
.and_then(|style| style.color)
|
||
.map_or(black, |color| color.to_rgb());
|
||
if let Some((last_chunk, last_severity, last_color)) = chunks.last_mut()
|
||
&& *last_severity == chunk.diagnostic_severity
|
||
&& *last_color == color
|
||
{
|
||
last_chunk.push_str(chunk.text);
|
||
continue;
|
||
}
|
||
|
||
chunks.push((chunk.text.to_string(), chunk.diagnostic_severity, color));
|
||
}
|
||
|
||
assert_eq!(
|
||
chunks,
|
||
[
|
||
(
|
||
"struct A {\n b: usize;\n".into(),
|
||
Some(lsp::DiagnosticSeverity::ERROR),
|
||
black
|
||
),
|
||
("\n".into(), None, black),
|
||
("}".into(), Some(lsp::DiagnosticSeverity::ERROR), black),
|
||
("\nconst c: ".into(), None, black),
|
||
("usize".into(), None, red),
|
||
(" = ".into(), None, black),
|
||
("1".into(), None, red),
|
||
(";\n".into(), None, black),
|
||
]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_point_translation_with_replace_blocks(cx: &mut gpui::TestAppContext) {
|
||
cx.background_executor
|
||
.set_block_on_ticks(usize::MAX..=usize::MAX);
|
||
|
||
cx.update(|cx| init_test(cx, |_| {}));
|
||
|
||
let buffer = cx.update(|cx| MultiBuffer::build_simple("abcde\nfghij\nklmno\npqrst", cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Courier"),
|
||
px(16.0),
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let snapshot = map.update(cx, |map, cx| {
|
||
map.insert_blocks(
|
||
[BlockProperties {
|
||
placement: BlockPlacement::Replace(
|
||
buffer_snapshot.anchor_before(Point::new(1, 2))
|
||
..=buffer_snapshot.anchor_after(Point::new(2, 3)),
|
||
),
|
||
height: Some(4),
|
||
style: BlockStyle::Fixed,
|
||
render: Arc::new(|_| div().into_any()),
|
||
priority: 0,
|
||
}],
|
||
cx,
|
||
);
|
||
map.snapshot(cx)
|
||
});
|
||
|
||
assert_eq!(snapshot.text(), "abcde\n\n\n\n\npqrst");
|
||
|
||
let point_to_display_points = [
|
||
(Point::new(1, 0), DisplayPoint::new(DisplayRow(1), 0)),
|
||
(Point::new(2, 0), DisplayPoint::new(DisplayRow(1), 0)),
|
||
(Point::new(3, 0), DisplayPoint::new(DisplayRow(5), 0)),
|
||
];
|
||
for (buffer_point, display_point) in point_to_display_points {
|
||
assert_eq!(
|
||
snapshot.point_to_display_point(buffer_point, Bias::Left),
|
||
display_point,
|
||
"point_to_display_point({:?}, Bias::Left)",
|
||
buffer_point
|
||
);
|
||
assert_eq!(
|
||
snapshot.point_to_display_point(buffer_point, Bias::Right),
|
||
display_point,
|
||
"point_to_display_point({:?}, Bias::Right)",
|
||
buffer_point
|
||
);
|
||
}
|
||
|
||
let display_points_to_points = [
|
||
(
|
||
DisplayPoint::new(DisplayRow(1), 0),
|
||
Point::new(1, 0),
|
||
Point::new(2, 5),
|
||
),
|
||
(
|
||
DisplayPoint::new(DisplayRow(2), 0),
|
||
Point::new(1, 0),
|
||
Point::new(2, 5),
|
||
),
|
||
(
|
||
DisplayPoint::new(DisplayRow(3), 0),
|
||
Point::new(1, 0),
|
||
Point::new(2, 5),
|
||
),
|
||
(
|
||
DisplayPoint::new(DisplayRow(4), 0),
|
||
Point::new(1, 0),
|
||
Point::new(2, 5),
|
||
),
|
||
(
|
||
DisplayPoint::new(DisplayRow(5), 0),
|
||
Point::new(3, 0),
|
||
Point::new(3, 0),
|
||
),
|
||
];
|
||
for (display_point, left_buffer_point, right_buffer_point) in display_points_to_points {
|
||
assert_eq!(
|
||
snapshot.display_point_to_point(display_point, Bias::Left),
|
||
left_buffer_point,
|
||
"display_point_to_point({:?}, Bias::Left)",
|
||
display_point
|
||
);
|
||
assert_eq!(
|
||
snapshot.display_point_to_point(display_point, Bias::Right),
|
||
right_buffer_point,
|
||
"display_point_to_point({:?}, Bias::Right)",
|
||
display_point
|
||
);
|
||
}
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
|
||
cx.background_executor
|
||
.set_block_on_ticks(usize::MAX..=usize::MAX);
|
||
|
||
let text = r#"
|
||
fn outer() {}
|
||
|
||
mod module {
|
||
fn inner() {}
|
||
}"#
|
||
.unindent();
|
||
|
||
let theme =
|
||
SyntaxTheme::new_test(vec![("mod.body", Hsla::red()), ("fn.name", Hsla::blue())]);
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
name: "Test".into(),
|
||
matcher: LanguageMatcher {
|
||
path_suffixes: vec![".test".to_string()],
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||
)
|
||
.with_highlights_query(
|
||
r#"
|
||
(mod_item name: (identifier) body: _ @mod.body)
|
||
(function_item name: (identifier) @fn.name)
|
||
"#,
|
||
)
|
||
.unwrap(),
|
||
);
|
||
language.set_theme(&theme);
|
||
|
||
cx.update(|cx| init_test(cx, |_| {}));
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
|
||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
|
||
let font_size = px(16.0);
|
||
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer,
|
||
font("Courier"),
|
||
font_size,
|
||
Some(px(40.0)),
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
assert_eq!(
|
||
cx.update(|cx| syntax_chunks(DisplayRow(0)..DisplayRow(5), &map, &theme, cx)),
|
||
[
|
||
("fn \n".to_string(), None),
|
||
("oute".to_string(), Some(Hsla::blue())),
|
||
("\n".to_string(), None),
|
||
("r".to_string(), Some(Hsla::blue())),
|
||
("() \n{}\n\n".to_string(), None),
|
||
]
|
||
);
|
||
assert_eq!(
|
||
cx.update(|cx| syntax_chunks(DisplayRow(3)..DisplayRow(5), &map, &theme, cx)),
|
||
[("{}\n\n".to_string(), None)]
|
||
);
|
||
|
||
map.update(cx, |map, cx| {
|
||
map.fold(
|
||
vec![Crease::simple(
|
||
MultiBufferPoint::new(0, 6)..MultiBufferPoint::new(3, 2),
|
||
FoldPlaceholder::test(),
|
||
)],
|
||
cx,
|
||
)
|
||
});
|
||
assert_eq!(
|
||
cx.update(|cx| syntax_chunks(DisplayRow(1)..DisplayRow(4), &map, &theme, cx)),
|
||
[
|
||
("out".to_string(), Some(Hsla::blue())),
|
||
("⋯\n".to_string(), None),
|
||
(" ".to_string(), Some(Hsla::red())),
|
||
("\n".to_string(), None),
|
||
("fn ".to_string(), Some(Hsla::red())),
|
||
("i".to_string(), Some(Hsla::blue())),
|
||
("\n".to_string(), None)
|
||
]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
|
||
cx.update(|cx| init_test(cx, |_| {}));
|
||
|
||
let theme =
|
||
SyntaxTheme::new_test(vec![("operator", Hsla::red()), ("string", Hsla::green())]);
|
||
let language = Arc::new(
|
||
Language::new(
|
||
LanguageConfig {
|
||
name: "Test".into(),
|
||
matcher: LanguageMatcher {
|
||
path_suffixes: vec![".test".to_string()],
|
||
..Default::default()
|
||
},
|
||
..Default::default()
|
||
},
|
||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||
)
|
||
.with_highlights_query(
|
||
r#"
|
||
":" @operator
|
||
(string_literal) @string
|
||
"#,
|
||
)
|
||
.unwrap(),
|
||
);
|
||
language.set_theme(&theme);
|
||
|
||
let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»«:» B = "c «d»""#, false);
|
||
|
||
let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
|
||
cx.condition(&buffer, |buf, _| !buf.is_parsing()).await;
|
||
|
||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||
|
||
let font_size = px(16.0);
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer,
|
||
font("Courier"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
let style = HighlightStyle {
|
||
color: Some(Hsla::blue()),
|
||
..Default::default()
|
||
};
|
||
|
||
map.update(cx, |map, cx| {
|
||
map.highlight_text(
|
||
HighlightKey::Editor,
|
||
highlighted_ranges
|
||
.into_iter()
|
||
.map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
|
||
.map(|range| {
|
||
buffer_snapshot.anchor_before(range.start)
|
||
..buffer_snapshot.anchor_before(range.end)
|
||
})
|
||
.collect(),
|
||
style,
|
||
false,
|
||
cx,
|
||
);
|
||
});
|
||
|
||
assert_eq!(
|
||
cx.update(|cx| chunks(DisplayRow(0)..DisplayRow(10), &map, &theme, cx)),
|
||
[
|
||
("const ".to_string(), None, None),
|
||
("a".to_string(), None, Some(Hsla::blue())),
|
||
(":".to_string(), Some(Hsla::red()), Some(Hsla::blue())),
|
||
(" B = ".to_string(), None, None),
|
||
("\"c ".to_string(), Some(Hsla::green()), None),
|
||
("d".to_string(), Some(Hsla::green()), Some(Hsla::blue())),
|
||
("\"".to_string(), Some(Hsla::green()), None),
|
||
]
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_clip_point(cx: &mut gpui::App) {
|
||
init_test(cx, |_| {});
|
||
|
||
fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::App) {
|
||
let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx);
|
||
|
||
match bias {
|
||
Bias::Left => {
|
||
if shift_right {
|
||
*markers[1].column_mut() += 1;
|
||
}
|
||
|
||
assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0])
|
||
}
|
||
Bias::Right => {
|
||
if shift_right {
|
||
*markers[0].column_mut() += 1;
|
||
}
|
||
|
||
assert_eq!(unmarked_snapshot.clip_point(markers[0], bias), markers[1])
|
||
}
|
||
};
|
||
}
|
||
|
||
use Bias::{Left, Right};
|
||
assert("ˇˇα", false, Left, cx);
|
||
assert("ˇˇα", true, Left, cx);
|
||
assert("ˇˇα", false, Right, cx);
|
||
assert("ˇαˇ", true, Right, cx);
|
||
assert("ˇˇ✋", false, Left, cx);
|
||
assert("ˇˇ✋", true, Left, cx);
|
||
assert("ˇˇ✋", false, Right, cx);
|
||
assert("ˇ✋ˇ", true, Right, cx);
|
||
assert("ˇˇ🍐", false, Left, cx);
|
||
assert("ˇˇ🍐", true, Left, cx);
|
||
assert("ˇˇ🍐", false, Right, cx);
|
||
assert("ˇ🍐ˇ", true, Right, cx);
|
||
assert("ˇˇ\t", false, Left, cx);
|
||
assert("ˇˇ\t", true, Left, cx);
|
||
assert("ˇˇ\t", false, Right, cx);
|
||
assert("ˇ\tˇ", true, Right, cx);
|
||
assert(" ˇˇ\t", false, Left, cx);
|
||
assert(" ˇˇ\t", true, Left, cx);
|
||
assert(" ˇˇ\t", false, Right, cx);
|
||
assert(" ˇ\tˇ", true, Right, cx);
|
||
assert(" ˇˇ\t", false, Left, cx);
|
||
assert(" ˇˇ\t", false, Right, cx);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_clip_at_line_ends(cx: &mut gpui::App) {
|
||
init_test(cx, |_| {});
|
||
|
||
fn assert(text: &str, cx: &mut gpui::App) {
|
||
let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx);
|
||
unmarked_snapshot.clip_at_line_ends = true;
|
||
assert_eq!(
|
||
unmarked_snapshot.clip_point(markers[1], Bias::Left),
|
||
markers[0]
|
||
);
|
||
}
|
||
|
||
assert("ˇˇ", cx);
|
||
assert("ˇaˇ", cx);
|
||
assert("aˇbˇ", cx);
|
||
assert("aˇαˇ", cx);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_creases(cx: &mut gpui::App) {
|
||
init_test(cx, |_| {});
|
||
|
||
let text = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj\nkkk\nlll";
|
||
let buffer = MultiBuffer::build_simple(text, cx);
|
||
let font_size = px(14.0);
|
||
cx.new(|cx| {
|
||
let mut map = DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Helvetica"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
);
|
||
let snapshot = map.buffer.read(cx).snapshot(cx);
|
||
let range =
|
||
snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_after(Point::new(3, 3));
|
||
|
||
map.crease_map.insert(
|
||
[Crease::inline(
|
||
range,
|
||
FoldPlaceholder::test(),
|
||
|_row, _status, _toggle, _window, _cx| div(),
|
||
|_row, _status, _window, _cx| div(),
|
||
)],
|
||
&map.buffer.read(cx).snapshot(cx),
|
||
);
|
||
|
||
map
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_tabs_with_multibyte_chars(cx: &mut gpui::App) {
|
||
init_test(cx, |_| {});
|
||
|
||
let text = "✅\t\tα\nβ\t\n🏀β\t\tγ";
|
||
let buffer = MultiBuffer::build_simple(text, cx);
|
||
let font_size = px(14.0);
|
||
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Helvetica"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
let map = map.update(cx, |map, cx| map.snapshot(cx));
|
||
assert_eq!(map.text(), "✅ α\nβ \n🏀β γ");
|
||
assert_eq!(
|
||
map.text_chunks(DisplayRow(0)).collect::<String>(),
|
||
"✅ α\nβ \n🏀β γ"
|
||
);
|
||
assert_eq!(
|
||
map.text_chunks(DisplayRow(1)).collect::<String>(),
|
||
"β \n🏀β γ"
|
||
);
|
||
assert_eq!(
|
||
map.text_chunks(DisplayRow(2)).collect::<String>(),
|
||
"🏀β γ"
|
||
);
|
||
|
||
let point = MultiBufferPoint::new(0, "✅\t\t".len() as u32);
|
||
let display_point = DisplayPoint::new(DisplayRow(0), "✅ ".len() as u32);
|
||
assert_eq!(point.to_display_point(&map), display_point);
|
||
assert_eq!(display_point.to_point(&map), point);
|
||
|
||
let point = MultiBufferPoint::new(1, "β\t".len() as u32);
|
||
let display_point = DisplayPoint::new(DisplayRow(1), "β ".len() as u32);
|
||
assert_eq!(point.to_display_point(&map), display_point);
|
||
assert_eq!(display_point.to_point(&map), point,);
|
||
|
||
let point = MultiBufferPoint::new(2, "🏀β\t\t".len() as u32);
|
||
let display_point = DisplayPoint::new(DisplayRow(2), "🏀β ".len() as u32);
|
||
assert_eq!(point.to_display_point(&map), display_point);
|
||
assert_eq!(display_point.to_point(&map), point,);
|
||
|
||
// Display points inside of expanded tabs
|
||
assert_eq!(
|
||
DisplayPoint::new(DisplayRow(0), "✅ ".len() as u32).to_point(&map),
|
||
MultiBufferPoint::new(0, "✅\t".len() as u32),
|
||
);
|
||
assert_eq!(
|
||
DisplayPoint::new(DisplayRow(0), "✅ ".len() as u32).to_point(&map),
|
||
MultiBufferPoint::new(0, "✅".len() as u32),
|
||
);
|
||
|
||
// Clipping display points inside of multi-byte characters
|
||
assert_eq!(
|
||
map.clip_point(
|
||
DisplayPoint::new(DisplayRow(0), "✅".len() as u32 - 1),
|
||
Left
|
||
),
|
||
DisplayPoint::new(DisplayRow(0), 0)
|
||
);
|
||
assert_eq!(
|
||
map.clip_point(
|
||
DisplayPoint::new(DisplayRow(0), "✅".len() as u32 - 1),
|
||
Bias::Right
|
||
),
|
||
DisplayPoint::new(DisplayRow(0), "✅".len() as u32)
|
||
);
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_max_point(cx: &mut gpui::App) {
|
||
init_test(cx, |_| {});
|
||
|
||
let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
|
||
let font_size = px(14.0);
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Helvetica"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
assert_eq!(
|
||
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
|
||
DisplayPoint::new(DisplayRow(1), 11)
|
||
)
|
||
}
|
||
|
||
fn syntax_chunks(
|
||
rows: Range<DisplayRow>,
|
||
map: &Entity<DisplayMap>,
|
||
theme: &SyntaxTheme,
|
||
cx: &mut App,
|
||
) -> Vec<(String, Option<Hsla>)> {
|
||
chunks(rows, map, theme, cx)
|
||
.into_iter()
|
||
.map(|(text, color, _)| (text, color))
|
||
.collect()
|
||
}
|
||
|
||
fn chunks(
|
||
rows: Range<DisplayRow>,
|
||
map: &Entity<DisplayMap>,
|
||
theme: &SyntaxTheme,
|
||
cx: &mut App,
|
||
) -> Vec<(String, Option<Hsla>, Option<Hsla>)> {
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
let mut chunks: Vec<(String, Option<Hsla>, Option<Hsla>)> = Vec::new();
|
||
for chunk in snapshot.chunks(rows, true, HighlightStyles::default()) {
|
||
let syntax_color = chunk
|
||
.syntax_highlight_id
|
||
.and_then(|id| id.style(theme)?.color);
|
||
let highlight_color = chunk.highlight_style.and_then(|style| style.color);
|
||
if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut()
|
||
&& syntax_color == *last_syntax_color
|
||
&& highlight_color == *last_highlight_color
|
||
{
|
||
last_chunk.push_str(chunk.text);
|
||
continue;
|
||
}
|
||
chunks.push((chunk.text.to_string(), syntax_color, highlight_color));
|
||
}
|
||
chunks
|
||
}
|
||
|
||
fn init_test(cx: &mut App, f: impl Fn(&mut SettingsContent)) {
|
||
let settings = SettingsStore::test(cx);
|
||
cx.set_global(settings);
|
||
crate::init(cx);
|
||
theme::init(LoadThemes::JustBase, cx);
|
||
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||
store.update_user_settings(cx, f);
|
||
});
|
||
}
|
||
|
||
#[gpui::test]
|
||
fn test_isomorphic_display_point_ranges_for_buffer_range(cx: &mut gpui::TestAppContext) {
|
||
cx.update(|cx| init_test(cx, |_| {}));
|
||
|
||
let buffer = cx.new(|cx| Buffer::local("let x = 5;\n", cx));
|
||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||
let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
|
||
|
||
let font_size = px(14.0);
|
||
let map = cx.new(|cx| {
|
||
DisplayMap::new(
|
||
buffer.clone(),
|
||
font("Helvetica"),
|
||
font_size,
|
||
None,
|
||
1,
|
||
1,
|
||
FoldPlaceholder::test(),
|
||
DiagnosticSeverity::Warning,
|
||
cx,
|
||
)
|
||
});
|
||
|
||
// Without inlays, a buffer range maps to a single display range.
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
|
||
MultiBufferOffset(4)..MultiBufferOffset(9),
|
||
);
|
||
assert_eq!(ranges.len(), 1);
|
||
// "x = 5" is columns 4..9 with no inlays shifting anything.
|
||
assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 4));
|
||
assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 9));
|
||
|
||
// Insert a 4-char inlay hint ": i32" at buffer offset 5 (after "x").
|
||
map.update(cx, |map, cx| {
|
||
map.splice_inlays(
|
||
&[],
|
||
vec![Inlay::mock_hint(
|
||
0,
|
||
buffer_snapshot.anchor_after(MultiBufferOffset(5)),
|
||
": i32",
|
||
)],
|
||
cx,
|
||
);
|
||
});
|
||
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
|
||
assert_eq!(snapshot.text(), "let x: i32 = 5;\n");
|
||
|
||
// A buffer range [4..9] ("x = 5") now spans across the inlay.
|
||
// It should be split into two display ranges that skip the inlay text.
|
||
let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
|
||
MultiBufferOffset(4)..MultiBufferOffset(9),
|
||
);
|
||
assert_eq!(
|
||
ranges.len(),
|
||
2,
|
||
"expected the range to be split around the inlay, got: {:?}",
|
||
ranges,
|
||
);
|
||
// First sub-range: buffer [4, 5) → "x" at display columns 4..5
|
||
assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 4));
|
||
assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 5));
|
||
// Second sub-range: buffer [5, 9) → " = 5" at display columns 10..14
|
||
// (shifted right by the 5-char ": i32" inlay)
|
||
assert_eq!(ranges[1].start, DisplayPoint::new(DisplayRow(0), 10));
|
||
assert_eq!(ranges[1].end, DisplayPoint::new(DisplayRow(0), 14));
|
||
|
||
// A range entirely before the inlay is not split.
|
||
let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
|
||
MultiBufferOffset(0)..MultiBufferOffset(5),
|
||
);
|
||
assert_eq!(ranges.len(), 1);
|
||
assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 0));
|
||
assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 5));
|
||
|
||
// A range entirely after the inlay is not split.
|
||
let ranges = snapshot.isomorphic_display_point_ranges_for_buffer_range(
|
||
MultiBufferOffset(5)..MultiBufferOffset(9),
|
||
);
|
||
assert_eq!(ranges.len(), 1);
|
||
assert_eq!(ranges[0].start, DisplayPoint::new(DisplayRow(0), 10));
|
||
assert_eq!(ranges[0].end, DisplayPoint::new(DisplayRow(0), 14));
|
||
}
|
||
}
|