Detailed changes
@@ -0,0 +1,92 @@
+[package]
+name = "editor"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/editor.rs"
+doctest = false
+
+[features]
+test-support = [
+ "copilot/test-support",
+ "text/test-support",
+ "language/test-support",
+ "gpui/test-support",
+ "multi_buffer/test-support",
+ "project/test-support",
+ "util/test-support",
+ "workspace/test-support",
+ "tree-sitter-rust",
+ "tree-sitter-typescript"
+]
+
+[dependencies]
+client = { path = "../client" }
+clock = { path = "../clock" }
+copilot = { path = "../copilot" }
+db = { path = "../db" }
+drag_and_drop = { path = "../drag_and_drop" }
+collections = { path = "../collections" }
+context_menu = { path = "../context_menu" }
+fuzzy = { path = "../fuzzy" }
+git = { path = "../git" }
+gpui = { path = "../gpui" }
+language = { path = "../language" }
+lsp = { path = "../lsp" }
+multi_buffer = { path = "../multi_buffer" }
+project = { path = "../project" }
+rpc = { path = "../rpc" }
+rich_text = { path = "../rich_text" }
+settings = { path = "../settings" }
+snippet = { path = "../snippet" }
+sum_tree = { path = "../sum_tree" }
+text = { path = "../text" }
+theme = { path = "../theme" }
+util = { path = "../util" }
+sqlez = { path = "../sqlez" }
+workspace = { path = "../workspace" }
+
+aho-corasick = "1.1"
+anyhow.workspace = true
+convert_case = "0.6.0"
+futures.workspace = true
+indoc = "1.0.4"
+itertools = "0.10"
+lazy_static.workspace = true
+log.workspace = true
+ordered-float.workspace = true
+parking_lot.workspace = true
+postage.workspace = true
+rand.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+smallvec.workspace = true
+smol.workspace = true
+
+tree-sitter-rust = { workspace = true, optional = true }
+tree-sitter-html = { workspace = true, optional = true }
+tree-sitter-typescript = { workspace = true, optional = true }
+
+[dev-dependencies]
+copilot = { path = "../copilot", features = ["test-support"] }
+text = { path = "../text", features = ["test-support"] }
+language = { path = "../language", features = ["test-support"] }
+lsp = { path = "../lsp", features = ["test-support"] }
+gpui = { path = "../gpui", features = ["test-support"] }
+util = { path = "../util", features = ["test-support"] }
+project = { path = "../project", features = ["test-support"] }
+settings = { path = "../settings", features = ["test-support"] }
+workspace = { path = "../workspace", features = ["test-support"] }
+multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
+
+ctor.workspace = true
+env_logger.workspace = true
+rand.workspace = true
+unindent.workspace = true
+tree-sitter.workspace = true
+tree-sitter-rust.workspace = true
+tree-sitter-html.workspace = true
+tree-sitter-typescript.workspace = true
@@ -0,0 +1,113 @@
+use crate::EditorSettings;
+use gpui::{Entity, ModelContext};
+use settings::SettingsStore;
+use smol::Timer;
+use std::time::Duration;
+
+pub struct BlinkManager {
+ blink_interval: Duration,
+
+ blink_epoch: usize,
+ blinking_paused: bool,
+ visible: bool,
+ enabled: bool,
+}
+
+impl BlinkManager {
+ pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
+ // Make sure we blink the cursors if the setting is re-enabled
+ cx.observe_global::<SettingsStore, _>(move |this, cx| {
+ this.blink_cursors(this.blink_epoch, cx)
+ })
+ .detach();
+
+ Self {
+ blink_interval,
+
+ blink_epoch: 0,
+ blinking_paused: false,
+ visible: true,
+ enabled: false,
+ }
+ }
+
+ fn next_blink_epoch(&mut self) -> usize {
+ self.blink_epoch += 1;
+ self.blink_epoch
+ }
+
+ pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
+ self.show_cursor(cx);
+
+ let epoch = self.next_blink_epoch();
+ let interval = self.blink_interval;
+ cx.spawn(|this, mut cx| {
+ let this = this.downgrade();
+ async move {
+ Timer::after(interval).await;
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
+ }
+ }
+ })
+ .detach();
+ }
+
+ fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
+ if epoch == self.blink_epoch {
+ self.blinking_paused = false;
+ self.blink_cursors(epoch, cx);
+ }
+ }
+
+ fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
+ if settings::get::<EditorSettings>(cx).cursor_blink {
+ if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
+ self.visible = !self.visible;
+ cx.notify();
+
+ let epoch = self.next_blink_epoch();
+ let interval = self.blink_interval;
+ cx.spawn(|this, mut cx| {
+ let this = this.downgrade();
+ async move {
+ Timer::after(interval).await;
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
+ }
+ }
+ })
+ .detach();
+ }
+ } else {
+ self.show_cursor(cx);
+ }
+ }
+
+ pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
+ if !self.visible {
+ self.visible = true;
+ cx.notify();
+ }
+ }
+
+ pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
+ self.enabled = true;
+ // Set cursors as invisible and start blinking: this causes cursors
+ // to be visible during the next render.
+ self.visible = false;
+ self.blink_cursors(self.blink_epoch, cx);
+ }
+
+ pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
+ self.enabled = false;
+ }
+
+ pub fn visible(&self) -> bool {
+ self.visible
+ }
+}
+
+impl Entity for BlinkManager {
+ type Event = ();
+}
@@ -0,0 +1,1912 @@
+mod block_map;
+mod fold_map;
+mod inlay_map;
+mod tab_map;
+mod wrap_map;
+
+use crate::{
+ link_go_to_definition::InlayHighlight, movement::TextLayoutDetails, Anchor, AnchorRangeExt,
+ EditorStyle, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
+};
+pub use block_map::{BlockMap, BlockPoint};
+use collections::{BTreeMap, HashMap, HashSet};
+use fold_map::FoldMap;
+use gpui::{
+ color::Color,
+ fonts::{FontId, HighlightStyle, Underline},
+ text_layout::{Line, RunStyle},
+ Entity, ModelContext, ModelHandle,
+};
+use inlay_map::InlayMap;
+use language::{
+ language_settings::language_settings, OffsetUtf16, Point, Subscription as BufferSubscription,
+};
+use lsp::DiagnosticSeverity;
+use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc};
+use sum_tree::{Bias, TreeMap};
+use tab_map::TabMap;
+use wrap_map::WrapMap;
+
+pub use block_map::{
+ BlockBufferRows as DisplayBufferRows, BlockChunks as DisplayChunks, BlockContext,
+ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
+};
+
+pub use self::fold_map::FoldPoint;
+pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+pub enum FoldStatus {
+ Folded,
+ Foldable,
+}
+
+pub trait ToDisplayPoint {
+ fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
+}
+
+type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
+type InlayHighlights = BTreeMap<TypeId, HashMap<InlayId, (HighlightStyle, InlayHighlight)>>;
+
+pub struct DisplayMap {
+ buffer: ModelHandle<MultiBuffer>,
+ buffer_subscription: BufferSubscription,
+ fold_map: FoldMap,
+ inlay_map: InlayMap,
+ tab_map: TabMap,
+ wrap_map: ModelHandle<WrapMap>,
+ block_map: BlockMap,
+ text_highlights: TextHighlights,
+ inlay_highlights: InlayHighlights,
+ pub clip_at_line_ends: bool,
+}
+
+impl Entity for DisplayMap {
+ type Event = ();
+}
+
+impl DisplayMap {
+ pub fn new(
+ buffer: ModelHandle<MultiBuffer>,
+ font_id: FontId,
+ font_size: f32,
+ wrap_width: Option<f32>,
+ buffer_header_height: u8,
+ excerpt_header_height: u8,
+ cx: &mut ModelContext<Self>,
+ ) -> Self {
+ let buffer_subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+
+ let tab_size = Self::tab_size(&buffer, cx);
+ let (inlay_map, snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ let (fold_map, snapshot) = FoldMap::new(snapshot);
+ let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
+ let (wrap_map, snapshot) = WrapMap::new(snapshot, font_id, 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 {
+ buffer,
+ buffer_subscription,
+ fold_map,
+ inlay_map,
+ tab_map,
+ wrap_map,
+ block_map,
+ text_highlights: Default::default(),
+ inlay_highlights: Default::default(),
+ clip_at_line_ends: false,
+ }
+ }
+
+ pub fn snapshot(&mut self, cx: &mut ModelContext<Self>) -> DisplaySnapshot {
+ let buffer_snapshot = self.buffer.read(cx).snapshot(cx);
+ let edits = self.buffer_subscription.consume().into_inner();
+ let (inlay_snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+ let (fold_snapshot, edits) = self.fold_map.read(inlay_snapshot.clone(), edits);
+ let tab_size = Self::tab_size(&self.buffer, cx);
+ let (tab_snapshot, edits) = self.tab_map.sync(fold_snapshot.clone(), edits, tab_size);
+ let (wrap_snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(tab_snapshot.clone(), edits, cx));
+ let block_snapshot = self.block_map.read(wrap_snapshot.clone(), edits);
+
+ DisplaySnapshot {
+ buffer_snapshot: self.buffer.read(cx).snapshot(cx),
+ fold_snapshot,
+ inlay_snapshot,
+ tab_snapshot,
+ wrap_snapshot,
+ block_snapshot,
+ text_highlights: self.text_highlights.clone(),
+ inlay_highlights: self.inlay_highlights.clone(),
+ clip_at_line_ends: self.clip_at_line_ends,
+ }
+ }
+
+ pub fn set_state(&mut self, other: &DisplaySnapshot, cx: &mut ModelContext<Self>) {
+ self.fold(
+ other
+ .folds_in_range(0..other.buffer_snapshot.len())
+ .map(|fold| fold.to_offset(&other.buffer_snapshot)),
+ cx,
+ );
+ }
+
+ pub fn fold<T: ToOffset>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ cx: &mut ModelContext<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);
+ let (snapshot, edits) = fold_map.fold(ranges);
+ 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);
+ }
+
+ pub fn unfold<T: ToOffset>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ inclusive: bool,
+ cx: &mut ModelContext<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);
+ let (snapshot, edits) = fold_map.unfold(ranges, inclusive);
+ 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);
+ }
+
+ pub fn insert_blocks(
+ &mut self,
+ blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
+ cx: &mut ModelContext<Self>,
+ ) -> Vec<BlockId> {
+ 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 (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 mut block_map = self.block_map.write(snapshot, edits);
+ block_map.insert(blocks)
+ }
+
+ pub fn replace_blocks(&mut self, styles: HashMap<BlockId, RenderBlock>) {
+ self.block_map.replace(styles);
+ }
+
+ pub fn remove_blocks(&mut self, ids: HashSet<BlockId>, cx: &mut ModelContext<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 (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 mut block_map = self.block_map.write(snapshot, edits);
+ block_map.remove(ids);
+ }
+
+ pub fn highlight_text(
+ &mut self,
+ type_id: TypeId,
+ ranges: Vec<Range<Anchor>>,
+ style: HighlightStyle,
+ ) {
+ self.text_highlights
+ .insert(Some(type_id), Arc::new((style, ranges)));
+ }
+
+ pub fn highlight_inlays(
+ &mut self,
+ type_id: TypeId,
+ highlights: Vec<InlayHighlight>,
+ style: HighlightStyle,
+ ) {
+ for highlight in highlights {
+ self.inlay_highlights
+ .entry(type_id)
+ .or_default()
+ .insert(highlight.inlay, (style, highlight));
+ }
+ }
+
+ pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
+ let highlights = self.text_highlights.get(&Some(type_id))?;
+ Some((highlights.0, &highlights.1))
+ }
+ pub fn clear_highlights(&mut self, type_id: TypeId) -> bool {
+ let mut cleared = self.text_highlights.remove(&Some(type_id)).is_some();
+ cleared |= self.inlay_highlights.remove(&type_id).is_none();
+ cleared
+ }
+
+ pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) -> bool {
+ self.wrap_map
+ .update(cx, |map, cx| map.set_font(font_id, font_size, cx))
+ }
+
+ pub fn set_fold_ellipses_color(&mut self, color: Color) -> bool {
+ self.fold_map.set_ellipses_color(color)
+ }
+
+ pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
+ self.wrap_map
+ .update(cx, |map, cx| map.set_wrap_width(width, cx))
+ }
+
+ pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+ self.inlay_map.current_inlays()
+ }
+
+ pub fn splice_inlays(
+ &mut self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<Inlay>,
+ cx: &mut ModelContext<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 (snapshot, edits) = self.inlay_map.sync(buffer_snapshot, edits);
+ let (snapshot, edits) = self.fold_map.read(snapshot, edits);
+ let tab_size = Self::tab_size(&self.buffer, cx);
+ 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);
+
+ 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 (snapshot, edits) = self
+ .wrap_map
+ .update(cx, |map, cx| map.sync(snapshot, edits, cx));
+ self.block_map.read(snapshot, edits);
+ }
+
+ fn tab_size(buffer: &ModelHandle<MultiBuffer>, cx: &mut ModelContext<Self>) -> NonZeroU32 {
+ let language = buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|buffer| buffer.read(cx).language());
+ language_settings(language.as_deref(), None, cx).tab_size
+ }
+
+ #[cfg(test)]
+ pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
+ self.wrap_map.read(cx).is_rewrapping()
+ }
+}
+
+#[derive(Debug, Default)]
+pub struct Highlights<'a> {
+ pub text_highlights: Option<&'a TextHighlights>,
+ pub inlay_highlights: Option<&'a InlayHighlights>,
+ pub inlay_highlight_style: Option<HighlightStyle>,
+ pub suggestion_highlight_style: Option<HighlightStyle>,
+}
+
+pub struct HighlightedChunk<'a> {
+ pub chunk: &'a str,
+ pub style: Option<HighlightStyle>,
+ pub is_tab: bool,
+}
+
+pub struct DisplaySnapshot {
+ pub buffer_snapshot: MultiBufferSnapshot,
+ pub fold_snapshot: fold_map::FoldSnapshot,
+ inlay_snapshot: inlay_map::InlaySnapshot,
+ tab_snapshot: tab_map::TabSnapshot,
+ wrap_snapshot: wrap_map::WrapSnapshot,
+ block_snapshot: block_map::BlockSnapshot,
+ text_highlights: TextHighlights,
+ inlay_highlights: InlayHighlights,
+ clip_at_line_ends: bool,
+}
+
+impl DisplaySnapshot {
+ #[cfg(test)]
+ pub fn fold_count(&self) -> usize {
+ self.fold_snapshot.fold_count()
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.buffer_snapshot.len() == 0
+ }
+
+ pub fn buffer_rows(&self, start_row: u32) -> DisplayBufferRows {
+ self.block_snapshot.buffer_rows(start_row)
+ }
+
+ pub fn max_buffer_row(&self) -> u32 {
+ self.buffer_snapshot.max_buffer_row()
+ }
+
+ pub fn prev_line_boundary(&self, mut point: Point) -> (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;
+ }
+ }
+
+ pub fn next_line_boundary(&self, mut point: Point) -> (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::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 {
+ return (point, display_point);
+ }
+ point = next_point;
+ }
+ }
+
+ // used by line_mode selections and tries to match vim behaviour
+ pub fn expand_to_line(&self, range: Range<Point>) -> Range<Point> {
+ let new_start = if range.start.row == 0 {
+ Point::new(0, 0)
+ } else if range.start.row == self.max_buffer_row()
+ || (range.end.column > 0 && range.end.row == self.max_buffer_row())
+ {
+ Point::new(range.start.row - 1, self.line_len(range.start.row - 1))
+ } else {
+ self.prev_line_boundary(range.start).0
+ };
+
+ let new_end = if range.end.column == 0 {
+ range.end
+ } else if range.end.row < self.max_buffer_row() {
+ self.buffer_snapshot
+ .clip_point(Point::new(range.end.row + 1, 0), Bias::Left)
+ } else {
+ self.buffer_snapshot.max_point()
+ };
+
+ new_start..new_end
+ }
+
+ fn point_to_display_point(&self, point: Point, 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.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)
+ }
+
+ 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))
+ }
+
+ 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);
+ let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
+ let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0;
+ fold_point.to_inlay_point(&self.fold_snapshot)
+ }
+
+ 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);
+ let tab_point = self.wrap_snapshot.to_tab_point(wrap_point);
+ self.tab_snapshot.to_fold_point(tab_point, bias).0
+ }
+
+ pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint {
+ let tab_point = self.tab_snapshot.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
+ pub fn text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
+ self.block_snapshot
+ .chunks(
+ display_row..self.max_point().row() + 1,
+ false,
+ 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
+ pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator<Item = &str> {
+ (0..=display_row).into_iter().rev().flat_map(|row| {
+ self.block_snapshot
+ .chunks(row..row + 1, false, Highlights::default())
+ .map(|h| h.text)
+ .collect::<Vec<_>>()
+ .into_iter()
+ .rev()
+ })
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ display_rows: Range<u32>,
+ language_aware: bool,
+ inlay_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
+ ) -> DisplayChunks<'a> {
+ self.block_snapshot.chunks(
+ display_rows,
+ language_aware,
+ Highlights {
+ text_highlights: Some(&self.text_highlights),
+ inlay_highlights: Some(&self.inlay_highlights),
+ inlay_highlight_style,
+ suggestion_highlight_style,
+ },
+ )
+ }
+
+ pub fn highlighted_chunks<'a>(
+ &'a self,
+ display_rows: Range<u32>,
+ language_aware: bool,
+ style: &'a EditorStyle,
+ ) -> impl Iterator<Item = HighlightedChunk<'a>> {
+ self.chunks(
+ display_rows,
+ language_aware,
+ Some(style.theme.hint),
+ Some(style.theme.suggestion),
+ )
+ .map(|chunk| {
+ let mut highlight_style = chunk
+ .syntax_highlight_id
+ .and_then(|id| id.style(&style.syntax));
+
+ if let Some(chunk_highlight) = chunk.highlight_style {
+ if let Some(highlight_style) = highlight_style.as_mut() {
+ highlight_style.highlight(chunk_highlight);
+ } else {
+ highlight_style = Some(chunk_highlight);
+ }
+ }
+
+ let mut diagnostic_highlight = HighlightStyle::default();
+
+ if chunk.is_unnecessary {
+ diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade);
+ }
+
+ if let Some(severity) = chunk.diagnostic_severity {
+ // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code.
+ if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary {
+ let diagnostic_style = super::diagnostic_style(severity, true, style);
+ diagnostic_highlight.underline = Some(Underline {
+ color: Some(diagnostic_style.message.text.color),
+ thickness: 1.0.into(),
+ squiggly: true,
+ });
+ }
+ }
+
+ if let Some(highlight_style) = highlight_style.as_mut() {
+ highlight_style.highlight(diagnostic_highlight);
+ } else {
+ highlight_style = Some(diagnostic_highlight);
+ }
+
+ HighlightedChunk {
+ chunk: chunk.text,
+ style: highlight_style,
+ is_tab: chunk.is_tab,
+ }
+ })
+ }
+
+ pub fn lay_out_line_for_row(
+ &self,
+ display_row: u32,
+ TextLayoutDetails {
+ font_cache,
+ text_layout_cache,
+ editor_style,
+ }: &TextLayoutDetails,
+ ) -> Line {
+ let mut styles = Vec::new();
+ let mut line = String::new();
+ let mut ended_in_newline = false;
+
+ let range = display_row..display_row + 1;
+ for chunk in self.highlighted_chunks(range, false, editor_style) {
+ line.push_str(chunk.chunk);
+
+ let text_style = if let Some(style) = chunk.style {
+ editor_style
+ .text
+ .clone()
+ .highlight(style, font_cache)
+ .map(Cow::Owned)
+ .unwrap_or_else(|_| Cow::Borrowed(&editor_style.text))
+ } else {
+ Cow::Borrowed(&editor_style.text)
+ };
+ ended_in_newline = chunk.chunk.ends_with("\n");
+
+ styles.push((
+ chunk.chunk.len(),
+ RunStyle {
+ font_id: text_style.font_id,
+ color: text_style.color,
+ underline: text_style.underline,
+ },
+ ));
+ }
+
+ // our pixel positioning logic assumes each line ends in \n,
+ // this is almost always true except for the last line which
+ // may have no trailing newline.
+ if !ended_in_newline && display_row == self.max_point().row() {
+ line.push_str("\n");
+
+ styles.push((
+ "\n".len(),
+ RunStyle {
+ font_id: editor_style.text.font_id,
+ color: editor_style.text_color,
+ underline: editor_style.text.underline,
+ },
+ ));
+ }
+
+ text_layout_cache.layout_str(&line, editor_style.text.font_size, &styles)
+ }
+
+ pub fn x_for_point(
+ &self,
+ display_point: DisplayPoint,
+ text_layout_details: &TextLayoutDetails,
+ ) -> f32 {
+ let layout_line = self.lay_out_line_for_row(display_point.row(), text_layout_details);
+ layout_line.x_for_index(display_point.column() as usize)
+ }
+
+ pub fn column_for_x(
+ &self,
+ display_row: u32,
+ x_coordinate: f32,
+ text_layout_details: &TextLayoutDetails,
+ ) -> u32 {
+ let layout_line = self.lay_out_line_for_row(display_row, text_layout_details);
+ layout_line.closest_index_for_x(x_coordinate) as u32
+ }
+
+ pub fn chars_at(
+ &self,
+ mut point: DisplayPoint,
+ ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
+ point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
+ 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
+ }
+ })
+ .map(move |ch| {
+ let result = (ch, point);
+ if ch == '\n' {
+ *point.row_mut() += 1;
+ *point.column_mut() = 0;
+ } else {
+ *point.column_mut() += ch.len_utf8() as u32;
+ }
+ result
+ })
+ }
+
+ pub fn reverse_chars_at(
+ &self,
+ mut point: DisplayPoint,
+ ) -> impl Iterator<Item = (char, DisplayPoint)> + '_ {
+ point = DisplayPoint(self.block_snapshot.clip_point(point.0, Bias::Left));
+ self.reverse_text_chunks(point.row())
+ .flat_map(|chunk| chunk.chars().rev())
+ .skip_while({
+ let mut column = self.line_len(point.row());
+ if self.max_point().row() > point.row() {
+ column += 1;
+ }
+
+ move |char| {
+ let at_point = column <= point.column();
+ column = column.saturating_sub(char.len_utf8() as u32);
+ !at_point
+ }
+ })
+ .map(move |ch| {
+ if ch == '\n' {
+ *point.row_mut() -= 1;
+ *point.column_mut() = self.line_len(point.row());
+ } else {
+ *point.column_mut() = point.column().saturating_sub(ch.len_utf8() as u32);
+ }
+ (ch, point)
+ })
+ }
+
+ pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
+ let mut count = 0;
+ let mut column = 0;
+ for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
+ if column >= target {
+ break;
+ }
+ count += 1;
+ column += c.len_utf8() as u32;
+ }
+ count
+ }
+
+ pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
+ let mut column = 0;
+
+ for (count, (c, _)) in self.chars_at(DisplayPoint::new(display_row, 0)).enumerate() {
+ if c == '\n' || count >= char_count as usize {
+ break;
+ }
+ column += c.len_utf8() as u32;
+ }
+
+ column
+ }
+
+ 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_at_line_end(&self, point: DisplayPoint) -> DisplayPoint {
+ let mut point = point.0;
+ if point.column == self.line_len(point.row) {
+ point.column = point.column.saturating_sub(1);
+ point = self.block_snapshot.clip_point(point, Bias::Left);
+ }
+ DisplayPoint(point)
+ }
+
+ pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+ where
+ T: ToOffset,
+ {
+ self.fold_snapshot.folds_in_range(range)
+ }
+
+ pub fn blocks_in_range(
+ &self,
+ rows: Range<u32>,
+ ) -> impl Iterator<Item = (u32, &TransformBlock)> {
+ self.block_snapshot.blocks_in_range(rows)
+ }
+
+ pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
+ self.fold_snapshot.intersects_fold(offset)
+ }
+
+ pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+ self.fold_snapshot.is_line_folded(buffer_row)
+ }
+
+ pub fn is_block_line(&self, display_row: u32) -> bool {
+ self.block_snapshot.is_block_line(display_row)
+ }
+
+ pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
+ let wrap_row = self
+ .block_snapshot
+ .to_wrap_point(BlockPoint::new(display_row, 0))
+ .row();
+ self.wrap_snapshot.soft_wrap_indent(wrap_row)
+ }
+
+ pub fn text(&self) -> String {
+ self.text_chunks(0).collect()
+ }
+
+ pub fn line(&self, display_row: u32) -> 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(&self, display_row: u32) -> (u32, bool) {
+ let mut indent = 0;
+ let mut is_blank = true;
+ for (c, _) in self.chars_at(DisplayPoint::new(display_row, 0)) {
+ if c == ' ' {
+ indent += 1;
+ } else {
+ is_blank = c == '\n';
+ break;
+ }
+ }
+ (indent, is_blank)
+ }
+
+ pub fn line_indent_for_buffer_row(&self, buffer_row: u32) -> (u32, bool) {
+ let (buffer, range) = self
+ .buffer_snapshot
+ .buffer_line_for_row(buffer_row)
+ .unwrap();
+
+ let mut indent_size = 0;
+ let mut is_blank = false;
+ for c in buffer.chars_at(Point::new(range.start.row, 0)) {
+ if c == ' ' || c == '\t' {
+ indent_size += 1;
+ } else {
+ if c == '\n' {
+ is_blank = true;
+ }
+ break;
+ }
+ }
+
+ (indent_size, is_blank)
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ self.block_snapshot.line_len(row)
+ }
+
+ pub fn longest_row(&self) -> u32 {
+ self.block_snapshot.longest_row()
+ }
+
+ pub fn fold_for_line(self: &Self, buffer_row: u32) -> Option<FoldStatus> {
+ if self.is_line_folded(buffer_row) {
+ Some(FoldStatus::Folded)
+ } else if self.is_foldable(buffer_row) {
+ Some(FoldStatus::Foldable)
+ } else {
+ None
+ }
+ }
+
+ pub fn is_foldable(self: &Self, buffer_row: u32) -> bool {
+ let max_row = self.buffer_snapshot.max_buffer_row();
+ if buffer_row >= max_row {
+ return false;
+ }
+
+ let (indent_size, is_blank) = self.line_indent_for_buffer_row(buffer_row);
+ if is_blank {
+ return false;
+ }
+
+ for next_row in (buffer_row + 1)..=max_row {
+ let (next_indent_size, next_line_is_blank) = self.line_indent_for_buffer_row(next_row);
+ if next_indent_size > indent_size {
+ return true;
+ } else if !next_line_is_blank {
+ break;
+ }
+ }
+
+ false
+ }
+
+ pub fn foldable_range(self: &Self, buffer_row: u32) -> Option<Range<Point>> {
+ let start = Point::new(buffer_row, self.buffer_snapshot.line_len(buffer_row));
+ if self.is_foldable(start.row) && !self.is_line_folded(start.row) {
+ let (start_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 + 1)..=max_point.row {
+ let (indent, is_blank) = self.line_indent_for_buffer_row(row);
+ if !is_blank && indent <= start_indent {
+ let prev_row = row - 1;
+ end = Some(Point::new(
+ prev_row,
+ self.buffer_snapshot.line_len(prev_row),
+ ));
+ break;
+ }
+ }
+ let end = end.unwrap_or(max_point);
+ Some(start..end)
+ } else {
+ None
+ }
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn text_highlight_ranges<Tag: ?Sized + 'static>(
+ &self,
+ ) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
+ let type_id = TypeId::of::<Tag>();
+ self.text_highlights.get(&Some(type_id)).cloned()
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn inlay_highlights<Tag: ?Sized + 'static>(
+ &self,
+ ) -> Option<&HashMap<InlayId, (HighlightStyle, InlayHighlight)>> {
+ let type_id = TypeId::of::<Tag>();
+ self.inlay_highlights.get(&type_id)
+ }
+}
+
+#[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(),
+ self.column()
+ ))
+ }
+}
+
+impl DisplayPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(BlockPoint(Point::new(row, column)))
+ }
+
+ pub fn zero() -> Self {
+ Self::new(0, 0)
+ }
+
+ pub fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+
+ pub fn row(self) -> u32 {
+ 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) -> usize {
+ let wrap_point = map.block_snapshot.to_wrap_point(self.0);
+ let tab_point = map.wrap_snapshot.to_tab_point(wrap_point);
+ let fold_point = map.tab_snapshot.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 usize {
+ fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint {
+ map.point_to_display_point(self.to_point(&map.buffer_snapshot), Bias::Left)
+ }
+}
+
+impl ToDisplayPoint for OffsetUtf16 {
+ 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)
+ }
+}
+
+pub fn next_rows(display_row: u32, display_map: &DisplaySnapshot) -> impl Iterator<Item = u32> {
+ let max_row = display_map.max_point().row();
+ let start_row = display_row + 1;
+ let mut current = None;
+ std::iter::from_fn(move || {
+ if current == None {
+ current = Some(start_row);
+ } else {
+ current = Some(current.unwrap() + 1)
+ }
+ if current.unwrap() > max_row {
+ None
+ } else {
+ current
+ }
+ })
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::{
+ movement,
+ test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+ };
+ use gpui::{color::Color, elements::*, test::observe, AppContext};
+ use language::{
+ language_settings::{AllLanguageSettings, AllLanguageSettingsContent},
+ Buffer, Language, LanguageConfig, SelectionGoal,
+ };
+ use project::Project;
+ use rand::{prelude::*, Rng};
+ use settings::SettingsStore;
+ use smol::stream::StreamExt;
+ use std::{env, sync::Arc};
+ use theme::SyntaxTheme;
+ use util::test::{marked_text_ranges, sample_text};
+ use Bias::*;
+
+ #[gpui::test(iterations = 100)]
+ async fn test_random_display_map(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+ cx.foreground().set_block_on_ticks(0..=50);
+ cx.foreground().forbid_parking();
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let font_cache = cx.font_cache().clone();
+ let mut tab_size = rng.gen_range(1..=4);
+ let buffer_start_excerpt_header_height = rng.gen_range(1..=5);
+ let excerpt_header_height = rng.gen_range(1..=5);
+ let family_id = font_cache
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let max_wrap_width = 300.0;
+ let mut wrap_width = if rng.gen_bool(0.1) {
+ None
+ } else {
+ Some(rng.gen_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.defaults.tab_size = NonZeroU32::new(tab_size));
+ });
+
+ let buffer = cx.update(|cx| {
+ if rng.gen() {
+ let len = rng.gen_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 map = cx.add_model(|cx| {
+ DisplayMap::new(
+ buffer.clone(),
+ font_id,
+ font_size,
+ wrap_width,
+ buffer_start_excerpt_header_height,
+ excerpt_header_height,
+ 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.gen_range(0..100) {
+ 0..=19 => {
+ wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(rng.gen_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::<AllLanguageSettings>(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(tab_size);
+ });
+ });
+ });
+ }
+ 30..=44 => {
+ map.update(cx, |map, cx| {
+ if rng.gen() || blocks.is_empty() {
+ let buffer = map.snapshot(cx).buffer_snapshot;
+ let block_properties = (0..rng.gen_range(1..=1))
+ .map(|_| {
+ let position =
+ buffer.anchor_after(buffer.clip_offset(
+ rng.gen_range(0..=buffer.len()),
+ Bias::Left,
+ ));
+
+ let disposition = if rng.gen() {
+ BlockDisposition::Above
+ } else {
+ BlockDisposition::Below
+ };
+ let height = rng.gen_range(1..5);
+ log::info!(
+ "inserting block {:?} {:?} with height {}",
+ disposition,
+ position.to_point(&buffer),
+ height
+ );
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position,
+ height,
+ disposition,
+ render: Arc::new(|_| Empty::new().into_any()),
+ }
+ })
+ .collect::<Vec<_>>();
+ blocks.extend(map.insert_blocks(block_properties, cx));
+ } else {
+ blocks.shuffle(&mut rng);
+ let remove_count = rng.gen_range(1..=4.min(blocks.len()));
+ let block_ids_to_remove = (0..remove_count)
+ .map(|_| blocks.remove(rng.gen_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.gen_range(1..=3) {
+ buffer.read_with(cx, |buffer, cx| {
+ let buffer = buffer.read(cx);
+ let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+ let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+ ranges.push(start..end);
+ });
+ }
+
+ if rng.gen() && fold_count > 0 {
+ log::info!("unfolding ranges: {:?}", ranges);
+ map.update(cx, |map, cx| {
+ map.unfold(ranges, true, cx);
+ });
+ } else {
+ log::info!("folding ranges: {:?}", ranges);
+ map.update(cx, |map, cx| {
+ map.fold(ranges, 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.gen_range(0..=buffer.max_point().row);
+ let column = rng.gen_range(0..=buffer.line_len(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(0, 0), Left);
+ let max_point = snapshot.clip_point(snapshot.max_point(), Right);
+ for _ in 0..5 {
+ let row = rng.gen_range(0..=snapshot.max_point().row());
+ let column = rng.gen_range(0..=snapshot.line_len(row));
+ let point = snapshot.clip_point(DisplayPoint::new(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.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+ cx.update(|cx| {
+ init_test(cx, |_| {});
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let editor = cx.editor.clone();
+ let window = cx.window.clone();
+
+ cx.update_window(window, |cx| {
+ let text_layout_details =
+ editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
+
+ let font_cache = cx.font_cache().clone();
+
+ let family_id = font_cache
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 12.0;
+ let wrap_width = Some(64.);
+
+ let text = "one two three four five\nsix seven eight";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let map = cx.add_model(|cx| {
+ DisplayMap::new(buffer.clone(), font_id, font_size, wrap_width, 1, 1, cx)
+ });
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(0).collect::<String>(),
+ "one two \nthree four \nfive\nsix seven \neight"
+ );
+ assert_eq!(
+ snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
+ DisplayPoint::new(0, 7)
+ );
+ assert_eq!(
+ snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
+ DisplayPoint::new(1, 0)
+ );
+ assert_eq!(
+ movement::right(&snapshot, DisplayPoint::new(0, 7)),
+ DisplayPoint::new(1, 0)
+ );
+ assert_eq!(
+ movement::left(&snapshot, DisplayPoint::new(1, 0)),
+ DisplayPoint::new(0, 7)
+ );
+
+ let x = snapshot.x_for_point(DisplayPoint::new(1, 10), &text_layout_details);
+ assert_eq!(
+ movement::up(
+ &snapshot,
+ DisplayPoint::new(1, 10),
+ SelectionGoal::None,
+ false,
+ &text_layout_details,
+ ),
+ (
+ DisplayPoint::new(0, 7),
+ SelectionGoal::HorizontalPosition(x)
+ )
+ );
+ assert_eq!(
+ movement::down(
+ &snapshot,
+ DisplayPoint::new(0, 7),
+ SelectionGoal::HorizontalPosition(x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(1, 10),
+ SelectionGoal::HorizontalPosition(x)
+ )
+ );
+ assert_eq!(
+ movement::down(
+ &snapshot,
+ DisplayPoint::new(1, 10),
+ SelectionGoal::HorizontalPosition(x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 4),
+ SelectionGoal::HorizontalPosition(x)
+ )
+ );
+
+ let ix = 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(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_id, font_size + 3., cx));
+
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(
+ snapshot.text_chunks(1).collect::<String>(),
+ "three \nfour five\nsix and \nseven \neight"
+ )
+ });
+ }
+
+ #[gpui::test]
+ fn test_text_chunks(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ let text = sample_text(6, 6, 'a');
+ let buffer = MultiBuffer::build_simple(&text, cx);
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ vec![
+ (Point::new(1, 0)..Point::new(1, 0), "\t"),
+ (Point::new(1, 1)..Point::new(1, 1), "\t"),
+ (Point::new(2, 1)..Point::new(2, 1), "\t"),
+ ],
+ None,
+ cx,
+ )
+ });
+
+ assert_eq!(
+ map.update(cx, |map, cx| map.snapshot(cx))
+ .text_chunks(1)
+ .collect::<String>()
+ .lines()
+ .next(),
+ Some(" b bbbbb")
+ );
+ assert_eq!(
+ map.update(cx, |map, cx| map.snapshot(cx))
+ .text_chunks(2)
+ .collect::<String>()
+ .lines()
+ .next(),
+ Some("c ccccc")
+ );
+ }
+
+ #[gpui::test]
+ async fn test_chunks(cx: &mut gpui::TestAppContext) {
+ use unindent::Unindent as _;
+
+ let text = r#"
+ fn outer() {}
+
+ mod module {
+ fn inner() {}
+ }"#
+ .unindent();
+
+ let theme = SyntaxTheme::new(vec![
+ ("mod.body".to_string(), Color::red().into()),
+ ("fn.name".to_string(), Color::blue().into()),
+ ]);
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Test".into(),
+ path_suffixes: vec![".test".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .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.defaults.tab_size = Some(2.try_into().unwrap())));
+
+ let buffer = cx
+ .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+ let font_cache = cx.font_cache();
+ let family_id = font_cache
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
+ let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+ vec![
+ ("fn ".to_string(), None),
+ ("outer".to_string(), Some(Color::blue())),
+ ("() {}\n\nmod module ".to_string(), None),
+ ("{\n fn ".to_string(), Some(Color::red())),
+ ("inner".to_string(), Some(Color::blue())),
+ ("() {}\n}".to_string(), Some(Color::red())),
+ ]
+ );
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+ vec![
+ (" fn ".to_string(), Some(Color::red())),
+ ("inner".to_string(), Some(Color::blue())),
+ ("() {}\n}".to_string(), Some(Color::red())),
+ ]
+ );
+
+ map.update(cx, |map, cx| {
+ map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+ });
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)),
+ vec![
+ ("fn ".to_string(), None),
+ ("out".to_string(), Some(Color::blue())),
+ ("โฏ".to_string(), None),
+ (" fn ".to_string(), Some(Color::red())),
+ ("inner".to_string(), Some(Color::blue())),
+ ("() {}\n}".to_string(), Some(Color::red())),
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_chunks_with_soft_wrapping(cx: &mut gpui::TestAppContext) {
+ use unindent::Unindent as _;
+
+ cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
+
+ let text = r#"
+ fn outer() {}
+
+ mod module {
+ fn inner() {}
+ }"#
+ .unindent();
+
+ let theme = SyntaxTheme::new(vec![
+ ("mod.body".to_string(), Color::red().into()),
+ ("fn.name".to_string(), Color::blue().into()),
+ ]);
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Test".into(),
+ path_suffixes: vec![".test".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .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
+ .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+
+ let font_cache = cx.font_cache();
+
+ let family_id = font_cache
+ .load_family(&["Courier"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 16.0;
+
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, Some(40.0), 1, 1, cx));
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)),
+ [
+ ("fn \n".to_string(), None),
+ ("oute\nr".to_string(), Some(Color::blue())),
+ ("() \n{}\n\n".to_string(), None),
+ ]
+ );
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)),
+ [("{}\n\n".to_string(), None)]
+ );
+
+ map.update(cx, |map, cx| {
+ map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
+ });
+ assert_eq!(
+ cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)),
+ [
+ ("out".to_string(), Some(Color::blue())),
+ ("โฏ\n".to_string(), None),
+ (" \nfn ".to_string(), Some(Color::red())),
+ ("i\n".to_string(), Some(Color::blue()))
+ ]
+ );
+ }
+
+ #[gpui::test]
+ async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| init_test(cx, |_| {}));
+
+ let theme = SyntaxTheme::new(vec![
+ ("operator".to_string(), Color::red().into()),
+ ("string".to_string(), Color::green().into()),
+ ]);
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Test".into(),
+ path_suffixes: vec![".test".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .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
+ .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ buffer.condition(cx, |buf, _| !buf.is_parsing()).await;
+
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+
+ let font_cache = cx.font_cache();
+ let family_id = font_cache
+ .load_family(&["Courier"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 16.0;
+ let map = cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+
+ enum MyType {}
+
+ let style = HighlightStyle {
+ color: Some(Color::blue()),
+ ..Default::default()
+ };
+
+ map.update(cx, |map, _cx| {
+ map.highlight_text(
+ TypeId::of::<MyType>(),
+ highlighted_ranges
+ .into_iter()
+ .map(|range| {
+ buffer_snapshot.anchor_before(range.start)
+ ..buffer_snapshot.anchor_before(range.end)
+ })
+ .collect(),
+ style,
+ );
+ });
+
+ assert_eq!(
+ cx.update(|cx| chunks(0..10, &map, &theme, cx)),
+ [
+ ("const ".to_string(), None, None),
+ ("a".to_string(), None, Some(Color::blue())),
+ (":".to_string(), Some(Color::red()), None),
+ (" B = ".to_string(), None, None),
+ ("\"c ".to_string(), Some(Color::green()), None),
+ ("d".to_string(), Some(Color::green()), Some(Color::blue())),
+ ("\"".to_string(), Some(Color::green()), None),
+ ]
+ );
+ }
+
+ #[gpui::test]
+ fn test_clip_point(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::AppContext) {
+ 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::AppContext) {
+ init_test(cx, |_| {});
+
+ fn assert(text: &str, cx: &mut gpui::AppContext) {
+ 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_tabs_with_multibyte_chars(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ let text = "โ
\t\tฮฑ\nฮฒ\t\n๐ฮฒ\t\tฮณ";
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let font_cache = cx.font_cache();
+ let family_id = font_cache
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+ let map = map.update(cx, |map, cx| map.snapshot(cx));
+ assert_eq!(map.text(), "โ
ฮฑ\nฮฒ \n๐ฮฒ ฮณ");
+ assert_eq!(
+ map.text_chunks(0).collect::<String>(),
+ "โ
ฮฑ\nฮฒ \n๐ฮฒ ฮณ"
+ );
+ assert_eq!(map.text_chunks(1).collect::<String>(), "ฮฒ \n๐ฮฒ ฮณ");
+ assert_eq!(map.text_chunks(2).collect::<String>(), "๐ฮฒ ฮณ");
+
+ let point = Point::new(0, "โ
\t\t".len() as u32);
+ let display_point = DisplayPoint::new(0, "โ
".len() as u32);
+ assert_eq!(point.to_display_point(&map), display_point);
+ assert_eq!(display_point.to_point(&map), point);
+
+ let point = Point::new(1, "ฮฒ\t".len() as u32);
+ let display_point = DisplayPoint::new(1, "ฮฒ ".len() as u32);
+ assert_eq!(point.to_display_point(&map), display_point);
+ assert_eq!(display_point.to_point(&map), point,);
+
+ let point = Point::new(2, "๐ฮฒ\t\t".len() as u32);
+ let display_point = DisplayPoint::new(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(0, "โ
".len() as u32).to_point(&map),
+ Point::new(0, "โ
\t".len() as u32),
+ );
+ assert_eq!(
+ DisplayPoint::new(0, "โ
".len() as u32).to_point(&map),
+ Point::new(0, "โ
".len() as u32),
+ );
+
+ // Clipping display points inside of multi-byte characters
+ assert_eq!(
+ map.clip_point(DisplayPoint::new(0, "โ
".len() as u32 - 1), Left),
+ DisplayPoint::new(0, 0)
+ );
+ assert_eq!(
+ map.clip_point(DisplayPoint::new(0, "โ
".len() as u32 - 1), Bias::Right),
+ DisplayPoint::new(0, "โ
".len() as u32)
+ );
+ }
+
+ #[gpui::test]
+ fn test_max_point(cx: &mut gpui::AppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = MultiBuffer::build_simple("aaa\n\t\tbbb", cx);
+ let font_cache = cx.font_cache();
+ let family_id = font_cache
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let map =
+ cx.add_model(|cx| DisplayMap::new(buffer.clone(), font_id, font_size, None, 1, 1, cx));
+ assert_eq!(
+ map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
+ DisplayPoint::new(1, 11)
+ )
+ }
+
+ fn syntax_chunks<'a>(
+ rows: Range<u32>,
+ map: &ModelHandle<DisplayMap>,
+ theme: &'a SyntaxTheme,
+ cx: &mut AppContext,
+ ) -> Vec<(String, Option<Color>)> {
+ chunks(rows, map, theme, cx)
+ .into_iter()
+ .map(|(text, color, _)| (text, color))
+ .collect()
+ }
+
+ fn chunks<'a>(
+ rows: Range<u32>,
+ map: &ModelHandle<DisplayMap>,
+ theme: &'a SyntaxTheme,
+ cx: &mut AppContext,
+ ) -> Vec<(String, Option<Color>, Option<Color>)> {
+ let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
+ let mut chunks: Vec<(String, Option<Color>, Option<Color>)> = Vec::new();
+ for chunk in snapshot.chunks(rows, true, None, None) {
+ 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() {
+ if 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 AppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+ cx.foreground().forbid_parking();
+ cx.set_global(SettingsStore::test(cx));
+ language::init(cx);
+ crate::init(cx);
+ Project::init_settings(cx);
+ theme::init((), cx);
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, f);
+ });
+ }
+}
@@ -0,0 +1,1667 @@
+use super::{
+ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot},
+ Highlights,
+};
+use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _};
+use collections::{Bound, HashMap, HashSet};
+use gpui::{AnyElement, ViewContext};
+use language::{BufferSnapshot, Chunk, Patch, Point};
+use parking_lot::Mutex;
+use std::{
+ cell::RefCell,
+ cmp::{self, Ordering},
+ fmt::Debug,
+ ops::{Deref, DerefMut, Range},
+ sync::{
+ atomic::{AtomicUsize, Ordering::SeqCst},
+ Arc,
+ },
+};
+use sum_tree::{Bias, SumTree};
+use text::Edit;
+
+const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
+
+pub struct BlockMap {
+ next_block_id: AtomicUsize,
+ wrap_snapshot: RefCell<WrapSnapshot>,
+ blocks: Vec<Arc<Block>>,
+ transforms: RefCell<SumTree<Transform>>,
+ buffer_header_height: u8,
+ excerpt_header_height: u8,
+}
+
+pub struct BlockMapWriter<'a>(&'a mut BlockMap);
+
+pub struct BlockSnapshot {
+ wrap_snapshot: WrapSnapshot,
+ transforms: SumTree<Transform>,
+}
+
+#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct BlockId(usize);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct BlockPoint(pub Point);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+struct BlockRow(u32);
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+struct WrapRow(u32);
+
+pub type RenderBlock = Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>;
+
+pub struct Block {
+ id: BlockId,
+ position: Anchor,
+ height: u8,
+ style: BlockStyle,
+ render: Mutex<RenderBlock>,
+ disposition: BlockDisposition,
+}
+
+#[derive(Clone)]
+pub struct BlockProperties<P>
+where
+ P: Clone,
+{
+ pub position: P,
+ pub height: u8,
+ pub style: BlockStyle,
+ pub render: Arc<dyn Fn(&mut BlockContext) -> AnyElement<Editor>>,
+ pub disposition: BlockDisposition,
+}
+
+#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd)]
+pub enum BlockStyle {
+ Fixed,
+ Flex,
+ Sticky,
+}
+
+pub struct BlockContext<'a, 'b, 'c> {
+ pub view_context: &'c mut ViewContext<'a, 'b, Editor>,
+ pub anchor_x: f32,
+ pub scroll_x: f32,
+ pub gutter_width: f32,
+ pub gutter_padding: f32,
+ pub em_width: f32,
+ pub line_height: f32,
+ pub block_id: usize,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub enum BlockDisposition {
+ Above,
+ Below,
+}
+
+#[derive(Clone, Debug)]
+struct Transform {
+ summary: TransformSummary,
+ block: Option<TransformBlock>,
+}
+
+#[allow(clippy::large_enum_variant)]
+#[derive(Clone)]
+pub enum TransformBlock {
+ Custom(Arc<Block>),
+ ExcerptHeader {
+ id: ExcerptId,
+ buffer: BufferSnapshot,
+ range: ExcerptRange<text::Anchor>,
+ height: u8,
+ starts_new_buffer: bool,
+ },
+}
+
+impl TransformBlock {
+ fn disposition(&self) -> BlockDisposition {
+ match self {
+ TransformBlock::Custom(block) => block.disposition,
+ TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+ }
+ }
+
+ pub fn height(&self) -> u8 {
+ match self {
+ TransformBlock::Custom(block) => block.height,
+ TransformBlock::ExcerptHeader { height, .. } => *height,
+ }
+ }
+}
+
+impl Debug for TransformBlock {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(),
+ Self::ExcerptHeader { buffer, .. } => f
+ .debug_struct("ExcerptHeader")
+ .field("path", &buffer.file().map(|f| f.path()))
+ .finish(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+ input_rows: u32,
+ output_rows: u32,
+}
+
+pub struct BlockChunks<'a> {
+ transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
+ input_chunks: wrap_map::WrapChunks<'a>,
+ input_chunk: Chunk<'a>,
+ output_row: u32,
+ max_output_row: u32,
+}
+
+#[derive(Clone)]
+pub struct BlockBufferRows<'a> {
+ transforms: sum_tree::Cursor<'a, Transform, (BlockRow, WrapRow)>,
+ input_buffer_rows: wrap_map::WrapBufferRows<'a>,
+ output_row: u32,
+ started: bool,
+}
+
+impl BlockMap {
+ pub fn new(
+ wrap_snapshot: WrapSnapshot,
+ buffer_header_height: u8,
+ excerpt_header_height: u8,
+ ) -> Self {
+ let row_count = wrap_snapshot.max_point().row() + 1;
+ let map = Self {
+ next_block_id: AtomicUsize::new(0),
+ blocks: Vec::new(),
+ transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())),
+ wrap_snapshot: RefCell::new(wrap_snapshot.clone()),
+ buffer_header_height,
+ excerpt_header_height,
+ };
+ map.sync(
+ &wrap_snapshot,
+ Patch::new(vec![Edit {
+ old: 0..row_count,
+ new: 0..row_count,
+ }]),
+ );
+ map
+ }
+
+ pub fn read(&self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockSnapshot {
+ self.sync(&wrap_snapshot, edits);
+ *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone();
+ BlockSnapshot {
+ wrap_snapshot,
+ transforms: self.transforms.borrow().clone(),
+ }
+ }
+
+ pub fn write(&mut self, wrap_snapshot: WrapSnapshot, edits: Patch<u32>) -> BlockMapWriter {
+ self.sync(&wrap_snapshot, edits);
+ *self.wrap_snapshot.borrow_mut() = wrap_snapshot;
+ BlockMapWriter(self)
+ }
+
+ fn sync(&self, wrap_snapshot: &WrapSnapshot, mut edits: Patch<u32>) {
+ let buffer = wrap_snapshot.buffer_snapshot();
+
+ // Handle changing the last excerpt if it is empty.
+ if buffer.trailing_excerpt_update_count()
+ != self
+ .wrap_snapshot
+ .borrow()
+ .buffer_snapshot()
+ .trailing_excerpt_update_count()
+ {
+ let max_point = wrap_snapshot.max_point();
+ let edit_start = wrap_snapshot.prev_row_boundary(max_point);
+ let edit_end = max_point.row() + 1;
+ edits = edits.compose([WrapEdit {
+ old: edit_start..edit_end,
+ new: edit_start..edit_end,
+ }]);
+ }
+
+ let edits = edits.into_inner();
+ if edits.is_empty() {
+ return;
+ }
+
+ let mut transforms = self.transforms.borrow_mut();
+ let mut new_transforms = SumTree::new();
+ let old_row_count = transforms.summary().input_rows;
+ let new_row_count = wrap_snapshot.max_point().row() + 1;
+ let mut cursor = transforms.cursor::<WrapRow>();
+ let mut last_block_ix = 0;
+ let mut blocks_in_edit = Vec::new();
+ let mut edits = edits.into_iter().peekable();
+
+ while let Some(edit) = edits.next() {
+ // Preserve any old transforms that precede this edit.
+ let old_start = WrapRow(edit.old.start);
+ let new_start = WrapRow(edit.new.start);
+ new_transforms.append(cursor.slice(&old_start, Bias::Left, &()), &());
+ if let Some(transform) = cursor.item() {
+ if transform.is_isomorphic() && old_start == cursor.end(&()) {
+ new_transforms.push(transform.clone(), &());
+ cursor.next(&());
+ while let Some(transform) = cursor.item() {
+ if transform
+ .block
+ .as_ref()
+ .map_or(false, |b| b.disposition().is_below())
+ {
+ new_transforms.push(transform.clone(), &());
+ cursor.next(&());
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ // Preserve any portion of an old transform that precedes this edit.
+ let extent_before_edit = old_start.0 - cursor.start().0;
+ push_isomorphic(&mut new_transforms, extent_before_edit);
+
+ // Skip over any old transforms that intersect this edit.
+ let mut old_end = WrapRow(edit.old.end);
+ let mut new_end = WrapRow(edit.new.end);
+ cursor.seek(&old_end, Bias::Left, &());
+ cursor.next(&());
+ if old_end == *cursor.start() {
+ while let Some(transform) = cursor.item() {
+ if transform
+ .block
+ .as_ref()
+ .map_or(false, |b| b.disposition().is_below())
+ {
+ cursor.next(&());
+ } else {
+ break;
+ }
+ }
+ }
+
+ // Combine this edit with any subsequent edits that intersect the same transform.
+ while let Some(next_edit) = edits.peek() {
+ if next_edit.old.start <= cursor.start().0 {
+ old_end = WrapRow(next_edit.old.end);
+ new_end = WrapRow(next_edit.new.end);
+ cursor.seek(&old_end, Bias::Left, &());
+ cursor.next(&());
+ if old_end == *cursor.start() {
+ while let Some(transform) = cursor.item() {
+ if transform
+ .block
+ .as_ref()
+ .map_or(false, |b| b.disposition().is_below())
+ {
+ cursor.next(&());
+ } else {
+ break;
+ }
+ }
+ }
+ edits.next();
+ } else {
+ break;
+ }
+ }
+
+ // Find the blocks within this edited region.
+ let new_buffer_start =
+ wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left);
+ let start_bound = Bound::Included(new_buffer_start);
+ let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| {
+ probe
+ .position
+ .to_point(buffer)
+ .cmp(&new_buffer_start)
+ .then(Ordering::Greater)
+ }) {
+ Ok(ix) | Err(ix) => last_block_ix + ix,
+ };
+
+ let end_bound;
+ let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() {
+ end_bound = Bound::Unbounded;
+ self.blocks.len()
+ } else {
+ let new_buffer_end =
+ wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left);
+ end_bound = Bound::Excluded(new_buffer_end);
+ match self.blocks[start_block_ix..].binary_search_by(|probe| {
+ probe
+ .position
+ .to_point(buffer)
+ .cmp(&new_buffer_end)
+ .then(Ordering::Greater)
+ }) {
+ Ok(ix) | Err(ix) => start_block_ix + ix,
+ }
+ };
+ last_block_ix = end_block_ix;
+
+ debug_assert!(blocks_in_edit.is_empty());
+ blocks_in_edit.extend(
+ self.blocks[start_block_ix..end_block_ix]
+ .iter()
+ .map(|block| {
+ let mut position = block.position.to_point(buffer);
+ match block.disposition {
+ BlockDisposition::Above => position.column = 0,
+ BlockDisposition::Below => {
+ position.column = buffer.line_len(position.row)
+ }
+ }
+ let position = wrap_snapshot.make_wrap_point(position, Bias::Left);
+ (position.row(), TransformBlock::Custom(block.clone()))
+ }),
+ );
+ blocks_in_edit.extend(
+ buffer
+ .excerpt_boundaries_in_range((start_bound, end_bound))
+ .map(|excerpt_boundary| {
+ (
+ wrap_snapshot
+ .make_wrap_point(Point::new(excerpt_boundary.row, 0), Bias::Left)
+ .row(),
+ TransformBlock::ExcerptHeader {
+ id: excerpt_boundary.id,
+ buffer: excerpt_boundary.buffer,
+ range: excerpt_boundary.range,
+ height: if excerpt_boundary.starts_new_buffer {
+ self.buffer_header_height
+ } else {
+ self.excerpt_header_height
+ },
+ starts_new_buffer: excerpt_boundary.starts_new_buffer,
+ },
+ )
+ }),
+ );
+
+ // Place excerpt headers above custom blocks on the same row.
+ blocks_in_edit.sort_unstable_by(|(row_a, block_a), (row_b, block_b)| {
+ row_a.cmp(row_b).then_with(|| match (block_a, block_b) {
+ (
+ TransformBlock::ExcerptHeader { .. },
+ TransformBlock::ExcerptHeader { .. },
+ ) => Ordering::Equal,
+ (TransformBlock::ExcerptHeader { .. }, _) => Ordering::Less,
+ (_, TransformBlock::ExcerptHeader { .. }) => Ordering::Greater,
+ (TransformBlock::Custom(block_a), TransformBlock::Custom(block_b)) => block_a
+ .disposition
+ .cmp(&block_b.disposition)
+ .then_with(|| block_a.id.cmp(&block_b.id)),
+ })
+ });
+
+ // For each of these blocks, insert a new isomorphic transform preceding the block,
+ // and then insert the block itself.
+ for (block_row, block) in blocks_in_edit.drain(..) {
+ let insertion_row = match block.disposition() {
+ BlockDisposition::Above => block_row,
+ BlockDisposition::Below => block_row + 1,
+ };
+ let extent_before_block = insertion_row - new_transforms.summary().input_rows;
+ push_isomorphic(&mut new_transforms, extent_before_block);
+ new_transforms.push(Transform::block(block), &());
+ }
+
+ old_end = WrapRow(old_end.0.min(old_row_count));
+ new_end = WrapRow(new_end.0.min(new_row_count));
+
+ // Insert an isomorphic transform after the final block.
+ let extent_after_last_block = new_end.0 - new_transforms.summary().input_rows;
+ push_isomorphic(&mut new_transforms, extent_after_last_block);
+
+ // Preserve any portion of the old transform after this edit.
+ let extent_after_edit = cursor.start().0 - old_end.0;
+ push_isomorphic(&mut new_transforms, extent_after_edit);
+ }
+
+ new_transforms.append(cursor.suffix(&()), &());
+ debug_assert_eq!(
+ new_transforms.summary().input_rows,
+ wrap_snapshot.max_point().row() + 1
+ );
+
+ drop(cursor);
+ *transforms = new_transforms;
+ }
+
+ pub fn replace(&mut self, mut renderers: HashMap<BlockId, RenderBlock>) {
+ for block in &self.blocks {
+ if let Some(render) = renderers.remove(&block.id) {
+ *block.render.lock() = render;
+ }
+ }
+ }
+}
+
+fn push_isomorphic(tree: &mut SumTree<Transform>, rows: u32) {
+ if rows == 0 {
+ return;
+ }
+
+ let mut extent = Some(rows);
+ tree.update_last(
+ |last_transform| {
+ if last_transform.is_isomorphic() {
+ let extent = extent.take().unwrap();
+ last_transform.summary.input_rows += extent;
+ last_transform.summary.output_rows += extent;
+ }
+ },
+ &(),
+ );
+ if let Some(extent) = extent {
+ tree.push(Transform::isomorphic(extent), &());
+ }
+}
+
+impl BlockPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(Point::new(row, column))
+ }
+}
+
+impl Deref for BlockPoint {
+ type Target = Point;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl std::ops::DerefMut for BlockPoint {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl<'a> BlockMapWriter<'a> {
+ pub fn insert(
+ &mut self,
+ blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
+ ) -> Vec<BlockId> {
+ let mut ids = Vec::new();
+ let mut edits = Patch::default();
+ let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
+ let buffer = wrap_snapshot.buffer_snapshot();
+
+ for block in blocks {
+ let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst));
+ ids.push(id);
+
+ let position = block.position;
+ let point = position.to_point(buffer);
+ let wrap_row = wrap_snapshot
+ .make_wrap_point(Point::new(point.row, 0), Bias::Left)
+ .row();
+ let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
+ let end_row = wrap_snapshot
+ .next_row_boundary(WrapPoint::new(wrap_row, 0))
+ .unwrap_or(wrap_snapshot.max_point().row() + 1);
+
+ let block_ix = match self
+ .0
+ .blocks
+ .binary_search_by(|probe| probe.position.cmp(&position, buffer))
+ {
+ Ok(ix) | Err(ix) => ix,
+ };
+ self.0.blocks.insert(
+ block_ix,
+ Arc::new(Block {
+ id,
+ position,
+ height: block.height,
+ render: Mutex::new(block.render),
+ disposition: block.disposition,
+ style: block.style,
+ }),
+ );
+
+ edits = edits.compose([Edit {
+ old: start_row..end_row,
+ new: start_row..end_row,
+ }]);
+ }
+
+ self.0.sync(wrap_snapshot, edits);
+ ids
+ }
+
+ pub fn remove(&mut self, block_ids: HashSet<BlockId>) {
+ let wrap_snapshot = &*self.0.wrap_snapshot.borrow();
+ let buffer = wrap_snapshot.buffer_snapshot();
+ let mut edits = Patch::default();
+ let mut last_block_buffer_row = None;
+ self.0.blocks.retain(|block| {
+ if block_ids.contains(&block.id) {
+ let buffer_row = block.position.to_point(buffer).row;
+ if last_block_buffer_row != Some(buffer_row) {
+ last_block_buffer_row = Some(buffer_row);
+ let wrap_row = wrap_snapshot
+ .make_wrap_point(Point::new(buffer_row, 0), Bias::Left)
+ .row();
+ let start_row = wrap_snapshot.prev_row_boundary(WrapPoint::new(wrap_row, 0));
+ let end_row = wrap_snapshot
+ .next_row_boundary(WrapPoint::new(wrap_row, 0))
+ .unwrap_or(wrap_snapshot.max_point().row() + 1);
+ edits.push(Edit {
+ old: start_row..end_row,
+ new: start_row..end_row,
+ })
+ }
+ false
+ } else {
+ true
+ }
+ });
+ self.0.sync(wrap_snapshot, edits);
+ }
+}
+
+impl BlockSnapshot {
+ #[cfg(test)]
+ pub fn text(&self) -> String {
+ self.chunks(
+ 0..self.transforms.summary().output_rows,
+ false,
+ Highlights::default(),
+ )
+ .map(|chunk| chunk.text)
+ .collect()
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ rows: Range<u32>,
+ language_aware: bool,
+ highlights: Highlights<'a>,
+ ) -> BlockChunks<'a> {
+ let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
+ let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+ let input_end = {
+ cursor.seek(&BlockRow(rows.end), Bias::Right, &());
+ let overshoot = if cursor
+ .item()
+ .map_or(false, |transform| transform.is_isomorphic())
+ {
+ rows.end - cursor.start().0 .0
+ } else {
+ 0
+ };
+ cursor.start().1 .0 + overshoot
+ };
+ let input_start = {
+ cursor.seek(&BlockRow(rows.start), Bias::Right, &());
+ let overshoot = if cursor
+ .item()
+ .map_or(false, |transform| transform.is_isomorphic())
+ {
+ rows.start - cursor.start().0 .0
+ } else {
+ 0
+ };
+ cursor.start().1 .0 + overshoot
+ };
+ BlockChunks {
+ input_chunks: self.wrap_snapshot.chunks(
+ input_start..input_end,
+ language_aware,
+ highlights,
+ ),
+ input_chunk: Default::default(),
+ transforms: cursor,
+ output_row: rows.start,
+ max_output_row,
+ }
+ }
+
+ pub fn buffer_rows(&self, start_row: u32) -> BlockBufferRows {
+ let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+ cursor.seek(&BlockRow(start_row), Bias::Right, &());
+ let (output_start, input_start) = cursor.start();
+ let overshoot = if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+ start_row - output_start.0
+ } else {
+ 0
+ };
+ let input_start_row = input_start.0 + overshoot;
+ BlockBufferRows {
+ transforms: cursor,
+ input_buffer_rows: self.wrap_snapshot.buffer_rows(input_start_row),
+ output_row: start_row,
+ started: false,
+ }
+ }
+
+ pub fn blocks_in_range(
+ &self,
+ rows: Range<u32>,
+ ) -> impl Iterator<Item = (u32, &TransformBlock)> {
+ let mut cursor = self.transforms.cursor::<BlockRow>();
+ cursor.seek(&BlockRow(rows.start), Bias::Right, &());
+ std::iter::from_fn(move || {
+ while let Some(transform) = cursor.item() {
+ let start_row = cursor.start().0;
+ if start_row >= rows.end {
+ break;
+ }
+ if let Some(block) = &transform.block {
+ cursor.next(&());
+ return Some((start_row, block));
+ } else {
+ cursor.next(&());
+ }
+ }
+ None
+ })
+ }
+
+ pub fn max_point(&self) -> BlockPoint {
+ let row = self.transforms.summary().output_rows - 1;
+ BlockPoint::new(row, self.line_len(row))
+ }
+
+ pub fn longest_row(&self) -> u32 {
+ let input_row = self.wrap_snapshot.longest_row();
+ self.to_block_point(WrapPoint::new(input_row, 0)).row
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+ cursor.seek(&BlockRow(row), Bias::Right, &());
+ if let Some(transform) = cursor.item() {
+ let (output_start, input_start) = cursor.start();
+ let overshoot = row - output_start.0;
+ if transform.block.is_some() {
+ 0
+ } else {
+ self.wrap_snapshot.line_len(input_start.0 + overshoot)
+ }
+ } else {
+ panic!("row out of range");
+ }
+ }
+
+ pub fn is_block_line(&self, row: u32) -> bool {
+ let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+ cursor.seek(&BlockRow(row), Bias::Right, &());
+ cursor.item().map_or(false, |t| t.block.is_some())
+ }
+
+ pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint {
+ let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+ cursor.seek(&BlockRow(point.row), Bias::Right, &());
+
+ let max_input_row = WrapRow(self.transforms.summary().input_rows);
+ let mut search_left =
+ (bias == Bias::Left && cursor.start().1 .0 > 0) || cursor.end(&()).1 == max_input_row;
+ let mut reversed = false;
+
+ loop {
+ if let Some(transform) = cursor.item() {
+ if transform.is_isomorphic() {
+ let (output_start_row, input_start_row) = cursor.start();
+ let (output_end_row, input_end_row) = cursor.end(&());
+ let output_start = Point::new(output_start_row.0, 0);
+ let input_start = Point::new(input_start_row.0, 0);
+ let input_end = Point::new(input_end_row.0, 0);
+ let input_point = if point.row >= output_end_row.0 {
+ let line_len = self.wrap_snapshot.line_len(input_end_row.0 - 1);
+ self.wrap_snapshot
+ .clip_point(WrapPoint::new(input_end_row.0 - 1, line_len), bias)
+ } else {
+ let output_overshoot = point.0.saturating_sub(output_start);
+ self.wrap_snapshot
+ .clip_point(WrapPoint(input_start + output_overshoot), bias)
+ };
+
+ if (input_start..input_end).contains(&input_point.0) {
+ let input_overshoot = input_point.0.saturating_sub(input_start);
+ return BlockPoint(output_start + input_overshoot);
+ }
+ }
+
+ if search_left {
+ cursor.prev(&());
+ } else {
+ cursor.next(&());
+ }
+ } else if reversed {
+ return self.max_point();
+ } else {
+ reversed = true;
+ search_left = !search_left;
+ cursor.seek(&BlockRow(point.row), Bias::Right, &());
+ }
+ }
+ }
+
+ pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint {
+ let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>();
+ cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &());
+ if let Some(transform) = cursor.item() {
+ debug_assert!(transform.is_isomorphic());
+ } else {
+ return self.max_point();
+ }
+
+ let (input_start_row, output_start_row) = cursor.start();
+ let input_start = Point::new(input_start_row.0, 0);
+ let output_start = Point::new(output_start_row.0, 0);
+ let input_overshoot = wrap_point.0 - input_start;
+ BlockPoint(output_start + input_overshoot)
+ }
+
+ pub fn to_wrap_point(&self, block_point: BlockPoint) -> WrapPoint {
+ let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
+ cursor.seek(&BlockRow(block_point.row), Bias::Right, &());
+ if let Some(transform) = cursor.item() {
+ match transform.block.as_ref().map(|b| b.disposition()) {
+ Some(BlockDisposition::Above) => WrapPoint::new(cursor.start().1 .0, 0),
+ Some(BlockDisposition::Below) => {
+ let wrap_row = cursor.start().1 .0 - 1;
+ WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row))
+ }
+ None => {
+ let overshoot = block_point.row - cursor.start().0 .0;
+ let wrap_row = cursor.start().1 .0 + overshoot;
+ WrapPoint::new(wrap_row, block_point.column)
+ }
+ }
+ } else {
+ self.wrap_snapshot.max_point()
+ }
+ }
+}
+
+impl Transform {
+ fn isomorphic(rows: u32) -> Self {
+ Self {
+ summary: TransformSummary {
+ input_rows: rows,
+ output_rows: rows,
+ },
+ block: None,
+ }
+ }
+
+ fn block(block: TransformBlock) -> Self {
+ Self {
+ summary: TransformSummary {
+ input_rows: 0,
+ output_rows: block.height() as u32,
+ },
+ block: Some(block),
+ }
+ }
+
+ fn is_isomorphic(&self) -> bool {
+ self.block.is_none()
+ }
+}
+
+impl<'a> Iterator for BlockChunks<'a> {
+ type Item = Chunk<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.output_row >= self.max_output_row {
+ return None;
+ }
+
+ let transform = self.transforms.item()?;
+ if transform.block.is_some() {
+ let block_start = self.transforms.start().0 .0;
+ let mut block_end = self.transforms.end(&()).0 .0;
+ self.transforms.next(&());
+ if self.transforms.item().is_none() {
+ block_end -= 1;
+ }
+
+ let start_in_block = self.output_row - block_start;
+ let end_in_block = cmp::min(self.max_output_row, block_end) - block_start;
+ let line_count = end_in_block - start_in_block;
+ self.output_row += line_count;
+
+ return Some(Chunk {
+ text: unsafe { std::str::from_utf8_unchecked(&NEWLINES[..line_count as usize]) },
+ ..Default::default()
+ });
+ }
+
+ if self.input_chunk.text.is_empty() {
+ if let Some(input_chunk) = self.input_chunks.next() {
+ self.input_chunk = input_chunk;
+ } else {
+ self.output_row += 1;
+ if self.output_row < self.max_output_row {
+ self.transforms.next(&());
+ return Some(Chunk {
+ text: "\n",
+ ..Default::default()
+ });
+ } else {
+ return None;
+ }
+ }
+ }
+
+ let transform_end = self.transforms.end(&()).0 .0;
+ let (prefix_rows, prefix_bytes) =
+ offset_for_row(self.input_chunk.text, transform_end - self.output_row);
+ self.output_row += prefix_rows;
+ let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
+ self.input_chunk.text = suffix;
+ if self.output_row == transform_end {
+ self.transforms.next(&());
+ }
+
+ Some(Chunk {
+ text: prefix,
+ ..self.input_chunk
+ })
+ }
+}
+
+impl<'a> Iterator for BlockBufferRows<'a> {
+ type Item = Option<u32>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.started {
+ self.output_row += 1;
+ } else {
+ self.started = true;
+ }
+
+ if self.output_row >= self.transforms.end(&()).0 .0 {
+ self.transforms.next(&());
+ }
+
+ let transform = self.transforms.item()?;
+ if transform.block.is_some() {
+ Some(None)
+ } else {
+ Some(self.input_buffer_rows.next().unwrap())
+ }
+ }
+}
+
+impl sum_tree::Item for Transform {
+ type Summary = TransformSummary;
+
+ fn summary(&self) -> Self::Summary {
+ self.summary.clone()
+ }
+}
+
+impl sum_tree::Summary for TransformSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, summary: &Self, _: &()) {
+ self.input_rows += summary.input_rows;
+ self.output_rows += summary.output_rows;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapRow {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += summary.input_rows;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for BlockRow {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += summary.output_rows;
+ }
+}
+
+impl BlockDisposition {
+ fn is_below(&self) -> bool {
+ matches!(self, BlockDisposition::Below)
+ }
+}
+
+impl<'a, 'b, 'c> Deref for BlockContext<'a, 'b, 'c> {
+ type Target = ViewContext<'a, 'b, Editor>;
+
+ fn deref(&self) -> &Self::Target {
+ self.view_context
+ }
+}
+
+impl DerefMut for BlockContext<'_, '_, '_> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.view_context
+ }
+}
+
+impl Block {
+ pub fn render(&self, cx: &mut BlockContext) -> AnyElement<Editor> {
+ self.render.lock()(cx)
+ }
+
+ pub fn position(&self) -> &Anchor {
+ &self.position
+ }
+
+ pub fn style(&self) -> BlockStyle {
+ self.style
+ }
+}
+
+impl Debug for Block {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Block")
+ .field("id", &self.id)
+ .field("position", &self.position)
+ .field("disposition", &self.disposition)
+ .finish()
+ }
+}
+
+// Count the number of bytes prior to a target point. If the string doesn't contain the target
+// point, return its total extent. Otherwise return the target point itself.
+fn offset_for_row(s: &str, target: u32) -> (u32, usize) {
+ let mut row = 0;
+ let mut offset = 0;
+ for (ix, line) in s.split('\n').enumerate() {
+ if ix > 0 {
+ row += 1;
+ offset += 1;
+ }
+ if row >= target {
+ break;
+ }
+ offset += line.len() as usize;
+ }
+ (row, offset)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::display_map::inlay_map::InlayMap;
+ use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap};
+ use gpui::{elements::Empty, Element};
+ use multi_buffer::MultiBuffer;
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use std::env;
+ use util::RandomCharIter;
+
+ #[gpui::test]
+ fn test_offset_for_row() {
+ assert_eq!(offset_for_row("", 0), (0, 0));
+ assert_eq!(offset_for_row("", 1), (0, 0));
+ assert_eq!(offset_for_row("abcd", 0), (0, 0));
+ assert_eq!(offset_for_row("abcd", 1), (0, 4));
+ assert_eq!(offset_for_row("\n", 0), (0, 0));
+ assert_eq!(offset_for_row("\n", 1), (1, 1));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 0), (0, 0));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 1), (1, 4));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 2), (2, 8));
+ assert_eq!(offset_for_row("abc\ndef\nghi", 3), (2, 11));
+ }
+
+ #[gpui::test]
+ fn test_basic_blocks(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+
+ let text = "aaa\nbbb\nccc\nddd";
+
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 1.try_into().unwrap());
+ let (wrap_map, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, None, cx);
+ let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+ let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+ let block_ids = writer.insert(vec![
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 0)),
+ height: 1,
+ disposition: BlockDisposition::Above,
+ render: Arc::new(|_| Empty::new().into_any_named("block 1")),
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 2)),
+ height: 2,
+ disposition: BlockDisposition::Above,
+ render: Arc::new(|_| Empty::new().into_any_named("block 2")),
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(3, 3)),
+ height: 3,
+ disposition: BlockDisposition::Below,
+ render: Arc::new(|_| Empty::new().into_any_named("block 3")),
+ },
+ ]);
+
+ let snapshot = block_map.read(wraps_snapshot, Default::default());
+ assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n");
+
+ let blocks = snapshot
+ .blocks_in_range(0..8)
+ .map(|(start_row, block)| {
+ let block = block.as_custom().unwrap();
+ (start_row..start_row + block.height as u32, block.id)
+ })
+ .collect::<Vec<_>>();
+
+ // When multiple blocks are on the same line, the newer blocks appear first.
+ assert_eq!(
+ blocks,
+ &[
+ (1..2, block_ids[0]),
+ (2..4, block_ids[1]),
+ (7..10, block_ids[2]),
+ ]
+ );
+
+ assert_eq!(
+ snapshot.to_block_point(WrapPoint::new(0, 3)),
+ BlockPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.to_block_point(WrapPoint::new(1, 0)),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.to_block_point(WrapPoint::new(3, 3)),
+ BlockPoint::new(6, 3)
+ );
+
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(0, 3)),
+ WrapPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(1, 0)),
+ WrapPoint::new(1, 0)
+ );
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(3, 0)),
+ WrapPoint::new(1, 0)
+ );
+ assert_eq!(
+ snapshot.to_wrap_point(BlockPoint::new(7, 0)),
+ WrapPoint::new(3, 3)
+ );
+
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 0), Bias::Left),
+ BlockPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 0), Bias::Right),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 1), Bias::Left),
+ BlockPoint::new(0, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(1, 1), Bias::Right),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(4, 0), Bias::Left),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(4, 0), Bias::Right),
+ BlockPoint::new(4, 0)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(6, 3), Bias::Left),
+ BlockPoint::new(6, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(6, 3), Bias::Right),
+ BlockPoint::new(6, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(7, 0), Bias::Left),
+ BlockPoint::new(6, 3)
+ );
+ assert_eq!(
+ snapshot.clip_point(BlockPoint::new(7, 0), Bias::Right),
+ BlockPoint::new(6, 3)
+ );
+
+ assert_eq!(
+ snapshot.buffer_rows(0).collect::<Vec<_>>(),
+ &[
+ Some(0),
+ None,
+ None,
+ None,
+ Some(1),
+ Some(2),
+ Some(3),
+ None,
+ None,
+ None
+ ]
+ );
+
+ // Insert a line break, separating two block decorations into separate lines.
+ let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "!!!\n")], None, cx);
+ buffer.snapshot(cx)
+ });
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, 4.try_into().unwrap());
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let snapshot = block_map.read(wraps_snapshot, wrap_edits);
+ assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n");
+ }
+
+ #[gpui::test]
+ fn test_blocks_on_wrapped_lines(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+
+ let text = "one two three\nfour five six\nseven eight";
+
+ let buffer = MultiBuffer::build_simple(text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+ let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font_id, 14.0, Some(60.), cx);
+ let mut block_map = BlockMap::new(wraps_snapshot.clone(), 1, 1);
+
+ let mut writer = block_map.write(wraps_snapshot.clone(), Default::default());
+ writer.insert(vec![
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 12)),
+ disposition: BlockDisposition::Above,
+ render: Arc::new(|_| Empty::new().into_any_named("block 1")),
+ height: 1,
+ },
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer_snapshot.anchor_after(Point::new(1, 1)),
+ disposition: BlockDisposition::Below,
+ render: Arc::new(|_| Empty::new().into_any_named("block 2")),
+ height: 1,
+ },
+ ]);
+
+ // Blocks with an 'above' disposition go above their corresponding buffer line.
+ // Blocks with a 'below' disposition go below their corresponding buffer line.
+ let snapshot = block_map.read(wraps_snapshot, Default::default());
+ assert_eq!(
+ snapshot.text(),
+ "one two \nthree\n\nfour five \nsix\n\nseven \neight"
+ );
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_blocks(cx: &mut gpui::AppContext, mut rng: StdRng) {
+ init_test(cx);
+
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(rng.gen_range(0.0..=100.0))
+ };
+ let tab_size = 1.try_into().unwrap();
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let buffer_start_header_height = rng.gen_range(1..=5);
+ let excerpt_header_height = rng.gen_range(1..=5);
+
+ log::info!("Wrap width: {:?}", wrap_width);
+ log::info!("Excerpt Header Height: {:?}", excerpt_header_height);
+
+ let buffer = if rng.gen() {
+ let len = rng.gen_range(0..10);
+ let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+ log::info!("initial buffer text: {:?}", text);
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+
+ let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (mut tab_map, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+ let (wrap_map, wraps_snapshot) =
+ WrapMap::new(tab_snapshot, font_id, font_size, wrap_width, cx);
+ let mut block_map = BlockMap::new(
+ wraps_snapshot,
+ buffer_start_header_height,
+ excerpt_header_height,
+ );
+ let mut custom_blocks = Vec::new();
+
+ for _ in 0..operations {
+ let mut buffer_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=19 => {
+ let wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(rng.gen_range(0.0..=100.0))
+ };
+ log::info!("Setting wrap width to {:?}", wrap_width);
+ wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+ }
+ 20..=39 => {
+ let block_count = rng.gen_range(1..=5);
+ let block_properties = (0..block_count)
+ .map(|_| {
+ let buffer = buffer.read(cx).read(cx);
+ let position = buffer.anchor_after(
+ buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Left),
+ );
+
+ let disposition = if rng.gen() {
+ BlockDisposition::Above
+ } else {
+ BlockDisposition::Below
+ };
+ let height = rng.gen_range(1..5);
+ log::info!(
+ "inserting block {:?} {:?} with height {}",
+ disposition,
+ position.to_point(&buffer),
+ height
+ );
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position,
+ height,
+ disposition,
+ render: Arc::new(|_| Empty::new().into_any()),
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+ let block_ids = block_map.insert(block_properties.clone());
+ for (block_id, props) in block_ids.into_iter().zip(block_properties) {
+ custom_blocks.push((block_id, props));
+ }
+ }
+ 40..=59 if !custom_blocks.is_empty() => {
+ let block_count = rng.gen_range(1..=4.min(custom_blocks.len()));
+ let block_ids_to_remove = (0..block_count)
+ .map(|_| {
+ custom_blocks
+ .remove(rng.gen_range(0..custom_blocks.len()))
+ .0
+ })
+ .collect();
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), vec![]);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let mut block_map = block_map.write(wraps_snapshot, wrap_edits);
+ block_map.remove(block_ids_to_remove);
+ }
+ _ => {
+ buffer.update(cx, |buffer, cx| {
+ let mutation_count = rng.gen_range(1..=5);
+ let subscription = buffer.subscribe();
+ buffer.randomly_mutate(&mut rng, mutation_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ buffer_edits.extend(subscription.consume());
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+ });
+ }
+ }
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tab_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (wraps_snapshot, wrap_edits) = wrap_map.update(cx, |wrap_map, cx| {
+ wrap_map.sync(tab_snapshot, tab_edits, cx)
+ });
+ let blocks_snapshot = block_map.read(wraps_snapshot.clone(), wrap_edits);
+ assert_eq!(
+ blocks_snapshot.transforms.summary().input_rows,
+ wraps_snapshot.max_point().row() + 1
+ );
+ log::info!("blocks text: {:?}", blocks_snapshot.text());
+
+ let mut expected_blocks = Vec::new();
+ expected_blocks.extend(custom_blocks.iter().map(|(id, block)| {
+ let mut position = block.position.to_point(&buffer_snapshot);
+ match block.disposition {
+ BlockDisposition::Above => {
+ position.column = 0;
+ }
+ BlockDisposition::Below => {
+ position.column = buffer_snapshot.line_len(position.row);
+ }
+ };
+ let row = wraps_snapshot.make_wrap_point(position, Bias::Left).row();
+ (
+ row,
+ ExpectedBlock::Custom {
+ disposition: block.disposition,
+ id: *id,
+ height: block.height,
+ },
+ )
+ }));
+ expected_blocks.extend(buffer_snapshot.excerpt_boundaries_in_range(0..).map(
+ |boundary| {
+ let position =
+ wraps_snapshot.make_wrap_point(Point::new(boundary.row, 0), Bias::Left);
+ (
+ position.row(),
+ ExpectedBlock::ExcerptHeader {
+ height: if boundary.starts_new_buffer {
+ buffer_start_header_height
+ } else {
+ excerpt_header_height
+ },
+ starts_new_buffer: boundary.starts_new_buffer,
+ },
+ )
+ },
+ ));
+ expected_blocks.sort_unstable();
+ let mut sorted_blocks_iter = expected_blocks.into_iter().peekable();
+
+ let input_buffer_rows = buffer_snapshot.buffer_rows(0).collect::<Vec<_>>();
+ let mut expected_buffer_rows = Vec::new();
+ let mut expected_text = String::new();
+ let mut expected_block_positions = Vec::new();
+ let input_text = wraps_snapshot.text();
+ for (row, input_line) in input_text.split('\n').enumerate() {
+ let row = row as u32;
+ if row > 0 {
+ expected_text.push('\n');
+ }
+
+ let buffer_row = input_buffer_rows[wraps_snapshot
+ .to_point(WrapPoint::new(row, 0), Bias::Left)
+ .row as usize];
+
+ while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+ if *block_row == row && block.disposition() == BlockDisposition::Above {
+ let (_, block) = sorted_blocks_iter.next().unwrap();
+ let height = block.height() as usize;
+ expected_block_positions
+ .push((expected_text.matches('\n').count() as u32, block));
+ let text = "\n".repeat(height);
+ expected_text.push_str(&text);
+ for _ in 0..height {
+ expected_buffer_rows.push(None);
+ }
+ } else {
+ break;
+ }
+ }
+
+ let soft_wrapped = wraps_snapshot.to_tab_point(WrapPoint::new(row, 0)).column() > 0;
+ expected_buffer_rows.push(if soft_wrapped { None } else { buffer_row });
+ expected_text.push_str(input_line);
+
+ while let Some((block_row, block)) = sorted_blocks_iter.peek() {
+ if *block_row == row && block.disposition() == BlockDisposition::Below {
+ let (_, block) = sorted_blocks_iter.next().unwrap();
+ let height = block.height() as usize;
+ expected_block_positions
+ .push((expected_text.matches('\n').count() as u32 + 1, block));
+ let text = "\n".repeat(height);
+ expected_text.push_str(&text);
+ for _ in 0..height {
+ expected_buffer_rows.push(None);
+ }
+ } else {
+ break;
+ }
+ }
+ }
+
+ let expected_lines = expected_text.split('\n').collect::<Vec<_>>();
+ let expected_row_count = expected_lines.len();
+ for start_row in 0..expected_row_count {
+ let expected_text = expected_lines[start_row..].join("\n");
+ let actual_text = blocks_snapshot
+ .chunks(
+ start_row as u32..blocks_snapshot.max_point().row + 1,
+ false,
+ Highlights::default(),
+ )
+ .map(|chunk| chunk.text)
+ .collect::<String>();
+ assert_eq!(
+ actual_text, expected_text,
+ "incorrect text starting from row {}",
+ start_row
+ );
+ assert_eq!(
+ blocks_snapshot
+ .buffer_rows(start_row as u32)
+ .collect::<Vec<_>>(),
+ &expected_buffer_rows[start_row..]
+ );
+ }
+
+ assert_eq!(
+ blocks_snapshot
+ .blocks_in_range(0..(expected_row_count as u32))
+ .map(|(row, block)| (row, block.clone().into()))
+ .collect::<Vec<_>>(),
+ expected_block_positions
+ );
+
+ let mut expected_longest_rows = Vec::new();
+ let mut longest_line_len = -1_isize;
+ for (row, line) in expected_lines.iter().enumerate() {
+ let row = row as u32;
+
+ assert_eq!(
+ blocks_snapshot.line_len(row),
+ line.len() as u32,
+ "invalid line len for row {}",
+ row
+ );
+
+ let line_char_count = line.chars().count() as isize;
+ match line_char_count.cmp(&longest_line_len) {
+ Ordering::Less => {}
+ Ordering::Equal => expected_longest_rows.push(row),
+ Ordering::Greater => {
+ longest_line_len = line_char_count;
+ expected_longest_rows.clear();
+ expected_longest_rows.push(row);
+ }
+ }
+ }
+
+ let longest_row = blocks_snapshot.longest_row();
+ assert!(
+ expected_longest_rows.contains(&longest_row),
+ "incorrect longest row {}. expected {:?} with length {}",
+ longest_row,
+ expected_longest_rows,
+ longest_line_len,
+ );
+
+ for row in 0..=blocks_snapshot.wrap_snapshot.max_point().row() {
+ let wrap_point = WrapPoint::new(row, 0);
+ let block_point = blocks_snapshot.to_block_point(wrap_point);
+ assert_eq!(blocks_snapshot.to_wrap_point(block_point), wrap_point);
+ }
+
+ let mut block_point = BlockPoint::new(0, 0);
+ for c in expected_text.chars() {
+ let left_point = blocks_snapshot.clip_point(block_point, Bias::Left);
+ let left_buffer_point = blocks_snapshot.to_point(left_point, Bias::Left);
+ assert_eq!(
+ blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(left_point)),
+ left_point
+ );
+ assert_eq!(
+ left_buffer_point,
+ buffer_snapshot.clip_point(left_buffer_point, Bias::Right),
+ "{:?} is not valid in buffer coordinates",
+ left_point
+ );
+
+ let right_point = blocks_snapshot.clip_point(block_point, Bias::Right);
+ let right_buffer_point = blocks_snapshot.to_point(right_point, Bias::Right);
+ assert_eq!(
+ blocks_snapshot.to_block_point(blocks_snapshot.to_wrap_point(right_point)),
+ right_point
+ );
+ assert_eq!(
+ right_buffer_point,
+ buffer_snapshot.clip_point(right_buffer_point, Bias::Left),
+ "{:?} is not valid in buffer coordinates",
+ right_point
+ );
+
+ if c == '\n' {
+ block_point.0 += Point::new(1, 0);
+ } else {
+ block_point.column += c.len_utf8() as u32;
+ }
+ }
+ }
+
+ #[derive(Debug, Eq, PartialEq, Ord, PartialOrd)]
+ enum ExpectedBlock {
+ ExcerptHeader {
+ height: u8,
+ starts_new_buffer: bool,
+ },
+ Custom {
+ disposition: BlockDisposition,
+ id: BlockId,
+ height: u8,
+ },
+ }
+
+ impl ExpectedBlock {
+ fn height(&self) -> u8 {
+ match self {
+ ExpectedBlock::ExcerptHeader { height, .. } => *height,
+ ExpectedBlock::Custom { height, .. } => *height,
+ }
+ }
+
+ fn disposition(&self) -> BlockDisposition {
+ match self {
+ ExpectedBlock::ExcerptHeader { .. } => BlockDisposition::Above,
+ ExpectedBlock::Custom { disposition, .. } => *disposition,
+ }
+ }
+ }
+
+ impl From<TransformBlock> for ExpectedBlock {
+ fn from(block: TransformBlock) -> Self {
+ match block {
+ TransformBlock::Custom(block) => ExpectedBlock::Custom {
+ id: block.id,
+ disposition: block.disposition,
+ height: block.height,
+ },
+ TransformBlock::ExcerptHeader {
+ height,
+ starts_new_buffer,
+ ..
+ } => ExpectedBlock::ExcerptHeader {
+ height,
+ starts_new_buffer,
+ },
+ }
+ }
+ }
+ }
+
+ fn init_test(cx: &mut gpui::AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ }
+
+ impl TransformBlock {
+ fn as_custom(&self) -> Option<&Block> {
+ match self {
+ TransformBlock::Custom(block) => Some(block),
+ TransformBlock::ExcerptHeader { .. } => None,
+ }
+ }
+ }
+
+ impl BlockSnapshot {
+ fn to_point(&self, point: BlockPoint, bias: Bias) -> Point {
+ self.wrap_snapshot.to_point(self.to_wrap_point(point), bias)
+ }
+ }
+}
@@ -0,0 +1,1706 @@
+use super::{
+ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
+ Highlights,
+};
+use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset};
+use gpui::{color::Color, fonts::HighlightStyle};
+use language::{Chunk, Edit, Point, TextSummary};
+use std::{
+ any::TypeId,
+ cmp::{self, Ordering},
+ iter,
+ ops::{Add, AddAssign, Range, Sub},
+};
+use sum_tree::{Bias, Cursor, FilterCursor, SumTree};
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct FoldPoint(pub Point);
+
+impl FoldPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(Point::new(row, column))
+ }
+
+ pub fn row(self) -> u32 {
+ self.0.row
+ }
+
+ pub fn column(self) -> u32 {
+ self.0.column
+ }
+
+ pub fn row_mut(&mut self) -> &mut u32 {
+ &mut self.0.row
+ }
+
+ #[cfg(test)]
+ pub fn column_mut(&mut self) -> &mut u32 {
+ &mut self.0.column
+ }
+
+ pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint {
+ let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>();
+ cursor.seek(&self, Bias::Right, &());
+ let overshoot = self.0 - cursor.start().0 .0;
+ InlayPoint(cursor.start().1 .0 + overshoot)
+ }
+
+ pub fn to_offset(self, snapshot: &FoldSnapshot) -> FoldOffset {
+ let mut cursor = snapshot
+ .transforms
+ .cursor::<(FoldPoint, TransformSummary)>();
+ cursor.seek(&self, Bias::Right, &());
+ let overshoot = self.0 - cursor.start().1.output.lines;
+ let mut offset = cursor.start().1.output.len;
+ if !overshoot.is_zero() {
+ let transform = cursor.item().expect("display point out of range");
+ assert!(transform.output_text.is_none());
+ let end_inlay_offset = snapshot
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1.input.lines + overshoot));
+ offset += end_inlay_offset.0 - cursor.start().1.input.len;
+ }
+ FoldOffset(offset)
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldPoint {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.lines;
+ }
+}
+
+pub struct FoldMapWriter<'a>(&'a mut FoldMap);
+
+impl<'a> FoldMapWriter<'a> {
+ pub fn fold<T: ToOffset>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ ) -> (FoldSnapshot, Vec<FoldEdit>) {
+ let mut edits = Vec::new();
+ let mut folds = Vec::new();
+ let snapshot = self.0.snapshot.inlay_snapshot.clone();
+ for range in ranges.into_iter() {
+ let buffer = &snapshot.buffer;
+ let range = range.start.to_offset(&buffer)..range.end.to_offset(&buffer);
+
+ // Ignore any empty ranges.
+ if range.start == range.end {
+ continue;
+ }
+
+ // For now, ignore any ranges that span an excerpt boundary.
+ let fold = Fold(buffer.anchor_after(range.start)..buffer.anchor_before(range.end));
+ if fold.0.start.excerpt_id != fold.0.end.excerpt_id {
+ continue;
+ }
+
+ folds.push(fold);
+
+ let inlay_range =
+ snapshot.to_inlay_offset(range.start)..snapshot.to_inlay_offset(range.end);
+ edits.push(InlayEdit {
+ old: inlay_range.clone(),
+ new: inlay_range,
+ });
+ }
+
+ let buffer = &snapshot.buffer;
+ folds.sort_unstable_by(|a, b| sum_tree::SeekTarget::cmp(a, b, buffer));
+
+ self.0.snapshot.folds = {
+ let mut new_tree = SumTree::new();
+ let mut cursor = self.0.snapshot.folds.cursor::<Fold>();
+ for fold in folds {
+ new_tree.append(cursor.slice(&fold, Bias::Right, buffer), buffer);
+ new_tree.push(fold, buffer);
+ }
+ new_tree.append(cursor.suffix(buffer), buffer);
+ new_tree
+ };
+
+ consolidate_inlay_edits(&mut edits);
+ let edits = self.0.sync(snapshot.clone(), edits);
+ (self.0.snapshot.clone(), edits)
+ }
+
+ pub fn unfold<T: ToOffset>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ inclusive: bool,
+ ) -> (FoldSnapshot, Vec<FoldEdit>) {
+ let mut edits = Vec::new();
+ let mut fold_ixs_to_delete = Vec::new();
+ let snapshot = self.0.snapshot.inlay_snapshot.clone();
+ let buffer = &snapshot.buffer;
+ for range in ranges.into_iter() {
+ // Remove intersecting folds and add their ranges to edits that are passed to sync.
+ let mut folds_cursor =
+ intersecting_folds(&snapshot, &self.0.snapshot.folds, range, inclusive);
+ while let Some(fold) = folds_cursor.item() {
+ let offset_range = fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer);
+ if offset_range.end > offset_range.start {
+ let inlay_range = snapshot.to_inlay_offset(offset_range.start)
+ ..snapshot.to_inlay_offset(offset_range.end);
+ edits.push(InlayEdit {
+ old: inlay_range.clone(),
+ new: inlay_range,
+ });
+ }
+ fold_ixs_to_delete.push(*folds_cursor.start());
+ folds_cursor.next(buffer);
+ }
+ }
+
+ fold_ixs_to_delete.sort_unstable();
+ fold_ixs_to_delete.dedup();
+
+ self.0.snapshot.folds = {
+ let mut cursor = self.0.snapshot.folds.cursor::<usize>();
+ let mut folds = SumTree::new();
+ for fold_ix in fold_ixs_to_delete {
+ folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer);
+ cursor.next(buffer);
+ }
+ folds.append(cursor.suffix(buffer), buffer);
+ folds
+ };
+
+ consolidate_inlay_edits(&mut edits);
+ let edits = self.0.sync(snapshot.clone(), edits);
+ (self.0.snapshot.clone(), edits)
+ }
+}
+
+pub struct FoldMap {
+ snapshot: FoldSnapshot,
+ ellipses_color: Option<Color>,
+}
+
+impl FoldMap {
+ pub fn new(inlay_snapshot: InlaySnapshot) -> (Self, FoldSnapshot) {
+ let this = Self {
+ snapshot: FoldSnapshot {
+ folds: Default::default(),
+ transforms: SumTree::from_item(
+ Transform {
+ summary: TransformSummary {
+ input: inlay_snapshot.text_summary(),
+ output: inlay_snapshot.text_summary(),
+ },
+ output_text: None,
+ },
+ &(),
+ ),
+ inlay_snapshot: inlay_snapshot.clone(),
+ version: 0,
+ ellipses_color: None,
+ },
+ ellipses_color: None,
+ };
+ let snapshot = this.snapshot.clone();
+ (this, snapshot)
+ }
+
+ pub fn read(
+ &mut self,
+ inlay_snapshot: InlaySnapshot,
+ edits: Vec<InlayEdit>,
+ ) -> (FoldSnapshot, Vec<FoldEdit>) {
+ let edits = self.sync(inlay_snapshot, edits);
+ self.check_invariants();
+ (self.snapshot.clone(), edits)
+ }
+
+ pub fn write(
+ &mut self,
+ inlay_snapshot: InlaySnapshot,
+ edits: Vec<InlayEdit>,
+ ) -> (FoldMapWriter, FoldSnapshot, Vec<FoldEdit>) {
+ let (snapshot, edits) = self.read(inlay_snapshot, edits);
+ (FoldMapWriter(self), snapshot, edits)
+ }
+
+ pub fn set_ellipses_color(&mut self, color: Color) -> bool {
+ if self.ellipses_color != Some(color) {
+ self.ellipses_color = Some(color);
+ true
+ } else {
+ false
+ }
+ }
+
+ fn check_invariants(&self) {
+ if cfg!(test) {
+ assert_eq!(
+ self.snapshot.transforms.summary().input.len,
+ self.snapshot.inlay_snapshot.len().0,
+ "transform tree does not match inlay snapshot's length"
+ );
+
+ let mut folds = self.snapshot.folds.iter().peekable();
+ while let Some(fold) = folds.next() {
+ if let Some(next_fold) = folds.peek() {
+ let comparison = fold
+ .0
+ .cmp(&next_fold.0, &self.snapshot.inlay_snapshot.buffer);
+ assert!(comparison.is_le());
+ }
+ }
+ }
+ }
+
+ fn sync(
+ &mut self,
+ inlay_snapshot: InlaySnapshot,
+ inlay_edits: Vec<InlayEdit>,
+ ) -> Vec<FoldEdit> {
+ if inlay_edits.is_empty() {
+ if self.snapshot.inlay_snapshot.version != inlay_snapshot.version {
+ self.snapshot.version += 1;
+ }
+ self.snapshot.inlay_snapshot = inlay_snapshot;
+ Vec::new()
+ } else {
+ let mut inlay_edits_iter = inlay_edits.iter().cloned().peekable();
+
+ let mut new_transforms = SumTree::new();
+ let mut cursor = self.snapshot.transforms.cursor::<InlayOffset>();
+ cursor.seek(&InlayOffset(0), Bias::Right, &());
+
+ while let Some(mut edit) = inlay_edits_iter.next() {
+ new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &());
+ edit.new.start -= edit.old.start - *cursor.start();
+ edit.old.start = *cursor.start();
+
+ cursor.seek(&edit.old.end, Bias::Right, &());
+ cursor.next(&());
+
+ let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize;
+ loop {
+ edit.old.end = *cursor.start();
+
+ if let Some(next_edit) = inlay_edits_iter.peek() {
+ if next_edit.old.start > edit.old.end {
+ break;
+ }
+
+ let next_edit = inlay_edits_iter.next().unwrap();
+ delta += next_edit.new_len().0 as isize - next_edit.old_len().0 as isize;
+
+ if next_edit.old.end >= edit.old.end {
+ edit.old.end = next_edit.old.end;
+ cursor.seek(&edit.old.end, Bias::Right, &());
+ cursor.next(&());
+ }
+ } else {
+ break;
+ }
+ }
+
+ edit.new.end =
+ InlayOffset(((edit.new.start + edit.old_len()).0 as isize + delta) as usize);
+
+ let anchor = inlay_snapshot
+ .buffer
+ .anchor_before(inlay_snapshot.to_buffer_offset(edit.new.start));
+ let mut folds_cursor = self.snapshot.folds.cursor::<Fold>();
+ folds_cursor.seek(
+ &Fold(anchor..Anchor::max()),
+ Bias::Left,
+ &inlay_snapshot.buffer,
+ );
+
+ let mut folds = iter::from_fn({
+ let inlay_snapshot = &inlay_snapshot;
+ move || {
+ let item = folds_cursor.item().map(|f| {
+ let buffer_start = f.0.start.to_offset(&inlay_snapshot.buffer);
+ let buffer_end = f.0.end.to_offset(&inlay_snapshot.buffer);
+ inlay_snapshot.to_inlay_offset(buffer_start)
+ ..inlay_snapshot.to_inlay_offset(buffer_end)
+ });
+ folds_cursor.next(&inlay_snapshot.buffer);
+ item
+ }
+ })
+ .peekable();
+
+ while folds.peek().map_or(false, |fold| fold.start < edit.new.end) {
+ let mut fold = folds.next().unwrap();
+ let sum = new_transforms.summary();
+
+ assert!(fold.start.0 >= sum.input.len);
+
+ while folds
+ .peek()
+ .map_or(false, |next_fold| next_fold.start <= fold.end)
+ {
+ let next_fold = folds.next().unwrap();
+ if next_fold.end > fold.end {
+ fold.end = next_fold.end;
+ }
+ }
+
+ if fold.start.0 > sum.input.len {
+ let text_summary = inlay_snapshot
+ .text_summary_for_range(InlayOffset(sum.input.len)..fold.start);
+ new_transforms.push(
+ Transform {
+ summary: TransformSummary {
+ output: text_summary.clone(),
+ input: text_summary,
+ },
+ output_text: None,
+ },
+ &(),
+ );
+ }
+
+ if fold.end > fold.start {
+ let output_text = "โฏ";
+ new_transforms.push(
+ Transform {
+ summary: TransformSummary {
+ output: TextSummary::from(output_text),
+ input: inlay_snapshot
+ .text_summary_for_range(fold.start..fold.end),
+ },
+ output_text: Some(output_text),
+ },
+ &(),
+ );
+ }
+ }
+
+ let sum = new_transforms.summary();
+ if sum.input.len < edit.new.end.0 {
+ let text_summary = inlay_snapshot
+ .text_summary_for_range(InlayOffset(sum.input.len)..edit.new.end);
+ new_transforms.push(
+ Transform {
+ summary: TransformSummary {
+ output: text_summary.clone(),
+ input: text_summary,
+ },
+ output_text: None,
+ },
+ &(),
+ );
+ }
+ }
+
+ new_transforms.append(cursor.suffix(&()), &());
+ if new_transforms.is_empty() {
+ let text_summary = inlay_snapshot.text_summary();
+ new_transforms.push(
+ Transform {
+ summary: TransformSummary {
+ output: text_summary.clone(),
+ input: text_summary,
+ },
+ output_text: None,
+ },
+ &(),
+ );
+ }
+
+ drop(cursor);
+
+ let mut fold_edits = Vec::with_capacity(inlay_edits.len());
+ {
+ let mut old_transforms = self
+ .snapshot
+ .transforms
+ .cursor::<(InlayOffset, FoldOffset)>();
+ let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>();
+
+ for mut edit in inlay_edits {
+ old_transforms.seek(&edit.old.start, Bias::Left, &());
+ if old_transforms.item().map_or(false, |t| t.is_fold()) {
+ edit.old.start = old_transforms.start().0;
+ }
+ let old_start =
+ old_transforms.start().1 .0 + (edit.old.start - old_transforms.start().0).0;
+
+ old_transforms.seek_forward(&edit.old.end, Bias::Right, &());
+ if old_transforms.item().map_or(false, |t| t.is_fold()) {
+ old_transforms.next(&());
+ edit.old.end = old_transforms.start().0;
+ }
+ let old_end =
+ old_transforms.start().1 .0 + (edit.old.end - old_transforms.start().0).0;
+
+ new_transforms.seek(&edit.new.start, Bias::Left, &());
+ if new_transforms.item().map_or(false, |t| t.is_fold()) {
+ edit.new.start = new_transforms.start().0;
+ }
+ let new_start =
+ new_transforms.start().1 .0 + (edit.new.start - new_transforms.start().0).0;
+
+ new_transforms.seek_forward(&edit.new.end, Bias::Right, &());
+ if new_transforms.item().map_or(false, |t| t.is_fold()) {
+ new_transforms.next(&());
+ edit.new.end = new_transforms.start().0;
+ }
+ let new_end =
+ new_transforms.start().1 .0 + (edit.new.end - new_transforms.start().0).0;
+
+ fold_edits.push(FoldEdit {
+ old: FoldOffset(old_start)..FoldOffset(old_end),
+ new: FoldOffset(new_start)..FoldOffset(new_end),
+ });
+ }
+
+ consolidate_fold_edits(&mut fold_edits);
+ }
+
+ self.snapshot.transforms = new_transforms;
+ self.snapshot.inlay_snapshot = inlay_snapshot;
+ self.snapshot.version += 1;
+ fold_edits
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct FoldSnapshot {
+ transforms: SumTree<Transform>,
+ folds: SumTree<Fold>,
+ pub inlay_snapshot: InlaySnapshot,
+ pub version: usize,
+ pub ellipses_color: Option<Color>,
+}
+
+impl FoldSnapshot {
+ #[cfg(test)]
+ pub fn text(&self) -> String {
+ self.chunks(FoldOffset(0)..self.len(), false, Highlights::default())
+ .map(|c| c.text)
+ .collect()
+ }
+
+ #[cfg(test)]
+ pub fn fold_count(&self) -> usize {
+ self.folds.items(&self.inlay_snapshot.buffer).len()
+ }
+
+ pub fn text_summary_for_range(&self, range: Range<FoldPoint>) -> TextSummary {
+ let mut summary = TextSummary::default();
+
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
+ cursor.seek(&range.start, Bias::Right, &());
+ if let Some(transform) = cursor.item() {
+ let start_in_transform = range.start.0 - cursor.start().0 .0;
+ let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0 .0;
+ if let Some(output_text) = transform.output_text {
+ summary = TextSummary::from(
+ &output_text
+ [start_in_transform.column as usize..end_in_transform.column as usize],
+ );
+ } else {
+ let inlay_start = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + start_in_transform));
+ let inlay_end = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
+ summary = self
+ .inlay_snapshot
+ .text_summary_for_range(inlay_start..inlay_end);
+ }
+ }
+
+ if range.end > cursor.end(&()).0 {
+ cursor.next(&());
+ summary += &cursor
+ .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+ .output;
+ if let Some(transform) = cursor.item() {
+ let end_in_transform = range.end.0 - cursor.start().0 .0;
+ if let Some(output_text) = transform.output_text {
+ summary += TextSummary::from(&output_text[..end_in_transform.column as usize]);
+ } else {
+ let inlay_start = self.inlay_snapshot.to_offset(cursor.start().1);
+ let inlay_end = self
+ .inlay_snapshot
+ .to_offset(InlayPoint(cursor.start().1 .0 + end_in_transform));
+ summary += self
+ .inlay_snapshot
+ .text_summary_for_range(inlay_start..inlay_end);
+ }
+ }
+ }
+
+ summary
+ }
+
+ pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>();
+ cursor.seek(&point, Bias::Right, &());
+ if cursor.item().map_or(false, |t| t.is_fold()) {
+ if bias == Bias::Left || point == cursor.start().0 {
+ cursor.start().1
+ } else {
+ cursor.end(&()).1
+ }
+ } else {
+ let overshoot = point.0 - cursor.start().0 .0;
+ FoldPoint(cmp::min(
+ cursor.start().1 .0 + overshoot,
+ cursor.end(&()).1 .0,
+ ))
+ }
+ }
+
+ pub fn len(&self) -> FoldOffset {
+ FoldOffset(self.transforms.summary().output.len)
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ let line_start = FoldPoint::new(row, 0).to_offset(self).0;
+ let line_end = if row >= self.max_point().row() {
+ self.len().0
+ } else {
+ FoldPoint::new(row + 1, 0).to_offset(self).0 - 1
+ };
+ (line_end - line_start) as u32
+ }
+
+ pub fn buffer_rows(&self, start_row: u32) -> FoldBufferRows {
+ if start_row > self.transforms.summary().output.lines.row {
+ panic!("invalid display row {}", start_row);
+ }
+
+ let fold_point = FoldPoint::new(start_row, 0);
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
+ cursor.seek(&fold_point, Bias::Left, &());
+
+ let overshoot = fold_point.0 - cursor.start().0 .0;
+ let inlay_point = InlayPoint(cursor.start().1 .0 + overshoot);
+ let input_buffer_rows = self.inlay_snapshot.buffer_rows(inlay_point.row());
+
+ FoldBufferRows {
+ fold_point,
+ input_buffer_rows,
+ cursor,
+ }
+ }
+
+ pub fn max_point(&self) -> FoldPoint {
+ FoldPoint(self.transforms.summary().output.lines)
+ }
+
+ #[cfg(test)]
+ pub fn longest_row(&self) -> u32 {
+ self.transforms.summary().output.longest_row
+ }
+
+ pub fn folds_in_range<T>(&self, range: Range<T>) -> impl Iterator<Item = &Range<Anchor>>
+ where
+ T: ToOffset,
+ {
+ let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false);
+ iter::from_fn(move || {
+ let item = folds.item().map(|f| &f.0);
+ folds.next(&self.inlay_snapshot.buffer);
+ item
+ })
+ }
+
+ pub fn intersects_fold<T>(&self, offset: T) -> bool
+ where
+ T: ToOffset,
+ {
+ let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer);
+ let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset);
+ let mut cursor = self.transforms.cursor::<InlayOffset>();
+ cursor.seek(&inlay_offset, Bias::Right, &());
+ cursor.item().map_or(false, |t| t.output_text.is_some())
+ }
+
+ pub fn is_line_folded(&self, buffer_row: u32) -> bool {
+ let mut inlay_point = self
+ .inlay_snapshot
+ .to_inlay_point(Point::new(buffer_row, 0));
+ let mut cursor = self.transforms.cursor::<InlayPoint>();
+ cursor.seek(&inlay_point, Bias::Right, &());
+ loop {
+ match cursor.item() {
+ Some(transform) => {
+ let buffer_point = self.inlay_snapshot.to_buffer_point(inlay_point);
+ if buffer_point.row != buffer_row {
+ return false;
+ } else if transform.output_text.is_some() {
+ return true;
+ }
+ }
+ None => return false,
+ }
+
+ if cursor.end(&()).row() == inlay_point.row() {
+ cursor.next(&());
+ } else {
+ inlay_point.0 += Point::new(1, 0);
+ cursor.seek(&inlay_point, Bias::Right, &());
+ }
+ }
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ range: Range<FoldOffset>,
+ language_aware: bool,
+ highlights: Highlights<'a>,
+ ) -> FoldChunks<'a> {
+ let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
+
+ let inlay_end = {
+ transform_cursor.seek(&range.end, Bias::Right, &());
+ let overshoot = range.end.0 - transform_cursor.start().0 .0;
+ transform_cursor.start().1 + InlayOffset(overshoot)
+ };
+
+ let inlay_start = {
+ transform_cursor.seek(&range.start, Bias::Right, &());
+ let overshoot = range.start.0 - transform_cursor.start().0 .0;
+ transform_cursor.start().1 + InlayOffset(overshoot)
+ };
+
+ FoldChunks {
+ transform_cursor,
+ inlay_chunks: self.inlay_snapshot.chunks(
+ inlay_start..inlay_end,
+ language_aware,
+ highlights,
+ ),
+ inlay_chunk: None,
+ inlay_offset: inlay_start,
+ output_offset: range.start.0,
+ max_output_offset: range.end.0,
+ ellipses_color: self.ellipses_color,
+ }
+ }
+
+ pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator<Item = char> {
+ self.chunks(
+ start.to_offset(self)..self.len(),
+ false,
+ Highlights::default(),
+ )
+ .flat_map(|chunk| chunk.text.chars())
+ }
+
+ #[cfg(test)]
+ pub fn clip_offset(&self, offset: FoldOffset, bias: Bias) -> FoldOffset {
+ if offset > self.len() {
+ self.len()
+ } else {
+ self.clip_point(offset.to_point(self), bias).to_offset(self)
+ }
+ }
+
+ pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint {
+ let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>();
+ cursor.seek(&point, Bias::Right, &());
+ if let Some(transform) = cursor.item() {
+ let transform_start = cursor.start().0 .0;
+ if transform.output_text.is_some() {
+ if point.0 == transform_start || matches!(bias, Bias::Left) {
+ FoldPoint(transform_start)
+ } else {
+ FoldPoint(cursor.end(&()).0 .0)
+ }
+ } else {
+ let overshoot = InlayPoint(point.0 - transform_start);
+ let inlay_point = cursor.start().1 + overshoot;
+ let clipped_inlay_point = self.inlay_snapshot.clip_point(inlay_point, bias);
+ FoldPoint(cursor.start().0 .0 + (clipped_inlay_point - cursor.start().1).0)
+ }
+ } else {
+ FoldPoint(self.transforms.summary().output.lines)
+ }
+ }
+}
+
+fn intersecting_folds<'a, T>(
+ inlay_snapshot: &'a InlaySnapshot,
+ folds: &'a SumTree<Fold>,
+ range: Range<T>,
+ inclusive: bool,
+) -> FilterCursor<'a, impl 'a + FnMut(&FoldSummary) -> bool, Fold, usize>
+where
+ T: ToOffset,
+{
+ let buffer = &inlay_snapshot.buffer;
+ let start = buffer.anchor_before(range.start.to_offset(buffer));
+ let end = buffer.anchor_after(range.end.to_offset(buffer));
+ let mut cursor = folds.filter::<_, usize>(move |summary| {
+ let start_cmp = start.cmp(&summary.max_end, buffer);
+ let end_cmp = end.cmp(&summary.min_start, buffer);
+
+ if inclusive {
+ start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal
+ } else {
+ start_cmp == Ordering::Less && end_cmp == Ordering::Greater
+ }
+ });
+ cursor.next(buffer);
+ cursor
+}
+
+fn consolidate_inlay_edits(edits: &mut Vec<InlayEdit>) {
+ edits.sort_unstable_by(|a, b| {
+ a.old
+ .start
+ .cmp(&b.old.start)
+ .then_with(|| b.old.end.cmp(&a.old.end))
+ });
+
+ let mut i = 1;
+ while i < edits.len() {
+ let edit = edits[i].clone();
+ let prev_edit = &mut edits[i - 1];
+ if prev_edit.old.end >= edit.old.start {
+ prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
+ prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
+ prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
+ edits.remove(i);
+ continue;
+ }
+ i += 1;
+ }
+}
+
+fn consolidate_fold_edits(edits: &mut Vec<FoldEdit>) {
+ edits.sort_unstable_by(|a, b| {
+ a.old
+ .start
+ .cmp(&b.old.start)
+ .then_with(|| b.old.end.cmp(&a.old.end))
+ });
+
+ let mut i = 1;
+ while i < edits.len() {
+ let edit = edits[i].clone();
+ let prev_edit = &mut edits[i - 1];
+ if prev_edit.old.end >= edit.old.start {
+ prev_edit.old.end = prev_edit.old.end.max(edit.old.end);
+ prev_edit.new.start = prev_edit.new.start.min(edit.new.start);
+ prev_edit.new.end = prev_edit.new.end.max(edit.new.end);
+ edits.remove(i);
+ continue;
+ }
+ i += 1;
+ }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct Transform {
+ summary: TransformSummary,
+ output_text: Option<&'static str>,
+}
+
+impl Transform {
+ fn is_fold(&self) -> bool {
+ self.output_text.is_some()
+ }
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct TransformSummary {
+ output: TextSummary,
+ input: TextSummary,
+}
+
+impl sum_tree::Item for Transform {
+ type Summary = TransformSummary;
+
+ fn summary(&self) -> Self::Summary {
+ self.summary.clone()
+ }
+}
+
+impl sum_tree::Summary for TransformSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, other: &Self, _: &()) {
+ self.input += &other.input;
+ self.output += &other.output;
+ }
+}
+
+#[derive(Clone, Debug)]
+struct Fold(Range<Anchor>);
+
+impl Default for Fold {
+ fn default() -> Self {
+ Self(Anchor::min()..Anchor::max())
+ }
+}
+
+impl sum_tree::Item for Fold {
+ type Summary = FoldSummary;
+
+ fn summary(&self) -> Self::Summary {
+ FoldSummary {
+ start: self.0.start.clone(),
+ end: self.0.end.clone(),
+ min_start: self.0.start.clone(),
+ max_end: self.0.end.clone(),
+ count: 1,
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+struct FoldSummary {
+ start: Anchor,
+ end: Anchor,
+ min_start: Anchor,
+ max_end: Anchor,
+ count: usize,
+}
+
+impl Default for FoldSummary {
+ fn default() -> Self {
+ Self {
+ start: Anchor::min(),
+ end: Anchor::max(),
+ min_start: Anchor::max(),
+ max_end: Anchor::min(),
+ count: 0,
+ }
+ }
+}
+
+impl sum_tree::Summary for FoldSummary {
+ type Context = MultiBufferSnapshot;
+
+ fn add_summary(&mut self, other: &Self, buffer: &Self::Context) {
+ if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less {
+ self.min_start = other.min_start.clone();
+ }
+ if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater {
+ self.max_end = other.max_end.clone();
+ }
+
+ #[cfg(debug_assertions)]
+ {
+ let start_comparison = self.start.cmp(&other.start, buffer);
+ assert!(start_comparison <= Ordering::Equal);
+ if start_comparison == Ordering::Equal {
+ assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal);
+ }
+ }
+
+ self.start = other.start.clone();
+ self.end = other.end.clone();
+ self.count += other.count;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold {
+ fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
+ self.0.start = summary.start.clone();
+ self.0.end = summary.end.clone();
+ }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold {
+ fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering {
+ self.0.cmp(&other.0, buffer)
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, FoldSummary> for usize {
+ fn add_summary(&mut self, summary: &'a FoldSummary, _: &MultiBufferSnapshot) {
+ *self += summary.count;
+ }
+}
+
+#[derive(Clone)]
+pub struct FoldBufferRows<'a> {
+ cursor: Cursor<'a, Transform, (FoldPoint, InlayPoint)>,
+ input_buffer_rows: InlayBufferRows<'a>,
+ fold_point: FoldPoint,
+}
+
+impl<'a> Iterator for FoldBufferRows<'a> {
+ type Item = Option<u32>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let mut traversed_fold = false;
+ while self.fold_point > self.cursor.end(&()).0 {
+ self.cursor.next(&());
+ traversed_fold = true;
+ if self.cursor.item().is_none() {
+ break;
+ }
+ }
+
+ if self.cursor.item().is_some() {
+ if traversed_fold {
+ self.input_buffer_rows.seek(self.cursor.start().1.row());
+ self.input_buffer_rows.next();
+ }
+ *self.fold_point.row_mut() += 1;
+ self.input_buffer_rows.next()
+ } else {
+ None
+ }
+ }
+}
+
+pub struct FoldChunks<'a> {
+ transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
+ inlay_chunks: InlayChunks<'a>,
+ inlay_chunk: Option<(InlayOffset, Chunk<'a>)>,
+ inlay_offset: InlayOffset,
+ output_offset: usize,
+ max_output_offset: usize,
+ ellipses_color: Option<Color>,
+}
+
+impl<'a> Iterator for FoldChunks<'a> {
+ type Item = Chunk<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.output_offset >= self.max_output_offset {
+ return None;
+ }
+
+ let transform = self.transform_cursor.item()?;
+
+ // If we're in a fold, then return the fold's display text and
+ // advance the transform and buffer cursors to the end of the fold.
+ if let Some(output_text) = transform.output_text {
+ self.inlay_chunk.take();
+ self.inlay_offset += InlayOffset(transform.summary.input.len);
+ self.inlay_chunks.seek(self.inlay_offset);
+
+ while self.inlay_offset >= self.transform_cursor.end(&()).1
+ && self.transform_cursor.item().is_some()
+ {
+ self.transform_cursor.next(&());
+ }
+
+ self.output_offset += output_text.len();
+ return Some(Chunk {
+ text: output_text,
+ highlight_style: self.ellipses_color.map(|color| HighlightStyle {
+ color: Some(color),
+ ..Default::default()
+ }),
+ ..Default::default()
+ });
+ }
+
+ // Retrieve a chunk from the current location in the buffer.
+ if self.inlay_chunk.is_none() {
+ let chunk_offset = self.inlay_chunks.offset();
+ self.inlay_chunk = self.inlay_chunks.next().map(|chunk| (chunk_offset, chunk));
+ }
+
+ // Otherwise, take a chunk from the buffer's text.
+ if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk {
+ let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
+ let transform_end = self.transform_cursor.end(&()).1;
+ let chunk_end = buffer_chunk_end.min(transform_end);
+
+ chunk.text = &chunk.text
+ [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0];
+
+ if chunk_end == transform_end {
+ self.transform_cursor.next(&());
+ } else if chunk_end == buffer_chunk_end {
+ self.inlay_chunk.take();
+ }
+
+ self.inlay_offset = chunk_end;
+ self.output_offset += chunk.text.len();
+ return Some(chunk);
+ }
+
+ None
+ }
+}
+
+#[derive(Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+ offset: InlayOffset,
+ is_start: bool,
+ tag: Option<TypeId>,
+ style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for HighlightEndpoint {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.offset
+ .cmp(&other.offset)
+ .then_with(|| other.is_start.cmp(&self.is_start))
+ }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct FoldOffset(pub usize);
+
+impl FoldOffset {
+ pub fn to_point(self, snapshot: &FoldSnapshot) -> FoldPoint {
+ let mut cursor = snapshot
+ .transforms
+ .cursor::<(FoldOffset, TransformSummary)>();
+ cursor.seek(&self, Bias::Right, &());
+ let overshoot = if cursor.item().map_or(true, |t| t.is_fold()) {
+ Point::new(0, (self.0 - cursor.start().0 .0) as u32)
+ } else {
+ let inlay_offset = cursor.start().1.input.len + self.0 - cursor.start().0 .0;
+ let inlay_point = snapshot.inlay_snapshot.to_point(InlayOffset(inlay_offset));
+ inlay_point.0 - cursor.start().1.input.lines
+ };
+ FoldPoint(cursor.start().1.output.lines + overshoot)
+ }
+
+ #[cfg(test)]
+ pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset {
+ let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>();
+ cursor.seek(&self, Bias::Right, &());
+ let overshoot = self.0 - cursor.start().0 .0;
+ InlayOffset(cursor.start().1 .0 + overshoot)
+ }
+}
+
+impl Add for FoldOffset {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self(self.0 + rhs.0)
+ }
+}
+
+impl AddAssign for FoldOffset {
+ fn add_assign(&mut self, rhs: Self) {
+ self.0 += rhs.0;
+ }
+}
+
+impl Sub for FoldOffset {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Self(self.0 - rhs.0)
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for FoldOffset {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.len;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.input.lines;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.input.len;
+ }
+}
+
+pub type FoldEdit = Edit<FoldOffset>;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{display_map::inlay_map::InlayMap, MultiBuffer, ToPoint};
+ use collections::HashSet;
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use std::{env, mem};
+ use text::Patch;
+ use util::test::sample_text;
+ use util::RandomCharIter;
+ use Bias::{Left, Right};
+
+ #[gpui::test]
+ fn test_basic_folds(cx: &mut gpui::AppContext) {
+ init_test(cx);
+ let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+ let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+ let (mut writer, _, _) = map.write(inlay_snapshot, vec![]);
+ let (snapshot2, edits) = writer.fold(vec![
+ Point::new(0, 2)..Point::new(2, 2),
+ Point::new(2, 4)..Point::new(4, 1),
+ ]);
+ assert_eq!(snapshot2.text(), "aaโฏccโฏeeeee");
+ assert_eq!(
+ edits,
+ &[
+ FoldEdit {
+ old: FoldOffset(2)..FoldOffset(16),
+ new: FoldOffset(2)..FoldOffset(5),
+ },
+ FoldEdit {
+ old: FoldOffset(18)..FoldOffset(29),
+ new: FoldOffset(7)..FoldOffset(10)
+ },
+ ]
+ );
+
+ let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ vec![
+ (Point::new(0, 0)..Point::new(0, 1), "123"),
+ (Point::new(2, 3)..Point::new(2, 3), "123"),
+ ],
+ None,
+ cx,
+ );
+ buffer.snapshot(cx)
+ });
+
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (snapshot3, edits) = map.read(inlay_snapshot, inlay_edits);
+ assert_eq!(snapshot3.text(), "123aโฏc123cโฏeeeee");
+ assert_eq!(
+ edits,
+ &[
+ FoldEdit {
+ old: FoldOffset(0)..FoldOffset(1),
+ new: FoldOffset(0)..FoldOffset(3),
+ },
+ FoldEdit {
+ old: FoldOffset(6)..FoldOffset(6),
+ new: FoldOffset(8)..FoldOffset(11),
+ },
+ ]
+ );
+
+ let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(2, 6)..Point::new(4, 3), "456")], None, cx);
+ buffer.snapshot(cx)
+ });
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (snapshot4, _) = map.read(inlay_snapshot.clone(), inlay_edits);
+ assert_eq!(snapshot4.text(), "123aโฏc123456eee");
+
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false);
+ let (snapshot5, _) = map.read(inlay_snapshot.clone(), vec![]);
+ assert_eq!(snapshot5.text(), "123aโฏc123456eee");
+
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true);
+ let (snapshot6, _) = map.read(inlay_snapshot, vec![]);
+ assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee");
+ }
+
+ #[gpui::test]
+ fn test_adjacent_folds(cx: &mut gpui::AppContext) {
+ init_test(cx);
+ let buffer = MultiBuffer::build_simple("abcdefghijkl", cx);
+ let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+
+ {
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![5..8]);
+ let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ assert_eq!(snapshot.text(), "abcdeโฏijkl");
+
+ // Create an fold adjacent to the start of the first fold.
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![0..1, 2..5]);
+ let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ assert_eq!(snapshot.text(), "โฏbโฏijkl");
+
+ // Create an fold adjacent to the end of the first fold.
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![11..11, 8..10]);
+ let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ assert_eq!(snapshot.text(), "โฏbโฏkl");
+ }
+
+ {
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+ // Create two adjacent folds.
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![0..2, 2..5]);
+ let (snapshot, _) = map.read(inlay_snapshot, vec![]);
+ assert_eq!(snapshot.text(), "โฏfghijkl");
+
+ // Edit within one of the folds.
+ let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+ buffer.edit([(0..1, "12345")], None, cx);
+ buffer.snapshot(cx)
+ });
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
+ assert_eq!(snapshot.text(), "12345โฏfghijkl");
+ }
+ }
+
+ #[gpui::test]
+ fn test_overlapping_folds(cx: &mut gpui::AppContext) {
+ let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![
+ Point::new(0, 2)..Point::new(2, 2),
+ Point::new(0, 4)..Point::new(1, 0),
+ Point::new(1, 2)..Point::new(3, 2),
+ Point::new(3, 1)..Point::new(4, 1),
+ ]);
+ let (snapshot, _) = map.read(inlay_snapshot, vec![]);
+ assert_eq!(snapshot.text(), "aaโฏeeeee");
+ }
+
+ #[gpui::test]
+ fn test_merging_folds_via_edit(cx: &mut gpui::AppContext) {
+ init_test(cx);
+ let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+ let subscription = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![
+ Point::new(0, 2)..Point::new(2, 2),
+ Point::new(3, 1)..Point::new(4, 1),
+ ]);
+ let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ assert_eq!(snapshot.text(), "aaโฏcccc\ndโฏeeeee");
+
+ let buffer_snapshot = buffer.update(cx, |buffer, cx| {
+ buffer.edit([(Point::new(2, 2)..Point::new(3, 1), "")], None, cx);
+ buffer.snapshot(cx)
+ });
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot, subscription.consume().into_inner());
+ let (snapshot, _) = map.read(inlay_snapshot, inlay_edits);
+ assert_eq!(snapshot.text(), "aaโฏeeeee");
+ }
+
+ #[gpui::test]
+ fn test_folds_in_range(cx: &mut gpui::AppContext) {
+ let buffer = MultiBuffer::build_simple(&sample_text(5, 6, 'a'), cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![
+ Point::new(0, 2)..Point::new(2, 2),
+ Point::new(0, 4)..Point::new(1, 0),
+ Point::new(1, 2)..Point::new(3, 2),
+ Point::new(3, 1)..Point::new(4, 1),
+ ]);
+ let (snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ let fold_ranges = snapshot
+ .folds_in_range(Point::new(1, 0)..Point::new(1, 3))
+ .map(|fold| fold.start.to_point(&buffer_snapshot)..fold.end.to_point(&buffer_snapshot))
+ .collect::<Vec<_>>();
+ assert_eq!(
+ fold_ranges,
+ vec![
+ Point::new(0, 2)..Point::new(2, 2),
+ Point::new(1, 2)..Point::new(3, 2)
+ ]
+ );
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_folds(cx: &mut gpui::AppContext, mut rng: StdRng) {
+ init_test(cx);
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let len = rng.gen_range(0..10);
+ let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
+ let buffer = if rng.gen() {
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+ let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+ let (mut initial_snapshot, _) = map.read(inlay_snapshot.clone(), vec![]);
+ let mut snapshot_edits = Vec::new();
+
+ let mut next_inlay_id = 0;
+ for _ in 0..operations {
+ log::info!("text: {:?}", buffer_snapshot.text());
+ let mut buffer_edits = Vec::new();
+ let mut inlay_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=39 => {
+ snapshot_edits.extend(map.randomly_mutate(&mut rng));
+ }
+ 40..=59 => {
+ let (_, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ inlay_edits = edits;
+ }
+ _ => buffer.update(cx, |buffer, cx| {
+ let subscription = buffer.subscribe();
+ let edit_count = rng.gen_range(1..=5);
+ buffer.randomly_mutate(&mut rng, edit_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ let edits = subscription.consume().into_inner();
+ log::info!("editing {:?}", edits);
+ buffer_edits.extend(edits);
+ }),
+ };
+
+ let (inlay_snapshot, new_inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ log::info!("inlay text {:?}", inlay_snapshot.text());
+
+ let inlay_edits = Patch::new(inlay_edits)
+ .compose(new_inlay_edits)
+ .into_inner();
+ let (snapshot, edits) = map.read(inlay_snapshot.clone(), inlay_edits);
+ snapshot_edits.push((snapshot.clone(), edits));
+
+ let mut expected_text: String = inlay_snapshot.text().to_string();
+ for fold_range in map.merged_fold_ranges().into_iter().rev() {
+ let fold_inlay_start = inlay_snapshot.to_inlay_offset(fold_range.start);
+ let fold_inlay_end = inlay_snapshot.to_inlay_offset(fold_range.end);
+ expected_text.replace_range(fold_inlay_start.0..fold_inlay_end.0, "โฏ");
+ }
+
+ assert_eq!(snapshot.text(), expected_text);
+ log::info!(
+ "fold text {:?} ({} lines)",
+ expected_text,
+ expected_text.matches('\n').count() + 1
+ );
+
+ let mut prev_row = 0;
+ let mut expected_buffer_rows = Vec::new();
+ for fold_range in map.merged_fold_ranges().into_iter() {
+ let fold_start = inlay_snapshot
+ .to_point(inlay_snapshot.to_inlay_offset(fold_range.start))
+ .row();
+ let fold_end = inlay_snapshot
+ .to_point(inlay_snapshot.to_inlay_offset(fold_range.end))
+ .row();
+ expected_buffer_rows.extend(
+ inlay_snapshot
+ .buffer_rows(prev_row)
+ .take((1 + fold_start - prev_row) as usize),
+ );
+ prev_row = 1 + fold_end;
+ }
+ expected_buffer_rows.extend(inlay_snapshot.buffer_rows(prev_row));
+
+ assert_eq!(
+ expected_buffer_rows.len(),
+ expected_text.matches('\n').count() + 1,
+ "wrong expected buffer rows {:?}. text: {:?}",
+ expected_buffer_rows,
+ expected_text
+ );
+
+ for (output_row, line) in expected_text.lines().enumerate() {
+ let line_len = snapshot.line_len(output_row as u32);
+ assert_eq!(line_len, line.len() as u32);
+ }
+
+ let longest_row = snapshot.longest_row();
+ let longest_char_column = expected_text
+ .split('\n')
+ .nth(longest_row as usize)
+ .unwrap()
+ .chars()
+ .count();
+ let mut fold_point = FoldPoint::new(0, 0);
+ let mut fold_offset = FoldOffset(0);
+ let mut char_column = 0;
+ for c in expected_text.chars() {
+ let inlay_point = fold_point.to_inlay_point(&snapshot);
+ let inlay_offset = fold_offset.to_inlay_offset(&snapshot);
+ assert_eq!(
+ snapshot.to_fold_point(inlay_point, Right),
+ fold_point,
+ "{:?} -> fold point",
+ inlay_point,
+ );
+ assert_eq!(
+ inlay_snapshot.to_offset(inlay_point),
+ inlay_offset,
+ "inlay_snapshot.to_offset({:?})",
+ inlay_point,
+ );
+ assert_eq!(
+ fold_point.to_offset(&snapshot),
+ fold_offset,
+ "fold_point.to_offset({:?})",
+ fold_point,
+ );
+
+ if c == '\n' {
+ *fold_point.row_mut() += 1;
+ *fold_point.column_mut() = 0;
+ char_column = 0;
+ } else {
+ *fold_point.column_mut() += c.len_utf8() as u32;
+ char_column += 1;
+ }
+ fold_offset.0 += c.len_utf8();
+ if char_column > longest_char_column {
+ panic!(
+ "invalid longest row {:?} (chars {}), found row {:?} (chars: {})",
+ longest_row,
+ longest_char_column,
+ fold_point.row(),
+ char_column
+ );
+ }
+ }
+
+ for _ in 0..5 {
+ let mut start = snapshot
+ .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Left);
+ let mut end = snapshot
+ .clip_offset(FoldOffset(rng.gen_range(0..=snapshot.len().0)), Bias::Right);
+ if start > end {
+ mem::swap(&mut start, &mut end);
+ }
+
+ let text = &expected_text[start.0..end.0];
+ assert_eq!(
+ snapshot
+ .chunks(start..end, false, Highlights::default())
+ .map(|c| c.text)
+ .collect::<String>(),
+ text,
+ );
+ }
+
+ let mut fold_row = 0;
+ while fold_row < expected_buffer_rows.len() as u32 {
+ assert_eq!(
+ snapshot.buffer_rows(fold_row).collect::<Vec<_>>(),
+ expected_buffer_rows[(fold_row as usize)..],
+ "wrong buffer rows starting at fold row {}",
+ fold_row,
+ );
+ fold_row += 1;
+ }
+
+ let folded_buffer_rows = map
+ .merged_fold_ranges()
+ .iter()
+ .flat_map(|range| {
+ let start_row = range.start.to_point(&buffer_snapshot).row;
+ let end = range.end.to_point(&buffer_snapshot);
+ if end.column == 0 {
+ start_row..end.row
+ } else {
+ start_row..end.row + 1
+ }
+ })
+ .collect::<HashSet<_>>();
+ for row in 0..=buffer_snapshot.max_point().row {
+ assert_eq!(
+ snapshot.is_line_folded(row),
+ folded_buffer_rows.contains(&row),
+ "expected buffer row {}{} to be folded",
+ row,
+ if folded_buffer_rows.contains(&row) {
+ ""
+ } else {
+ " not"
+ }
+ );
+ }
+
+ for _ in 0..5 {
+ let end =
+ buffer_snapshot.clip_offset(rng.gen_range(0..=buffer_snapshot.len()), Right);
+ let start = buffer_snapshot.clip_offset(rng.gen_range(0..=end), Left);
+ let expected_folds = map
+ .snapshot
+ .folds
+ .items(&buffer_snapshot)
+ .into_iter()
+ .filter(|fold| {
+ let start = buffer_snapshot.anchor_before(start);
+ let end = buffer_snapshot.anchor_after(end);
+ start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less
+ && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater
+ })
+ .map(|fold| fold.0)
+ .collect::<Vec<_>>();
+
+ assert_eq!(
+ snapshot
+ .folds_in_range(start..end)
+ .cloned()
+ .collect::<Vec<_>>(),
+ expected_folds
+ );
+ }
+
+ let text = snapshot.text();
+ for _ in 0..5 {
+ let start_row = rng.gen_range(0..=snapshot.max_point().row());
+ let start_column = rng.gen_range(0..=snapshot.line_len(start_row));
+ let end_row = rng.gen_range(0..=snapshot.max_point().row());
+ let end_column = rng.gen_range(0..=snapshot.line_len(end_row));
+ let mut start =
+ snapshot.clip_point(FoldPoint::new(start_row, start_column), Bias::Left);
+ let mut end = snapshot.clip_point(FoldPoint::new(end_row, end_column), Bias::Right);
+ if start > end {
+ mem::swap(&mut start, &mut end);
+ }
+
+ let lines = start..end;
+ let bytes = start.to_offset(&snapshot)..end.to_offset(&snapshot);
+ assert_eq!(
+ snapshot.text_summary_for_range(lines),
+ TextSummary::from(&text[bytes.start.0..bytes.end.0])
+ )
+ }
+
+ let mut text = initial_snapshot.text();
+ for (snapshot, edits) in snapshot_edits.drain(..) {
+ let new_text = snapshot.text();
+ for edit in edits {
+ let old_bytes = edit.new.start.0..edit.new.start.0 + edit.old_len().0;
+ let new_bytes = edit.new.start.0..edit.new.end.0;
+ text.replace_range(old_bytes, &new_text[new_bytes]);
+ }
+
+ assert_eq!(text, new_text);
+ initial_snapshot = snapshot;
+ }
+ }
+ }
+
+ #[gpui::test]
+ fn test_buffer_rows(cx: &mut gpui::AppContext) {
+ let text = sample_text(6, 6, 'a') + "\n";
+ let buffer = MultiBuffer::build_simple(&text, cx);
+
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot);
+ let mut map = FoldMap::new(inlay_snapshot.clone()).0;
+
+ let (mut writer, _, _) = map.write(inlay_snapshot.clone(), vec![]);
+ writer.fold(vec![
+ Point::new(0, 2)..Point::new(2, 2),
+ Point::new(3, 1)..Point::new(4, 1),
+ ]);
+
+ let (snapshot, _) = map.read(inlay_snapshot, vec![]);
+ assert_eq!(snapshot.text(), "aaโฏcccc\ndโฏeeeee\nffffff\n");
+ assert_eq!(
+ snapshot.buffer_rows(0).collect::<Vec<_>>(),
+ [Some(0), Some(3), Some(5), Some(6)]
+ );
+ assert_eq!(snapshot.buffer_rows(3).collect::<Vec<_>>(), [Some(6)]);
+ }
+
+ fn init_test(cx: &mut gpui::AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ }
+
+ impl FoldMap {
+ fn merged_fold_ranges(&self) -> Vec<Range<usize>> {
+ let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
+ let buffer = &inlay_snapshot.buffer;
+ let mut folds = self.snapshot.folds.items(buffer);
+ // Ensure sorting doesn't change how folds get merged and displayed.
+ folds.sort_by(|a, b| a.0.cmp(&b.0, buffer));
+ let mut fold_ranges = folds
+ .iter()
+ .map(|fold| fold.0.start.to_offset(buffer)..fold.0.end.to_offset(buffer))
+ .peekable();
+
+ let mut merged_ranges = Vec::new();
+ while let Some(mut fold_range) = fold_ranges.next() {
+ while let Some(next_range) = fold_ranges.peek() {
+ if fold_range.end >= next_range.start {
+ if next_range.end > fold_range.end {
+ fold_range.end = next_range.end;
+ }
+ fold_ranges.next();
+ } else {
+ break;
+ }
+ }
+ if fold_range.end > fold_range.start {
+ merged_ranges.push(fold_range);
+ }
+ }
+ merged_ranges
+ }
+
+ pub fn randomly_mutate(
+ &mut self,
+ rng: &mut impl Rng,
+ ) -> Vec<(FoldSnapshot, Vec<FoldEdit>)> {
+ let mut snapshot_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=39 if !self.snapshot.folds.is_empty() => {
+ let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
+ let buffer = &inlay_snapshot.buffer;
+ let mut to_unfold = Vec::new();
+ for _ in 0..rng.gen_range(1..=3) {
+ let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+ let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+ to_unfold.push(start..end);
+ }
+ log::info!("unfolding {:?}", to_unfold);
+ let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
+ snapshot_edits.push((snapshot, edits));
+ let (snapshot, edits) = writer.fold(to_unfold);
+ snapshot_edits.push((snapshot, edits));
+ }
+ _ => {
+ let inlay_snapshot = self.snapshot.inlay_snapshot.clone();
+ let buffer = &inlay_snapshot.buffer;
+ let mut to_fold = Vec::new();
+ for _ in 0..rng.gen_range(1..=2) {
+ let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
+ let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
+ to_fold.push(start..end);
+ }
+ log::info!("folding {:?}", to_fold);
+ let (mut writer, snapshot, edits) = self.write(inlay_snapshot, vec![]);
+ snapshot_edits.push((snapshot, edits));
+ let (snapshot, edits) = writer.fold(to_fold);
+ snapshot_edits.push((snapshot, edits));
+ }
+ }
+ snapshot_edits
+ }
+ }
+}
@@ -0,0 +1,1895 @@
+use crate::{Anchor, InlayId, MultiBufferSnapshot, ToOffset};
+use collections::{BTreeMap, BTreeSet};
+use gpui::fonts::HighlightStyle;
+use language::{Chunk, Edit, Point, TextSummary};
+use multi_buffer::{MultiBufferChunks, MultiBufferRows};
+use std::{
+ any::TypeId,
+ cmp,
+ iter::Peekable,
+ ops::{Add, AddAssign, Range, Sub, SubAssign},
+ sync::Arc,
+ vec,
+};
+use sum_tree::{Bias, Cursor, SumTree, TreeMap};
+use text::{Patch, Rope};
+
+use super::Highlights;
+
+pub struct InlayMap {
+ snapshot: InlaySnapshot,
+ inlays: Vec<Inlay>,
+}
+
+#[derive(Clone)]
+pub struct InlaySnapshot {
+ pub buffer: MultiBufferSnapshot,
+ transforms: SumTree<Transform>,
+ pub version: usize,
+}
+
+#[derive(Clone, Debug)]
+enum Transform {
+ Isomorphic(TextSummary),
+ Inlay(Inlay),
+}
+
+#[derive(Debug, Clone)]
+pub struct Inlay {
+ pub id: InlayId,
+ pub position: Anchor,
+ pub text: text::Rope,
+}
+
+impl Inlay {
+ pub fn hint(id: usize, position: Anchor, hint: &project::InlayHint) -> Self {
+ let mut text = hint.text();
+ if hint.padding_right && !text.ends_with(' ') {
+ text.push(' ');
+ }
+ if hint.padding_left && !text.starts_with(' ') {
+ text.insert(0, ' ');
+ }
+ Self {
+ id: InlayId::Hint(id),
+ position,
+ text: text.into(),
+ }
+ }
+
+ pub fn suggestion<T: Into<Rope>>(id: usize, position: Anchor, text: T) -> Self {
+ Self {
+ id: InlayId::Suggestion(id),
+ position,
+ text: text.into(),
+ }
+ }
+}
+
+impl sum_tree::Item for Transform {
+ type Summary = TransformSummary;
+
+ fn summary(&self) -> Self::Summary {
+ match self {
+ Transform::Isomorphic(summary) => TransformSummary {
+ input: summary.clone(),
+ output: summary.clone(),
+ },
+ Transform::Inlay(inlay) => TransformSummary {
+ input: TextSummary::default(),
+ output: inlay.text.summary(),
+ },
+ }
+ }
+}
+
+#[derive(Clone, Debug, Default)]
+struct TransformSummary {
+ input: TextSummary,
+ output: TextSummary,
+}
+
+impl sum_tree::Summary for TransformSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, other: &Self, _: &()) {
+ self.input += &other.input;
+ self.output += &other.output;
+ }
+}
+
+pub type InlayEdit = Edit<InlayOffset>;
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayOffset(pub usize);
+
+impl Add for InlayOffset {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self(self.0 + rhs.0)
+ }
+}
+
+impl Sub for InlayOffset {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Self(self.0 - rhs.0)
+ }
+}
+
+impl AddAssign for InlayOffset {
+ fn add_assign(&mut self, rhs: Self) {
+ self.0 += rhs.0;
+ }
+}
+
+impl SubAssign for InlayOffset {
+ fn sub_assign(&mut self, rhs: Self) {
+ self.0 -= rhs.0;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayOffset {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.len;
+ }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct InlayPoint(pub Point);
+
+impl Add for InlayPoint {
+ type Output = Self;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Self(self.0 + rhs.0)
+ }
+}
+
+impl Sub for InlayPoint {
+ type Output = Self;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Self(self.0 - rhs.0)
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for InlayPoint {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += &summary.output.lines;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for usize {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ *self += &summary.input.len;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for Point {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ *self += &summary.input.lines;
+ }
+}
+
+#[derive(Clone)]
+pub struct InlayBufferRows<'a> {
+ transforms: Cursor<'a, Transform, (InlayPoint, Point)>,
+ buffer_rows: MultiBufferRows<'a>,
+ inlay_row: u32,
+ max_buffer_row: u32,
+}
+
+#[derive(Debug, Copy, Clone, Eq, PartialEq)]
+struct HighlightEndpoint {
+ offset: InlayOffset,
+ is_start: bool,
+ tag: Option<TypeId>,
+ style: HighlightStyle,
+}
+
+impl PartialOrd for HighlightEndpoint {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+impl Ord for HighlightEndpoint {
+ fn cmp(&self, other: &Self) -> cmp::Ordering {
+ self.offset
+ .cmp(&other.offset)
+ .then_with(|| other.is_start.cmp(&self.is_start))
+ }
+}
+
+pub struct InlayChunks<'a> {
+ transforms: Cursor<'a, Transform, (InlayOffset, usize)>,
+ buffer_chunks: MultiBufferChunks<'a>,
+ buffer_chunk: Option<Chunk<'a>>,
+ inlay_chunks: Option<text::Chunks<'a>>,
+ inlay_chunk: Option<&'a str>,
+ output_offset: InlayOffset,
+ max_output_offset: InlayOffset,
+ inlay_highlight_style: Option<HighlightStyle>,
+ suggestion_highlight_style: Option<HighlightStyle>,
+ highlight_endpoints: Peekable<vec::IntoIter<HighlightEndpoint>>,
+ active_highlights: BTreeMap<Option<TypeId>, HighlightStyle>,
+ highlights: Highlights<'a>,
+ snapshot: &'a InlaySnapshot,
+}
+
+impl<'a> InlayChunks<'a> {
+ pub fn seek(&mut self, offset: InlayOffset) {
+ self.transforms.seek(&offset, Bias::Right, &());
+
+ let buffer_offset = self.snapshot.to_buffer_offset(offset);
+ self.buffer_chunks.seek(buffer_offset);
+ self.inlay_chunks = None;
+ self.buffer_chunk = None;
+ self.output_offset = offset;
+ }
+
+ pub fn offset(&self) -> InlayOffset {
+ self.output_offset
+ }
+}
+
+impl<'a> Iterator for InlayChunks<'a> {
+ type Item = Chunk<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.output_offset == self.max_output_offset {
+ return None;
+ }
+
+ let mut next_highlight_endpoint = InlayOffset(usize::MAX);
+ while let Some(endpoint) = self.highlight_endpoints.peek().copied() {
+ if endpoint.offset <= self.output_offset {
+ if endpoint.is_start {
+ self.active_highlights.insert(endpoint.tag, endpoint.style);
+ } else {
+ self.active_highlights.remove(&endpoint.tag);
+ }
+ self.highlight_endpoints.next();
+ } else {
+ next_highlight_endpoint = endpoint.offset;
+ break;
+ }
+ }
+
+ let chunk = match self.transforms.item()? {
+ Transform::Isomorphic(_) => {
+ let chunk = self
+ .buffer_chunk
+ .get_or_insert_with(|| self.buffer_chunks.next().unwrap());
+ if chunk.text.is_empty() {
+ *chunk = self.buffer_chunks.next().unwrap();
+ }
+
+ let (prefix, suffix) = chunk.text.split_at(
+ chunk
+ .text
+ .len()
+ .min(self.transforms.end(&()).0 .0 - self.output_offset.0)
+ .min(next_highlight_endpoint.0 - self.output_offset.0),
+ );
+
+ chunk.text = suffix;
+ self.output_offset.0 += prefix.len();
+ let mut prefix = Chunk {
+ text: prefix,
+ ..chunk.clone()
+ };
+ if !self.active_highlights.is_empty() {
+ let mut highlight_style = HighlightStyle::default();
+ for active_highlight in self.active_highlights.values() {
+ highlight_style.highlight(*active_highlight);
+ }
+ prefix.highlight_style = Some(highlight_style);
+ }
+ prefix
+ }
+ Transform::Inlay(inlay) => {
+ let mut inlay_style_and_highlight = None;
+ if let Some(inlay_highlights) = self.highlights.inlay_highlights {
+ for (_, inlay_id_to_data) in inlay_highlights.iter() {
+ let style_and_highlight = inlay_id_to_data.get(&inlay.id);
+ if style_and_highlight.is_some() {
+ inlay_style_and_highlight = style_and_highlight;
+ break;
+ }
+ }
+ }
+
+ let mut highlight_style = match inlay.id {
+ InlayId::Suggestion(_) => self.suggestion_highlight_style,
+ InlayId::Hint(_) => self.inlay_highlight_style,
+ };
+ let next_inlay_highlight_endpoint;
+ let offset_in_inlay = self.output_offset - self.transforms.start().0;
+ if let Some((style, highlight)) = inlay_style_and_highlight {
+ let range = &highlight.range;
+ if offset_in_inlay.0 < range.start {
+ next_inlay_highlight_endpoint = range.start - offset_in_inlay.0;
+ } else if offset_in_inlay.0 >= range.end {
+ next_inlay_highlight_endpoint = usize::MAX;
+ } else {
+ next_inlay_highlight_endpoint = range.end - offset_in_inlay.0;
+ highlight_style
+ .get_or_insert_with(|| Default::default())
+ .highlight(style.clone());
+ }
+ } else {
+ next_inlay_highlight_endpoint = usize::MAX;
+ }
+
+ let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| {
+ let start = offset_in_inlay;
+ let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0)
+ - self.transforms.start().0;
+ inlay.text.chunks_in_range(start.0..end.0)
+ });
+ let inlay_chunk = self
+ .inlay_chunk
+ .get_or_insert_with(|| inlay_chunks.next().unwrap());
+ let (chunk, remainder) =
+ inlay_chunk.split_at(inlay_chunk.len().min(next_inlay_highlight_endpoint));
+ *inlay_chunk = remainder;
+ if inlay_chunk.is_empty() {
+ self.inlay_chunk = None;
+ }
+
+ self.output_offset.0 += chunk.len();
+
+ if !self.active_highlights.is_empty() {
+ for active_highlight in self.active_highlights.values() {
+ highlight_style
+ .get_or_insert(Default::default())
+ .highlight(*active_highlight);
+ }
+ }
+ Chunk {
+ text: chunk,
+ highlight_style,
+ ..Default::default()
+ }
+ }
+ };
+
+ if self.output_offset == self.transforms.end(&()).0 {
+ self.inlay_chunks = None;
+ self.transforms.next(&());
+ }
+
+ Some(chunk)
+ }
+}
+
+impl<'a> InlayBufferRows<'a> {
+ pub fn seek(&mut self, row: u32) {
+ let inlay_point = InlayPoint::new(row, 0);
+ self.transforms.seek(&inlay_point, Bias::Left, &());
+
+ let mut buffer_point = self.transforms.start().1;
+ let buffer_row = if row == 0 {
+ 0
+ } else {
+ match self.transforms.item() {
+ Some(Transform::Isomorphic(_)) => {
+ buffer_point += inlay_point.0 - self.transforms.start().0 .0;
+ buffer_point.row
+ }
+ _ => cmp::min(buffer_point.row + 1, self.max_buffer_row),
+ }
+ };
+ self.inlay_row = inlay_point.row();
+ self.buffer_rows.seek(buffer_row);
+ }
+}
+
+impl<'a> Iterator for InlayBufferRows<'a> {
+ type Item = Option<u32>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let buffer_row = if self.inlay_row == 0 {
+ self.buffer_rows.next().unwrap()
+ } else {
+ match self.transforms.item()? {
+ Transform::Inlay(_) => None,
+ Transform::Isomorphic(_) => self.buffer_rows.next().unwrap(),
+ }
+ };
+
+ self.inlay_row += 1;
+ self.transforms
+ .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &());
+
+ Some(buffer_row)
+ }
+}
+
+impl InlayPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(Point::new(row, column))
+ }
+
+ pub fn row(self) -> u32 {
+ self.0.row
+ }
+}
+
+impl InlayMap {
+ pub fn new(buffer: MultiBufferSnapshot) -> (Self, InlaySnapshot) {
+ let version = 0;
+ let snapshot = InlaySnapshot {
+ buffer: buffer.clone(),
+ transforms: SumTree::from_iter(Some(Transform::Isomorphic(buffer.text_summary())), &()),
+ version,
+ };
+
+ (
+ Self {
+ snapshot: snapshot.clone(),
+ inlays: Vec::new(),
+ },
+ snapshot,
+ )
+ }
+
+ pub fn sync(
+ &mut self,
+ buffer_snapshot: MultiBufferSnapshot,
+ mut buffer_edits: Vec<text::Edit<usize>>,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ let snapshot = &mut self.snapshot;
+
+ if buffer_edits.is_empty() {
+ if snapshot.buffer.trailing_excerpt_update_count()
+ != buffer_snapshot.trailing_excerpt_update_count()
+ {
+ buffer_edits.push(Edit {
+ old: snapshot.buffer.len()..snapshot.buffer.len(),
+ new: buffer_snapshot.len()..buffer_snapshot.len(),
+ });
+ }
+ }
+
+ if buffer_edits.is_empty() {
+ if snapshot.buffer.edit_count() != buffer_snapshot.edit_count()
+ || snapshot.buffer.parse_count() != buffer_snapshot.parse_count()
+ || snapshot.buffer.diagnostics_update_count()
+ != buffer_snapshot.diagnostics_update_count()
+ || snapshot.buffer.git_diff_update_count()
+ != buffer_snapshot.git_diff_update_count()
+ || snapshot.buffer.trailing_excerpt_update_count()
+ != buffer_snapshot.trailing_excerpt_update_count()
+ {
+ snapshot.version += 1;
+ }
+
+ snapshot.buffer = buffer_snapshot;
+ (snapshot.clone(), Vec::new())
+ } else {
+ let mut inlay_edits = Patch::default();
+ let mut new_transforms = SumTree::new();
+ let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>();
+ let mut buffer_edits_iter = buffer_edits.iter().peekable();
+ while let Some(buffer_edit) = buffer_edits_iter.next() {
+ new_transforms.append(cursor.slice(&buffer_edit.old.start, Bias::Left, &()), &());
+ if let Some(Transform::Isomorphic(transform)) = cursor.item() {
+ if cursor.end(&()).0 == buffer_edit.old.start {
+ push_isomorphic(&mut new_transforms, transform.clone());
+ cursor.next(&());
+ }
+ }
+
+ // Remove all the inlays and transforms contained by the edit.
+ let old_start =
+ cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0);
+ cursor.seek(&buffer_edit.old.end, Bias::Right, &());
+ let old_end =
+ cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0);
+
+ // Push the unchanged prefix.
+ let prefix_start = new_transforms.summary().input.len;
+ let prefix_end = buffer_edit.new.start;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+ );
+ let new_start = InlayOffset(new_transforms.summary().output.len);
+
+ let start_ix = match self.inlays.binary_search_by(|probe| {
+ probe
+ .position
+ .to_offset(&buffer_snapshot)
+ .cmp(&buffer_edit.new.start)
+ .then(std::cmp::Ordering::Greater)
+ }) {
+ Ok(ix) | Err(ix) => ix,
+ };
+
+ for inlay in &self.inlays[start_ix..] {
+ let buffer_offset = inlay.position.to_offset(&buffer_snapshot);
+ if buffer_offset > buffer_edit.new.end {
+ break;
+ }
+
+ let prefix_start = new_transforms.summary().input.len;
+ let prefix_end = buffer_offset;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(prefix_start..prefix_end),
+ );
+
+ if inlay.position.is_valid(&buffer_snapshot) {
+ new_transforms.push(Transform::Inlay(inlay.clone()), &());
+ }
+ }
+
+ // Apply the rest of the edit.
+ let transform_start = new_transforms.summary().input.len;
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(transform_start..buffer_edit.new.end),
+ );
+ let new_end = InlayOffset(new_transforms.summary().output.len);
+ inlay_edits.push(Edit {
+ old: old_start..old_end,
+ new: new_start..new_end,
+ });
+
+ // If the next edit doesn't intersect the current isomorphic transform, then
+ // we can push its remainder.
+ if buffer_edits_iter
+ .peek()
+ .map_or(true, |edit| edit.old.start >= cursor.end(&()).0)
+ {
+ let transform_start = new_transforms.summary().input.len;
+ let transform_end =
+ buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end);
+ push_isomorphic(
+ &mut new_transforms,
+ buffer_snapshot.text_summary_for_range(transform_start..transform_end),
+ );
+ cursor.next(&());
+ }
+ }
+
+ new_transforms.append(cursor.suffix(&()), &());
+ if new_transforms.is_empty() {
+ new_transforms.push(Transform::Isomorphic(Default::default()), &());
+ }
+
+ drop(cursor);
+ snapshot.transforms = new_transforms;
+ snapshot.version += 1;
+ snapshot.buffer = buffer_snapshot;
+ snapshot.check_invariants();
+
+ (snapshot.clone(), inlay_edits.into_inner())
+ }
+ }
+
+ pub fn splice(
+ &mut self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<Inlay>,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ let snapshot = &mut self.snapshot;
+ let mut edits = BTreeSet::new();
+
+ self.inlays.retain(|inlay| {
+ let retain = !to_remove.contains(&inlay.id);
+ if !retain {
+ let offset = inlay.position.to_offset(&snapshot.buffer);
+ edits.insert(offset);
+ }
+ retain
+ });
+
+ for inlay_to_insert in to_insert {
+ // Avoid inserting empty inlays.
+ if inlay_to_insert.text.is_empty() {
+ continue;
+ }
+
+ let offset = inlay_to_insert.position.to_offset(&snapshot.buffer);
+ match self.inlays.binary_search_by(|probe| {
+ probe
+ .position
+ .cmp(&inlay_to_insert.position, &snapshot.buffer)
+ }) {
+ Ok(ix) | Err(ix) => {
+ self.inlays.insert(ix, inlay_to_insert);
+ }
+ }
+
+ edits.insert(offset);
+ }
+
+ let buffer_edits = edits
+ .into_iter()
+ .map(|offset| Edit {
+ old: offset..offset,
+ new: offset..offset,
+ })
+ .collect();
+ let buffer_snapshot = snapshot.buffer.clone();
+ let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits);
+ (snapshot, edits)
+ }
+
+ pub fn current_inlays(&self) -> impl Iterator<Item = &Inlay> {
+ self.inlays.iter()
+ }
+
+ #[cfg(test)]
+ pub(crate) fn randomly_mutate(
+ &mut self,
+ next_inlay_id: &mut usize,
+ rng: &mut rand::rngs::StdRng,
+ ) -> (InlaySnapshot, Vec<InlayEdit>) {
+ use rand::prelude::*;
+ use util::post_inc;
+
+ let mut to_remove = Vec::new();
+ let mut to_insert = Vec::new();
+ let snapshot = &mut self.snapshot;
+ for i in 0..rng.gen_range(1..=5) {
+ if self.inlays.is_empty() || rng.gen() {
+ let position = snapshot.buffer.random_byte_range(0, rng).start;
+ let bias = if rng.gen() { Bias::Left } else { Bias::Right };
+ let len = if rng.gen_bool(0.01) {
+ 0
+ } else {
+ rng.gen_range(1..=5)
+ };
+ let text = util::RandomCharIter::new(&mut *rng)
+ .filter(|ch| *ch != '\r')
+ .take(len)
+ .collect::<String>();
+
+ let inlay_id = if i % 2 == 0 {
+ InlayId::Hint(post_inc(next_inlay_id))
+ } else {
+ InlayId::Suggestion(post_inc(next_inlay_id))
+ };
+ log::info!(
+ "creating inlay {:?} at buffer offset {} with bias {:?} and text {:?}",
+ inlay_id,
+ position,
+ bias,
+ text
+ );
+
+ to_insert.push(Inlay {
+ id: inlay_id,
+ position: snapshot.buffer.anchor_at(position, bias),
+ text: text.into(),
+ });
+ } else {
+ to_remove.push(
+ self.inlays
+ .iter()
+ .choose(rng)
+ .map(|inlay| inlay.id)
+ .unwrap(),
+ );
+ }
+ }
+ log::info!("removing inlays: {:?}", to_remove);
+
+ let (snapshot, edits) = self.splice(to_remove, to_insert);
+ (snapshot, edits)
+ }
+}
+
+impl InlaySnapshot {
+ pub fn to_point(&self, offset: InlayOffset) -> InlayPoint {
+ let mut cursor = self
+ .transforms
+ .cursor::<(InlayOffset, (InlayPoint, usize))>();
+ cursor.seek(&offset, Bias::Right, &());
+ let overshoot = offset.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_offset_start = cursor.start().1 .1;
+ let buffer_offset_end = buffer_offset_start + overshoot;
+ let buffer_start = self.buffer.offset_to_point(buffer_offset_start);
+ let buffer_end = self.buffer.offset_to_point(buffer_offset_end);
+ InlayPoint(cursor.start().1 .0 .0 + (buffer_end - buffer_start))
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let overshoot = inlay.text.offset_to_point(overshoot);
+ InlayPoint(cursor.start().1 .0 .0 + overshoot)
+ }
+ None => self.max_point(),
+ }
+ }
+
+ pub fn len(&self) -> InlayOffset {
+ InlayOffset(self.transforms.summary().output.len)
+ }
+
+ pub fn max_point(&self) -> InlayPoint {
+ InlayPoint(self.transforms.summary().output.lines)
+ }
+
+ pub fn to_offset(&self, point: InlayPoint) -> InlayOffset {
+ let mut cursor = self
+ .transforms
+ .cursor::<(InlayPoint, (InlayOffset, Point))>();
+ cursor.seek(&point, Bias::Right, &());
+ let overshoot = point.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_point_start = cursor.start().1 .1;
+ let buffer_point_end = buffer_point_start + overshoot;
+ let buffer_offset_start = self.buffer.point_to_offset(buffer_point_start);
+ let buffer_offset_end = self.buffer.point_to_offset(buffer_point_end);
+ InlayOffset(cursor.start().1 .0 .0 + (buffer_offset_end - buffer_offset_start))
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let overshoot = inlay.text.point_to_offset(overshoot);
+ InlayOffset(cursor.start().1 .0 .0 + overshoot)
+ }
+ None => self.len(),
+ }
+ }
+
+ pub fn to_buffer_point(&self, point: InlayPoint) -> Point {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ cursor.seek(&point, Bias::Right, &());
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let overshoot = point.0 - cursor.start().0 .0;
+ cursor.start().1 + overshoot
+ }
+ Some(Transform::Inlay(_)) => cursor.start().1,
+ None => self.buffer.max_point(),
+ }
+ }
+
+ pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize {
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&offset, Bias::Right, &());
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let overshoot = offset - cursor.start().0;
+ cursor.start().1 + overshoot.0
+ }
+ Some(Transform::Inlay(_)) => cursor.start().1,
+ None => self.buffer.len(),
+ }
+ }
+
+ pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset {
+ let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>();
+ cursor.seek(&offset, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ if offset == cursor.end(&()).0 {
+ while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ break;
+ } else {
+ cursor.next(&());
+ }
+ }
+ return cursor.end(&()).1;
+ } else {
+ let overshoot = offset - cursor.start().0;
+ return InlayOffset(cursor.start().1 .0 + overshoot);
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ cursor.next(&());
+ } else {
+ return cursor.start().1;
+ }
+ }
+ None => {
+ return self.len();
+ }
+ }
+ }
+ }
+
+ pub fn to_inlay_point(&self, point: Point) -> InlayPoint {
+ let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>();
+ cursor.seek(&point, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ if point == cursor.end(&()).0 {
+ while let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ break;
+ } else {
+ cursor.next(&());
+ }
+ }
+ return cursor.end(&()).1;
+ } else {
+ let overshoot = point - cursor.start().0;
+ return InlayPoint(cursor.start().1 .0 + overshoot);
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ cursor.next(&());
+ } else {
+ return cursor.start().1;
+ }
+ }
+ None => {
+ return self.max_point();
+ }
+ }
+ }
+ }
+
+ pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ cursor.seek(&point, Bias::Left, &());
+ loop {
+ match cursor.item() {
+ Some(Transform::Isomorphic(transform)) => {
+ if cursor.start().0 == point {
+ if let Some(Transform::Inlay(inlay)) = cursor.prev_item() {
+ if inlay.position.bias() == Bias::Left {
+ return point;
+ } else if bias == Bias::Left {
+ cursor.prev(&());
+ } else if transform.first_line_chars == 0 {
+ point.0 += Point::new(1, 0);
+ } else {
+ point.0 += Point::new(0, 1);
+ }
+ } else {
+ return point;
+ }
+ } else if cursor.end(&()).0 == point {
+ if let Some(Transform::Inlay(inlay)) = cursor.next_item() {
+ if inlay.position.bias() == Bias::Right {
+ return point;
+ } else if bias == Bias::Right {
+ cursor.next(&());
+ } else if point.0.column == 0 {
+ point.0.row -= 1;
+ point.0.column = self.line_len(point.0.row);
+ } else {
+ point.0.column -= 1;
+ }
+ } else {
+ return point;
+ }
+ } else {
+ let overshoot = point.0 - cursor.start().0 .0;
+ let buffer_point = cursor.start().1 + overshoot;
+ let clipped_buffer_point = self.buffer.clip_point(buffer_point, bias);
+ let clipped_overshoot = clipped_buffer_point - cursor.start().1;
+ let clipped_point = InlayPoint(cursor.start().0 .0 + clipped_overshoot);
+ if clipped_point == point {
+ return clipped_point;
+ } else {
+ point = clipped_point;
+ }
+ }
+ }
+ Some(Transform::Inlay(inlay)) => {
+ if point == cursor.start().0 && inlay.position.bias() == Bias::Right {
+ match cursor.prev_item() {
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Left {
+ return point;
+ }
+ }
+ _ => return point,
+ }
+ } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left {
+ match cursor.next_item() {
+ Some(Transform::Inlay(inlay)) => {
+ if inlay.position.bias() == Bias::Right {
+ return point;
+ }
+ }
+ _ => return point,
+ }
+ }
+
+ if bias == Bias::Left {
+ point = cursor.start().0;
+ cursor.prev(&());
+ } else {
+ cursor.next(&());
+ point = cursor.start().0;
+ }
+ }
+ None => {
+ bias = bias.invert();
+ if bias == Bias::Left {
+ point = cursor.start().0;
+ cursor.prev(&());
+ } else {
+ cursor.next(&());
+ point = cursor.start().0;
+ }
+ }
+ }
+ }
+ }
+
+ pub fn text_summary(&self) -> TextSummary {
+ self.transforms.summary().output.clone()
+ }
+
+ pub fn text_summary_for_range(&self, range: Range<InlayOffset>) -> TextSummary {
+ let mut summary = TextSummary::default();
+
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&range.start, Bias::Right, &());
+
+ let overshoot = range.start.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let buffer_start = cursor.start().1;
+ let suffix_start = buffer_start + overshoot;
+ let suffix_end =
+ buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0);
+ summary = self.buffer.text_summary_for_range(suffix_start..suffix_end);
+ cursor.next(&());
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let suffix_start = overshoot;
+ let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0 .0;
+ summary = inlay.text.cursor(suffix_start).summary(suffix_end);
+ cursor.next(&());
+ }
+ None => {}
+ }
+
+ if range.end > cursor.start().0 {
+ summary += cursor
+ .summary::<_, TransformSummary>(&range.end, Bias::Right, &())
+ .output;
+
+ let overshoot = range.end.0 - cursor.start().0 .0;
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ let prefix_start = cursor.start().1;
+ let prefix_end = prefix_start + overshoot;
+ summary += self
+ .buffer
+ .text_summary_for_range::<TextSummary, _>(prefix_start..prefix_end);
+ }
+ Some(Transform::Inlay(inlay)) => {
+ let prefix_end = overshoot;
+ summary += inlay.text.cursor(0).summary::<TextSummary>(prefix_end);
+ }
+ None => {}
+ }
+ }
+
+ summary
+ }
+
+ pub fn buffer_rows<'a>(&'a self, row: u32) -> InlayBufferRows<'a> {
+ let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>();
+ let inlay_point = InlayPoint::new(row, 0);
+ cursor.seek(&inlay_point, Bias::Left, &());
+
+ let max_buffer_row = self.buffer.max_point().row;
+ let mut buffer_point = cursor.start().1;
+ let buffer_row = if row == 0 {
+ 0
+ } else {
+ match cursor.item() {
+ Some(Transform::Isomorphic(_)) => {
+ buffer_point += inlay_point.0 - cursor.start().0 .0;
+ buffer_point.row
+ }
+ _ => cmp::min(buffer_point.row + 1, max_buffer_row),
+ }
+ };
+
+ InlayBufferRows {
+ transforms: cursor,
+ inlay_row: inlay_point.row(),
+ buffer_rows: self.buffer.buffer_rows(buffer_row),
+ max_buffer_row,
+ }
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ let line_start = self.to_offset(InlayPoint::new(row, 0)).0;
+ let line_end = if row >= self.max_point().row() {
+ self.len().0
+ } else {
+ self.to_offset(InlayPoint::new(row + 1, 0)).0 - 1
+ };
+ (line_end - line_start) as u32
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ range: Range<InlayOffset>,
+ language_aware: bool,
+ highlights: Highlights<'a>,
+ ) -> InlayChunks<'a> {
+ let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
+ cursor.seek(&range.start, Bias::Right, &());
+
+ let mut highlight_endpoints = Vec::new();
+ if let Some(text_highlights) = highlights.text_highlights {
+ if !text_highlights.is_empty() {
+ self.apply_text_highlights(
+ &mut cursor,
+ &range,
+ text_highlights,
+ &mut highlight_endpoints,
+ );
+ cursor.seek(&range.start, Bias::Right, &());
+ }
+ }
+ highlight_endpoints.sort();
+ let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end);
+ let buffer_chunks = self.buffer.chunks(buffer_range, language_aware);
+
+ InlayChunks {
+ transforms: cursor,
+ buffer_chunks,
+ inlay_chunks: None,
+ inlay_chunk: None,
+ buffer_chunk: None,
+ output_offset: range.start,
+ max_output_offset: range.end,
+ inlay_highlight_style: highlights.inlay_highlight_style,
+ suggestion_highlight_style: highlights.suggestion_highlight_style,
+ highlight_endpoints: highlight_endpoints.into_iter().peekable(),
+ active_highlights: Default::default(),
+ highlights,
+ snapshot: self,
+ }
+ }
+
+ fn apply_text_highlights(
+ &self,
+ cursor: &mut Cursor<'_, Transform, (InlayOffset, usize)>,
+ range: &Range<InlayOffset>,
+ text_highlights: &TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>,
+ highlight_endpoints: &mut Vec<HighlightEndpoint>,
+ ) {
+ while cursor.start().0 < range.end {
+ let transform_start = self
+ .buffer
+ .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0)));
+ let transform_end =
+ {
+ let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0);
+ self.buffer.anchor_before(self.to_buffer_offset(cmp::min(
+ cursor.end(&()).0,
+ cursor.start().0 + overshoot,
+ )))
+ };
+
+ for (tag, text_highlights) in text_highlights.iter() {
+ let style = text_highlights.0;
+ let ranges = &text_highlights.1;
+
+ let start_ix = match ranges.binary_search_by(|probe| {
+ let cmp = probe.end.cmp(&transform_start, &self.buffer);
+ if cmp.is_gt() {
+ cmp::Ordering::Greater
+ } else {
+ cmp::Ordering::Less
+ }
+ }) {
+ Ok(i) | Err(i) => i,
+ };
+ for range in &ranges[start_ix..] {
+ if range.start.cmp(&transform_end, &self.buffer).is_ge() {
+ break;
+ }
+
+ highlight_endpoints.push(HighlightEndpoint {
+ offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)),
+ is_start: true,
+ tag: *tag,
+ style,
+ });
+ highlight_endpoints.push(HighlightEndpoint {
+ offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)),
+ is_start: false,
+ tag: *tag,
+ style,
+ });
+ }
+ }
+
+ cursor.next(&());
+ }
+ }
+
+ #[cfg(test)]
+ pub fn text(&self) -> String {
+ self.chunks(Default::default()..self.len(), false, Highlights::default())
+ .map(|chunk| chunk.text)
+ .collect()
+ }
+
+ fn check_invariants(&self) {
+ #[cfg(any(debug_assertions, feature = "test-support"))]
+ {
+ assert_eq!(self.transforms.summary().input, self.buffer.text_summary());
+ let mut transforms = self.transforms.iter().peekable();
+ while let Some(transform) = transforms.next() {
+ let transform_is_isomorphic = matches!(transform, Transform::Isomorphic(_));
+ if let Some(next_transform) = transforms.peek() {
+ let next_transform_is_isomorphic =
+ matches!(next_transform, Transform::Isomorphic(_));
+ assert!(
+ !transform_is_isomorphic || !next_transform_is_isomorphic,
+ "two adjacent isomorphic transforms"
+ );
+ }
+ }
+ }
+ }
+}
+
+fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
+ if summary.len == 0 {
+ return;
+ }
+
+ let mut summary = Some(summary);
+ sum_tree.update_last(
+ |transform| {
+ if let Transform::Isomorphic(transform) = transform {
+ *transform += summary.take().unwrap();
+ }
+ },
+ &(),
+ );
+
+ if let Some(summary) = summary {
+ sum_tree.push(Transform::Isomorphic(summary), &());
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::{InlayHighlights, TextHighlights},
+ link_go_to_definition::InlayHighlight,
+ InlayId, MultiBuffer,
+ };
+ use gpui::AppContext;
+ use project::{InlayHint, InlayHintLabel, ResolveState};
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use std::{cmp::Reverse, env, sync::Arc};
+ use text::Patch;
+ use util::post_inc;
+
+ #[test]
+ fn test_inlay_properties_label_padding() {
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String("a".to_string()),
+ position: text::Anchor::default(),
+ padding_left: false,
+ padding_right: false,
+ tooltip: None,
+ kind: None,
+ resolve_state: ResolveState::Resolved,
+ },
+ )
+ .text
+ .to_string(),
+ "a",
+ "Should not pad label if not requested"
+ );
+
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String("a".to_string()),
+ position: text::Anchor::default(),
+ padding_left: true,
+ padding_right: true,
+ tooltip: None,
+ kind: None,
+ resolve_state: ResolveState::Resolved,
+ },
+ )
+ .text
+ .to_string(),
+ " a ",
+ "Should pad label for every side requested"
+ );
+
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String(" a ".to_string()),
+ position: text::Anchor::default(),
+ padding_left: false,
+ padding_right: false,
+ tooltip: None,
+ kind: None,
+ resolve_state: ResolveState::Resolved,
+ },
+ )
+ .text
+ .to_string(),
+ " a ",
+ "Should not change already padded label"
+ );
+
+ assert_eq!(
+ Inlay::hint(
+ 0,
+ Anchor::min(),
+ &InlayHint {
+ label: InlayHintLabel::String(" a ".to_string()),
+ position: text::Anchor::default(),
+ padding_left: true,
+ padding_right: true,
+ tooltip: None,
+ kind: None,
+ resolve_state: ResolveState::Resolved,
+ },
+ )
+ .text
+ .to_string(),
+ " a ",
+ "Should not change already padded label"
+ );
+ }
+
+ #[gpui::test]
+ fn test_basic_inlays(cx: &mut AppContext) {
+ let buffer = MultiBuffer::build_simple("abcdefghi", cx);
+ let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ assert_eq!(inlay_snapshot.text(), "abcdefghi");
+ let mut next_inlay_id = 0;
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|123|".into(),
+ }],
+ );
+ assert_eq!(inlay_snapshot.text(), "abc|123|defghi");
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 0)),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 1)),
+ InlayPoint::new(0, 1)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 2)),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 3)),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 4)),
+ InlayPoint::new(0, 9)
+ );
+ assert_eq!(
+ inlay_snapshot.to_inlay_point(Point::new(0, 5)),
+ InlayPoint::new(0, 10)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+ InlayPoint::new(0, 3)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+ InlayPoint::new(0, 9)
+ );
+
+ // Edits before or after the inlay should not affect it.
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit([(2..3, "x"), (3..3, "y"), (4..4, "z")], None, cx)
+ });
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abxy|123|dzefghi");
+
+ // An edit surrounding the inlay should invalidate it.
+ buffer.update(cx, |buffer, cx| buffer.edit([(4..5, "D")], None, cx));
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abxyDzefghi");
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(3),
+ text: "|123|".into(),
+ },
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_after(3),
+ text: "|456|".into(),
+ },
+ ],
+ );
+ assert_eq!(inlay_snapshot.text(), "abx|123||456|yDzefghi");
+
+ // Edits ending where the inlay starts should not move it if it has a left bias.
+ buffer.update(cx, |buffer, cx| buffer.edit([(3..3, "JKL")], None, cx));
+ let (inlay_snapshot, _) = inlay_map.sync(
+ buffer.read(cx).snapshot(cx),
+ buffer_edits.consume().into_inner(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abx|123|JKL|456|yDzefghi");
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Left),
+ InlayPoint::new(0, 0)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 0), Bias::Right),
+ InlayPoint::new(0, 0)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Left),
+ InlayPoint::new(0, 1)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 1), Bias::Right),
+ InlayPoint::new(0, 1)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 2), Bias::Right),
+ InlayPoint::new(0, 2)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 3), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 4), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 5), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 6), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Left),
+ InlayPoint::new(0, 2)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 7), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Left),
+ InlayPoint::new(0, 8)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 8), Bias::Right),
+ InlayPoint::new(0, 8)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Left),
+ InlayPoint::new(0, 9)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 9), Bias::Right),
+ InlayPoint::new(0, 9)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Left),
+ InlayPoint::new(0, 10)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 10), Bias::Right),
+ InlayPoint::new(0, 10)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 11), Bias::Right),
+ InlayPoint::new(0, 11)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 12), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 13), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 14), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 15), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Left),
+ InlayPoint::new(0, 11)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 16), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Left),
+ InlayPoint::new(0, 17)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 17), Bias::Right),
+ InlayPoint::new(0, 17)
+ );
+
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Left),
+ InlayPoint::new(0, 18)
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(InlayPoint::new(0, 18), Bias::Right),
+ InlayPoint::new(0, 18)
+ );
+
+ // The inlays can be manually removed.
+ let (inlay_snapshot, _) = inlay_map.splice(
+ inlay_map.inlays.iter().map(|inlay| inlay.id).collect(),
+ Vec::new(),
+ );
+ assert_eq!(inlay_snapshot.text(), "abxJKLyDzefghi");
+ }
+
+ #[gpui::test]
+ fn test_inlay_buffer_rows(cx: &mut AppContext) {
+ let buffer = MultiBuffer::build_simple("abc\ndef\nghi", cx);
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer.read(cx).snapshot(cx));
+ assert_eq!(inlay_snapshot.text(), "abc\ndef\nghi");
+ let mut next_inlay_id = 0;
+
+ let (inlay_snapshot, _) = inlay_map.splice(
+ Vec::new(),
+ vec![
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(0),
+ text: "|123|\n".into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(4),
+ text: "|456|".into(),
+ },
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut next_inlay_id)),
+ position: buffer.read(cx).snapshot(cx).anchor_before(7),
+ text: "\n|567|\n".into(),
+ },
+ ],
+ );
+ assert_eq!(inlay_snapshot.text(), "|123|\nabc\n|456|def\n|567|\n\nghi");
+ assert_eq!(
+ inlay_snapshot.buffer_rows(0).collect::<Vec<_>>(),
+ vec![Some(0), None, Some(1), None, None, Some(2)]
+ );
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_inlays(cx: &mut AppContext, mut rng: StdRng) {
+ init_test(cx);
+
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let len = rng.gen_range(0..30);
+ let buffer = if rng.gen() {
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+ let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let mut next_inlay_id = 0;
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+ let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ for _ in 0..operations {
+ let mut inlay_edits = Patch::default();
+
+ let mut prev_inlay_text = inlay_snapshot.text();
+ let mut buffer_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=50 => {
+ let (snapshot, edits) = inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ log::info!("mutated text: {:?}", snapshot.text());
+ inlay_edits = Patch::new(edits);
+ }
+ _ => buffer.update(cx, |buffer, cx| {
+ let subscription = buffer.subscribe();
+ let edit_count = rng.gen_range(1..=5);
+ buffer.randomly_mutate(&mut rng, edit_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ let edits = subscription.consume().into_inner();
+ log::info!("editing {:?}", edits);
+ buffer_edits.extend(edits);
+ }),
+ };
+
+ let (new_inlay_snapshot, new_inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ inlay_snapshot = new_inlay_snapshot;
+ inlay_edits = inlay_edits.compose(new_inlay_edits);
+
+ log::info!("buffer text: {:?}", buffer_snapshot.text());
+ log::info!("inlay text: {:?}", inlay_snapshot.text());
+
+ let inlays = inlay_map
+ .inlays
+ .iter()
+ .filter(|inlay| inlay.position.is_valid(&buffer_snapshot))
+ .map(|inlay| {
+ let offset = inlay.position.to_offset(&buffer_snapshot);
+ (offset, inlay.clone())
+ })
+ .collect::<Vec<_>>();
+ let mut expected_text = Rope::from(buffer_snapshot.text());
+ for (offset, inlay) in inlays.iter().rev() {
+ expected_text.replace(*offset..*offset, &inlay.text.to_string());
+ }
+ assert_eq!(inlay_snapshot.text(), expected_text.to_string());
+
+ let expected_buffer_rows = inlay_snapshot.buffer_rows(0).collect::<Vec<_>>();
+ assert_eq!(
+ expected_buffer_rows.len() as u32,
+ expected_text.max_point().row + 1
+ );
+ for row_start in 0..expected_buffer_rows.len() {
+ assert_eq!(
+ inlay_snapshot
+ .buffer_rows(row_start as u32)
+ .collect::<Vec<_>>(),
+ &expected_buffer_rows[row_start..],
+ "incorrect buffer rows starting at {}",
+ row_start
+ );
+ }
+
+ let mut text_highlights = TextHighlights::default();
+ let text_highlight_count = rng.gen_range(0_usize..10);
+ let mut text_highlight_ranges = (0..text_highlight_count)
+ .map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
+ .collect::<Vec<_>>();
+ text_highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
+ log::info!("highlighting text ranges {text_highlight_ranges:?}");
+ text_highlights.insert(
+ Some(TypeId::of::<()>()),
+ Arc::new((
+ HighlightStyle::default(),
+ text_highlight_ranges
+ .into_iter()
+ .map(|range| {
+ buffer_snapshot.anchor_before(range.start)
+ ..buffer_snapshot.anchor_after(range.end)
+ })
+ .collect(),
+ )),
+ );
+
+ let mut inlay_highlights = InlayHighlights::default();
+ if !inlays.is_empty() {
+ let inlay_highlight_count = rng.gen_range(0..inlays.len());
+ let mut inlay_indices = BTreeSet::default();
+ while inlay_indices.len() < inlay_highlight_count {
+ inlay_indices.insert(rng.gen_range(0..inlays.len()));
+ }
+ let new_highlights = inlay_indices
+ .into_iter()
+ .filter_map(|i| {
+ let (_, inlay) = &inlays[i];
+ let inlay_text_len = inlay.text.len();
+ match inlay_text_len {
+ 0 => None,
+ 1 => Some(InlayHighlight {
+ inlay: inlay.id,
+ inlay_position: inlay.position,
+ range: 0..1,
+ }),
+ n => {
+ let inlay_text = inlay.text.to_string();
+ let mut highlight_end = rng.gen_range(1..n);
+ let mut highlight_start = rng.gen_range(0..highlight_end);
+ while !inlay_text.is_char_boundary(highlight_end) {
+ highlight_end += 1;
+ }
+ while !inlay_text.is_char_boundary(highlight_start) {
+ highlight_start -= 1;
+ }
+ Some(InlayHighlight {
+ inlay: inlay.id,
+ inlay_position: inlay.position,
+ range: highlight_start..highlight_end,
+ })
+ }
+ }
+ })
+ .map(|highlight| (highlight.inlay, (HighlightStyle::default(), highlight)))
+ .collect();
+ log::info!("highlighting inlay ranges {new_highlights:?}");
+ inlay_highlights.insert(TypeId::of::<()>(), new_highlights);
+ }
+
+ for _ in 0..5 {
+ let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
+ end = expected_text.clip_offset(end, Bias::Right);
+ let mut start = rng.gen_range(0..=end);
+ start = expected_text.clip_offset(start, Bias::Right);
+
+ let range = InlayOffset(start)..InlayOffset(end);
+ log::info!("calling inlay_snapshot.chunks({range:?})");
+ let actual_text = inlay_snapshot
+ .chunks(
+ range,
+ false,
+ Highlights {
+ text_highlights: Some(&text_highlights),
+ inlay_highlights: Some(&inlay_highlights),
+ ..Highlights::default()
+ },
+ )
+ .map(|chunk| chunk.text)
+ .collect::<String>();
+ assert_eq!(
+ actual_text,
+ expected_text.slice(start..end).to_string(),
+ "incorrect text in range {:?}",
+ start..end
+ );
+
+ assert_eq!(
+ inlay_snapshot.text_summary_for_range(InlayOffset(start)..InlayOffset(end)),
+ expected_text.slice(start..end).summary()
+ );
+ }
+
+ for edit in inlay_edits {
+ prev_inlay_text.replace_range(
+ edit.new.start.0..edit.new.start.0 + edit.old_len().0,
+ &inlay_snapshot.text()[edit.new.start.0..edit.new.end.0],
+ );
+ }
+ assert_eq!(prev_inlay_text, inlay_snapshot.text());
+
+ assert_eq!(expected_text.max_point(), inlay_snapshot.max_point().0);
+ assert_eq!(expected_text.len(), inlay_snapshot.len().0);
+
+ let mut buffer_point = Point::default();
+ let mut inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+ let mut buffer_chars = buffer_snapshot.chars_at(0);
+ loop {
+ // Ensure conversion from buffer coordinates to inlay coordinates
+ // is consistent.
+ let buffer_offset = buffer_snapshot.point_to_offset(buffer_point);
+ assert_eq!(
+ inlay_snapshot.to_point(inlay_snapshot.to_inlay_offset(buffer_offset)),
+ inlay_point
+ );
+
+ // No matter which bias we clip an inlay point with, it doesn't move
+ // because it was constructed from a buffer point.
+ assert_eq!(
+ inlay_snapshot.clip_point(inlay_point, Bias::Left),
+ inlay_point,
+ "invalid inlay point for buffer point {:?} when clipped left",
+ buffer_point
+ );
+ assert_eq!(
+ inlay_snapshot.clip_point(inlay_point, Bias::Right),
+ inlay_point,
+ "invalid inlay point for buffer point {:?} when clipped right",
+ buffer_point
+ );
+
+ if let Some(ch) = buffer_chars.next() {
+ if ch == '\n' {
+ buffer_point += Point::new(1, 0);
+ } else {
+ buffer_point += Point::new(0, ch.len_utf8() as u32);
+ }
+
+ // Ensure that moving forward in the buffer always moves the inlay point forward as well.
+ let new_inlay_point = inlay_snapshot.to_inlay_point(buffer_point);
+ assert!(new_inlay_point > inlay_point);
+ inlay_point = new_inlay_point;
+ } else {
+ break;
+ }
+ }
+
+ let mut inlay_point = InlayPoint::default();
+ let mut inlay_offset = InlayOffset::default();
+ for ch in expected_text.chars() {
+ assert_eq!(
+ inlay_snapshot.to_offset(inlay_point),
+ inlay_offset,
+ "invalid to_offset({:?})",
+ inlay_point
+ );
+ assert_eq!(
+ inlay_snapshot.to_point(inlay_offset),
+ inlay_point,
+ "invalid to_point({:?})",
+ inlay_offset
+ );
+
+ let mut bytes = [0; 4];
+ for byte in ch.encode_utf8(&mut bytes).as_bytes() {
+ inlay_offset.0 += 1;
+ if *byte == b'\n' {
+ inlay_point.0 += Point::new(1, 0);
+ } else {
+ inlay_point.0 += Point::new(0, 1);
+ }
+
+ let clipped_left_point = inlay_snapshot.clip_point(inlay_point, Bias::Left);
+ let clipped_right_point = inlay_snapshot.clip_point(inlay_point, Bias::Right);
+ assert!(
+ clipped_left_point <= clipped_right_point,
+ "inlay point {:?} when clipped left is greater than when clipped right ({:?} > {:?})",
+ inlay_point,
+ clipped_left_point,
+ clipped_right_point
+ );
+
+ // Ensure the clipped points are at valid text locations.
+ assert_eq!(
+ clipped_left_point.0,
+ expected_text.clip_point(clipped_left_point.0, Bias::Left)
+ );
+ assert_eq!(
+ clipped_right_point.0,
+ expected_text.clip_point(clipped_right_point.0, Bias::Right)
+ );
+
+ // Ensure the clipped points never overshoot the end of the map.
+ assert!(clipped_left_point <= inlay_snapshot.max_point());
+ assert!(clipped_right_point <= inlay_snapshot.max_point());
+
+ // Ensure the clipped points are at valid buffer locations.
+ assert_eq!(
+ inlay_snapshot
+ .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_left_point)),
+ clipped_left_point,
+ "to_buffer_point({:?}) = {:?}",
+ clipped_left_point,
+ inlay_snapshot.to_buffer_point(clipped_left_point),
+ );
+ assert_eq!(
+ inlay_snapshot
+ .to_inlay_point(inlay_snapshot.to_buffer_point(clipped_right_point)),
+ clipped_right_point,
+ "to_buffer_point({:?}) = {:?}",
+ clipped_right_point,
+ inlay_snapshot.to_buffer_point(clipped_right_point),
+ );
+ }
+ }
+ }
+ }
+
+ fn init_test(cx: &mut AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ }
+}
@@ -0,0 +1,765 @@
+use super::{
+ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
+ Highlights,
+};
+use crate::MultiBufferSnapshot;
+use language::{Chunk, Point};
+use std::{cmp, mem, num::NonZeroU32, ops::Range};
+use sum_tree::Bias;
+
+const MAX_EXPANSION_COLUMN: u32 = 256;
+
+pub struct TabMap(TabSnapshot);
+
+impl TabMap {
+ pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
+ let snapshot = TabSnapshot {
+ fold_snapshot,
+ tab_size,
+ max_expansion_column: MAX_EXPANSION_COLUMN,
+ version: 0,
+ };
+ (Self(snapshot.clone()), snapshot)
+ }
+
+ #[cfg(test)]
+ pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
+ self.0.max_expansion_column = column;
+ self.0.clone()
+ }
+
+ pub fn sync(
+ &mut self,
+ fold_snapshot: FoldSnapshot,
+ mut fold_edits: Vec<FoldEdit>,
+ tab_size: NonZeroU32,
+ ) -> (TabSnapshot, Vec<TabEdit>) {
+ let old_snapshot = &mut self.0;
+ let mut new_snapshot = TabSnapshot {
+ fold_snapshot,
+ tab_size,
+ max_expansion_column: old_snapshot.max_expansion_column,
+ version: old_snapshot.version,
+ };
+
+ if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
+ new_snapshot.version += 1;
+ }
+
+ let mut tab_edits = Vec::with_capacity(fold_edits.len());
+
+ if old_snapshot.tab_size == new_snapshot.tab_size {
+ // Expand each edit to include the next tab on the same line as the edit,
+ // and any subsequent tabs on that line that moved across the tab expansion
+ // boundary.
+ for fold_edit in &mut fold_edits {
+ let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+ let old_end_row_successor_offset = cmp::min(
+ FoldPoint::new(old_end.row() + 1, 0),
+ old_snapshot.fold_snapshot.max_point(),
+ )
+ .to_offset(&old_snapshot.fold_snapshot);
+ let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
+
+ let mut offset_from_edit = 0;
+ let mut first_tab_offset = None;
+ let mut last_tab_with_changed_expansion_offset = None;
+ 'outer: for chunk in old_snapshot.fold_snapshot.chunks(
+ fold_edit.old.end..old_end_row_successor_offset,
+ false,
+ Highlights::default(),
+ ) {
+ for (ix, _) in chunk.text.match_indices('\t') {
+ let offset_from_edit = offset_from_edit + (ix as u32);
+ if first_tab_offset.is_none() {
+ first_tab_offset = Some(offset_from_edit);
+ }
+
+ let old_column = old_end.column() + offset_from_edit;
+ let new_column = new_end.column() + offset_from_edit;
+ let was_expanded = old_column < old_snapshot.max_expansion_column;
+ let is_expanded = new_column < new_snapshot.max_expansion_column;
+ if was_expanded != is_expanded {
+ last_tab_with_changed_expansion_offset = Some(offset_from_edit);
+ } else if !was_expanded && !is_expanded {
+ break 'outer;
+ }
+ }
+
+ offset_from_edit += chunk.text.len() as u32;
+ if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
+ && new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
+ {
+ break;
+ }
+ }
+
+ if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
+ fold_edit.old.end.0 += offset as usize + 1;
+ fold_edit.new.end.0 += offset as usize + 1;
+ }
+ }
+
+ // Combine any edits that overlap due to the expansion.
+ let mut ix = 1;
+ while ix < fold_edits.len() {
+ let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
+ let prev_edit = prev_edits.last_mut().unwrap();
+ let edit = &next_edits[0];
+ if prev_edit.old.end >= edit.old.start {
+ prev_edit.old.end = edit.old.end;
+ prev_edit.new.end = edit.new.end;
+ fold_edits.remove(ix);
+ } else {
+ ix += 1;
+ }
+ }
+
+ for fold_edit in fold_edits {
+ let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
+ let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
+ let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
+ let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
+ tab_edits.push(TabEdit {
+ old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
+ new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
+ });
+ }
+ } else {
+ new_snapshot.version += 1;
+ tab_edits.push(TabEdit {
+ old: TabPoint::zero()..old_snapshot.max_point(),
+ new: TabPoint::zero()..new_snapshot.max_point(),
+ });
+ }
+
+ *old_snapshot = new_snapshot;
+ (old_snapshot.clone(), tab_edits)
+ }
+}
+
+#[derive(Clone)]
+pub struct TabSnapshot {
+ pub fold_snapshot: FoldSnapshot,
+ pub tab_size: NonZeroU32,
+ pub max_expansion_column: u32,
+ pub version: usize,
+}
+
+impl TabSnapshot {
+ pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
+ &self.fold_snapshot.inlay_snapshot.buffer
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ let max_point = self.max_point();
+ if row < max_point.row() {
+ self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
+ .0
+ .column
+ } else {
+ max_point.column()
+ }
+ }
+
+ pub fn text_summary(&self) -> TextSummary {
+ self.text_summary_for_range(TabPoint::zero()..self.max_point())
+ }
+
+ pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
+ let input_start = self.to_fold_point(range.start, Bias::Left).0;
+ let input_end = self.to_fold_point(range.end, Bias::Right).0;
+ let input_summary = self
+ .fold_snapshot
+ .text_summary_for_range(input_start..input_end);
+
+ let mut first_line_chars = 0;
+ let line_end = if range.start.row() == range.end.row() {
+ range.end
+ } else {
+ self.max_point()
+ };
+ for c in self
+ .chunks(range.start..line_end, false, Highlights::default())
+ .flat_map(|chunk| chunk.text.chars())
+ {
+ if c == '\n' {
+ break;
+ }
+ first_line_chars += 1;
+ }
+
+ let mut last_line_chars = 0;
+ if range.start.row() == range.end.row() {
+ last_line_chars = first_line_chars;
+ } else {
+ for _ in self
+ .chunks(
+ TabPoint::new(range.end.row(), 0)..range.end,
+ false,
+ Highlights::default(),
+ )
+ .flat_map(|chunk| chunk.text.chars())
+ {
+ last_line_chars += 1;
+ }
+ }
+
+ TextSummary {
+ lines: range.end.0 - range.start.0,
+ first_line_chars,
+ last_line_chars,
+ longest_row: input_summary.longest_row,
+ longest_row_chars: input_summary.longest_row_chars,
+ }
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ range: Range<TabPoint>,
+ language_aware: bool,
+ highlights: Highlights<'a>,
+ ) -> TabChunks<'a> {
+ let (input_start, expanded_char_column, to_next_stop) =
+ self.to_fold_point(range.start, Bias::Left);
+ let input_column = input_start.column();
+ let input_start = input_start.to_offset(&self.fold_snapshot);
+ let input_end = self
+ .to_fold_point(range.end, Bias::Right)
+ .0
+ .to_offset(&self.fold_snapshot);
+ let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
+ range.end.column() - range.start.column()
+ } else {
+ to_next_stop
+ };
+
+ TabChunks {
+ fold_chunks: self.fold_snapshot.chunks(
+ input_start..input_end,
+ language_aware,
+ highlights,
+ ),
+ input_column,
+ column: expanded_char_column,
+ max_expansion_column: self.max_expansion_column,
+ output_position: range.start.0,
+ max_output_position: range.end.0,
+ tab_size: self.tab_size,
+ chunk: Chunk {
+ text: &SPACES[0..(to_next_stop as usize)],
+ is_tab: true,
+ ..Default::default()
+ },
+ inside_leading_tab: to_next_stop > 0,
+ }
+ }
+
+ pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
+ self.fold_snapshot.buffer_rows(row)
+ }
+
+ #[cfg(test)]
+ pub fn text(&self) -> String {
+ self.chunks(
+ TabPoint::zero()..self.max_point(),
+ false,
+ Highlights::default(),
+ )
+ .map(|chunk| chunk.text)
+ .collect()
+ }
+
+ pub fn max_point(&self) -> TabPoint {
+ self.to_tab_point(self.fold_snapshot.max_point())
+ }
+
+ pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
+ self.to_tab_point(
+ self.fold_snapshot
+ .clip_point(self.to_fold_point(point, bias).0, bias),
+ )
+ }
+
+ pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
+ let expanded = self.expand_tabs(chars, input.column());
+ TabPoint::new(input.row(), expanded)
+ }
+
+ pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
+ let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
+ let expanded = output.column();
+ let (collapsed, expanded_char_column, to_next_stop) =
+ self.collapse_tabs(chars, expanded, bias);
+ (
+ FoldPoint::new(output.row(), collapsed as u32),
+ expanded_char_column,
+ to_next_stop,
+ )
+ }
+
+ pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
+ let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
+ let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
+ self.to_tab_point(fold_point)
+ }
+
+ pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
+ let fold_point = self.to_fold_point(point, bias).0;
+ let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
+ self.fold_snapshot
+ .inlay_snapshot
+ .to_buffer_point(inlay_point)
+ }
+
+ fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
+ let tab_size = self.tab_size.get();
+
+ let mut expanded_chars = 0;
+ let mut expanded_bytes = 0;
+ let mut collapsed_bytes = 0;
+ let end_column = column.min(self.max_expansion_column);
+ for c in chars {
+ if collapsed_bytes >= end_column {
+ break;
+ }
+ if c == '\t' {
+ let tab_len = tab_size - expanded_chars % tab_size;
+ expanded_bytes += tab_len;
+ expanded_chars += tab_len;
+ } else {
+ expanded_bytes += c.len_utf8() as u32;
+ expanded_chars += 1;
+ }
+ collapsed_bytes += c.len_utf8() as u32;
+ }
+ expanded_bytes + column.saturating_sub(collapsed_bytes)
+ }
+
+ fn collapse_tabs(
+ &self,
+ chars: impl Iterator<Item = char>,
+ column: u32,
+ bias: Bias,
+ ) -> (u32, u32, u32) {
+ let tab_size = self.tab_size.get();
+
+ let mut expanded_bytes = 0;
+ let mut expanded_chars = 0;
+ let mut collapsed_bytes = 0;
+ for c in chars {
+ if expanded_bytes >= column {
+ break;
+ }
+ if collapsed_bytes >= self.max_expansion_column {
+ break;
+ }
+
+ if c == '\t' {
+ let tab_len = tab_size - (expanded_chars % tab_size);
+ expanded_chars += tab_len;
+ expanded_bytes += tab_len;
+ if expanded_bytes > column {
+ expanded_chars -= expanded_bytes - column;
+ return match bias {
+ Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
+ Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
+ };
+ }
+ } else {
+ expanded_chars += 1;
+ expanded_bytes += c.len_utf8() as u32;
+ }
+
+ if expanded_bytes > column && matches!(bias, Bias::Left) {
+ expanded_chars -= 1;
+ break;
+ }
+
+ collapsed_bytes += c.len_utf8() as u32;
+ }
+ (
+ collapsed_bytes + column.saturating_sub(expanded_bytes),
+ expanded_chars,
+ 0,
+ )
+ }
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct TabPoint(pub Point);
+
+impl TabPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(Point::new(row, column))
+ }
+
+ pub fn zero() -> Self {
+ Self::new(0, 0)
+ }
+
+ pub fn row(self) -> u32 {
+ self.0.row
+ }
+
+ pub fn column(self) -> u32 {
+ self.0.column
+ }
+}
+
+impl From<Point> for TabPoint {
+ fn from(point: Point) -> Self {
+ Self(point)
+ }
+}
+
+pub type TabEdit = text::Edit<TabPoint>;
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct TextSummary {
+ pub lines: Point,
+ pub first_line_chars: u32,
+ pub last_line_chars: u32,
+ pub longest_row: u32,
+ pub longest_row_chars: u32,
+}
+
+impl<'a> From<&'a str> for TextSummary {
+ fn from(text: &'a str) -> Self {
+ let sum = text::TextSummary::from(text);
+
+ TextSummary {
+ lines: sum.lines,
+ first_line_chars: sum.first_line_chars,
+ last_line_chars: sum.last_line_chars,
+ longest_row: sum.longest_row,
+ longest_row_chars: sum.longest_row_chars,
+ }
+ }
+}
+
+impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
+ fn add_assign(&mut self, other: &'a Self) {
+ let joined_chars = self.last_line_chars + other.first_line_chars;
+ if joined_chars > self.longest_row_chars {
+ self.longest_row = self.lines.row;
+ self.longest_row_chars = joined_chars;
+ }
+ if other.longest_row_chars > self.longest_row_chars {
+ self.longest_row = self.lines.row + other.longest_row;
+ self.longest_row_chars = other.longest_row_chars;
+ }
+
+ if self.lines.row == 0 {
+ self.first_line_chars += other.first_line_chars;
+ }
+
+ if other.lines.row == 0 {
+ self.last_line_chars += other.first_line_chars;
+ } else {
+ self.last_line_chars = other.last_line_chars;
+ }
+
+ self.lines += &other.lines;
+ }
+}
+
+// Handles a tab width <= 16
+const SPACES: &str = " ";
+
+pub struct TabChunks<'a> {
+ fold_chunks: FoldChunks<'a>,
+ chunk: Chunk<'a>,
+ column: u32,
+ max_expansion_column: u32,
+ output_position: Point,
+ input_column: u32,
+ max_output_position: Point,
+ tab_size: NonZeroU32,
+ inside_leading_tab: bool,
+}
+
+impl<'a> Iterator for TabChunks<'a> {
+ type Item = Chunk<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.chunk.text.is_empty() {
+ if let Some(chunk) = self.fold_chunks.next() {
+ self.chunk = chunk;
+ if self.inside_leading_tab {
+ self.chunk.text = &self.chunk.text[1..];
+ self.inside_leading_tab = false;
+ self.input_column += 1;
+ }
+ } else {
+ return None;
+ }
+ }
+
+ for (ix, c) in self.chunk.text.char_indices() {
+ match c {
+ '\t' => {
+ if ix > 0 {
+ let (prefix, suffix) = self.chunk.text.split_at(ix);
+ self.chunk.text = suffix;
+ return Some(Chunk {
+ text: prefix,
+ ..self.chunk
+ });
+ } else {
+ self.chunk.text = &self.chunk.text[1..];
+ let tab_size = if self.input_column < self.max_expansion_column {
+ self.tab_size.get() as u32
+ } else {
+ 1
+ };
+ let mut len = tab_size - self.column % tab_size;
+ let next_output_position = cmp::min(
+ self.output_position + Point::new(0, len),
+ self.max_output_position,
+ );
+ len = next_output_position.column - self.output_position.column;
+ self.column += len;
+ self.input_column += 1;
+ self.output_position = next_output_position;
+ return Some(Chunk {
+ text: &SPACES[..len as usize],
+ is_tab: true,
+ ..self.chunk
+ });
+ }
+ }
+ '\n' => {
+ self.column = 0;
+ self.input_column = 0;
+ self.output_position += Point::new(1, 0);
+ }
+ _ => {
+ self.column += 1;
+ if !self.inside_leading_tab {
+ self.input_column += c.len_utf8() as u32;
+ }
+ self.output_position.column += c.len_utf8() as u32;
+ }
+ }
+ }
+
+ Some(mem::take(&mut self.chunk))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::{fold_map::FoldMap, inlay_map::InlayMap},
+ MultiBuffer,
+ };
+ use rand::{prelude::StdRng, Rng};
+
+ #[gpui::test]
+ fn test_expand_tabs(cx: &mut gpui::AppContext) {
+ let buffer = MultiBuffer::build_simple("", cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+ assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
+ assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
+ assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
+ }
+
+ #[gpui::test]
+ fn test_long_lines(cx: &mut gpui::AppContext) {
+ let max_expansion_column = 12;
+ let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
+ let output = "A BC DEF G HI J K L M";
+
+ let buffer = MultiBuffer::build_simple(input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+ tab_snapshot.max_expansion_column = max_expansion_column;
+ assert_eq!(tab_snapshot.text(), output);
+
+ for (ix, c) in input.char_indices() {
+ assert_eq!(
+ tab_snapshot
+ .chunks(
+ TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
+ false,
+ Highlights::default(),
+ )
+ .map(|c| c.text)
+ .collect::<String>(),
+ &output[ix..],
+ "text from index {ix}"
+ );
+
+ if c != '\t' {
+ let input_point = Point::new(0, ix as u32);
+ let output_point = Point::new(0, output.find(c).unwrap() as u32);
+ assert_eq!(
+ tab_snapshot.to_tab_point(FoldPoint(input_point)),
+ TabPoint(output_point),
+ "to_tab_point({input_point:?})"
+ );
+ assert_eq!(
+ tab_snapshot
+ .to_fold_point(TabPoint(output_point), Bias::Left)
+ .0,
+ FoldPoint(input_point),
+ "to_fold_point({output_point:?})"
+ );
+ }
+ }
+ }
+
+ #[gpui::test]
+ fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::AppContext) {
+ let max_expansion_column = 8;
+ let input = "abcdefgโฏhij";
+
+ let buffer = MultiBuffer::build_simple(input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+ tab_snapshot.max_expansion_column = max_expansion_column;
+ assert_eq!(tab_snapshot.text(), input);
+ }
+
+ #[gpui::test]
+ fn test_marking_tabs(cx: &mut gpui::AppContext) {
+ let input = "\t \thello";
+
+ let buffer = MultiBuffer::build_simple(&input, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
+ let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
+
+ assert_eq!(
+ chunks(&tab_snapshot, TabPoint::zero()),
+ vec![
+ (" ".to_string(), true),
+ (" ".to_string(), false),
+ (" ".to_string(), true),
+ ("hello".to_string(), false),
+ ]
+ );
+ assert_eq!(
+ chunks(&tab_snapshot, TabPoint::new(0, 2)),
+ vec![
+ (" ".to_string(), true),
+ (" ".to_string(), false),
+ (" ".to_string(), true),
+ ("hello".to_string(), false),
+ ]
+ );
+
+ fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
+ let mut chunks = Vec::new();
+ let mut was_tab = false;
+ let mut text = String::new();
+ for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
+ {
+ if chunk.is_tab != was_tab {
+ if !text.is_empty() {
+ chunks.push((mem::take(&mut text), was_tab));
+ }
+ was_tab = chunk.is_tab;
+ }
+ text.push_str(chunk.text);
+ }
+
+ if !text.is_empty() {
+ chunks.push((text, was_tab));
+ }
+ chunks
+ }
+ }
+
+ #[gpui::test(iterations = 100)]
+ fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
+ let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+ let len = rng.gen_range(0..30);
+ let buffer = if rng.gen() {
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ } else {
+ MultiBuffer::build_random(&mut rng, cx)
+ };
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ log::info!("Buffer text: {:?}", buffer_snapshot.text());
+
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
+ fold_map.randomly_mutate(&mut rng);
+ let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
+ log::info!("FoldMap text: {:?}", fold_snapshot.text());
+ let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+
+ let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+ let tabs_snapshot = tab_map.set_max_expansion_column(32);
+
+ let text = text::Rope::from(tabs_snapshot.text().as_str());
+ log::info!(
+ "TabMap text (tab size: {}): {:?}",
+ tab_size,
+ tabs_snapshot.text(),
+ );
+
+ for _ in 0..5 {
+ let end_row = rng.gen_range(0..=text.max_point().row);
+ let end_column = rng.gen_range(0..=text.line_len(end_row));
+ let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
+ let start_row = rng.gen_range(0..=text.max_point().row);
+ let start_column = rng.gen_range(0..=text.line_len(start_row));
+ let mut start =
+ TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
+ if start > end {
+ mem::swap(&mut start, &mut end);
+ }
+
+ let expected_text = text
+ .chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
+ .collect::<String>();
+ let expected_summary = TextSummary::from(expected_text.as_str());
+ assert_eq!(
+ tabs_snapshot
+ .chunks(start..end, false, Highlights::default())
+ .map(|c| c.text)
+ .collect::<String>(),
+ expected_text,
+ "chunks({:?}..{:?})",
+ start,
+ end
+ );
+
+ let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
+ if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
+ actual_summary.longest_row = expected_summary.longest_row;
+ actual_summary.longest_row_chars = expected_summary.longest_row_chars;
+ }
+ assert_eq!(actual_summary, expected_summary);
+ }
+
+ for row in 0..=text.max_point().row {
+ assert_eq!(
+ tabs_snapshot.line_len(row),
+ text.line_len(row),
+ "line_len({row})"
+ );
+ }
+ }
+}
@@ -0,0 +1,1355 @@
+use super::{
+ fold_map::FoldBufferRows,
+ tab_map::{self, TabEdit, TabPoint, TabSnapshot},
+ Highlights,
+};
+use crate::MultiBufferSnapshot;
+use gpui::{
+ fonts::FontId, text_layout::LineWrapper, AppContext, Entity, ModelContext, ModelHandle, Task,
+};
+use language::{Chunk, Point};
+use lazy_static::lazy_static;
+use smol::future::yield_now;
+use std::{cmp, collections::VecDeque, mem, ops::Range, time::Duration};
+use sum_tree::{Bias, Cursor, SumTree};
+use text::Patch;
+
+pub use super::tab_map::TextSummary;
+pub type WrapEdit = text::Edit<u32>;
+
+pub struct WrapMap {
+ snapshot: WrapSnapshot,
+ pending_edits: VecDeque<(TabSnapshot, Vec<TabEdit>)>,
+ interpolated_edits: Patch<u32>,
+ edits_since_sync: Patch<u32>,
+ wrap_width: Option<f32>,
+ background_task: Option<Task<()>>,
+ font: (FontId, f32),
+}
+
+impl Entity for WrapMap {
+ type Event = ();
+}
+
+#[derive(Clone)]
+pub struct WrapSnapshot {
+ tab_snapshot: TabSnapshot,
+ transforms: SumTree<Transform>,
+ interpolated: bool,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct Transform {
+ summary: TransformSummary,
+ display_text: Option<&'static str>,
+}
+
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+struct TransformSummary {
+ input: TextSummary,
+ output: TextSummary,
+}
+
+#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
+pub struct WrapPoint(pub Point);
+
+pub struct WrapChunks<'a> {
+ input_chunks: tab_map::TabChunks<'a>,
+ input_chunk: Chunk<'a>,
+ output_position: WrapPoint,
+ max_output_row: u32,
+ transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
+}
+
+#[derive(Clone)]
+pub struct WrapBufferRows<'a> {
+ input_buffer_rows: FoldBufferRows<'a>,
+ input_buffer_row: Option<u32>,
+ output_row: u32,
+ soft_wrapped: bool,
+ max_output_row: u32,
+ transforms: Cursor<'a, Transform, (WrapPoint, TabPoint)>,
+}
+
+impl WrapMap {
+ pub fn new(
+ tab_snapshot: TabSnapshot,
+ font_id: FontId,
+ font_size: f32,
+ wrap_width: Option<f32>,
+ cx: &mut AppContext,
+ ) -> (ModelHandle<Self>, WrapSnapshot) {
+ let handle = cx.add_model(|cx| {
+ let mut this = Self {
+ font: (font_id, font_size),
+ wrap_width: None,
+ pending_edits: Default::default(),
+ interpolated_edits: Default::default(),
+ edits_since_sync: Default::default(),
+ snapshot: WrapSnapshot::new(tab_snapshot),
+ background_task: None,
+ };
+ this.set_wrap_width(wrap_width, cx);
+ mem::take(&mut this.edits_since_sync);
+ this
+ });
+ let snapshot = handle.read(cx).snapshot.clone();
+ (handle, snapshot)
+ }
+
+ #[cfg(test)]
+ pub fn is_rewrapping(&self) -> bool {
+ self.background_task.is_some()
+ }
+
+ pub fn sync(
+ &mut self,
+ tab_snapshot: TabSnapshot,
+ edits: Vec<TabEdit>,
+ cx: &mut ModelContext<Self>,
+ ) -> (WrapSnapshot, Patch<u32>) {
+ if self.wrap_width.is_some() {
+ self.pending_edits.push_back((tab_snapshot, edits));
+ self.flush_edits(cx);
+ } else {
+ self.edits_since_sync = self
+ .edits_since_sync
+ .compose(&self.snapshot.interpolate(tab_snapshot, &edits));
+ self.snapshot.interpolated = false;
+ }
+
+ (self.snapshot.clone(), mem::take(&mut self.edits_since_sync))
+ }
+
+ pub fn set_font(
+ &mut self,
+ font_id: FontId,
+ font_size: f32,
+ cx: &mut ModelContext<Self>,
+ ) -> bool {
+ if (font_id, font_size) != self.font {
+ self.font = (font_id, font_size);
+ self.rewrap(cx);
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn set_wrap_width(&mut self, wrap_width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
+ if wrap_width == self.wrap_width {
+ return false;
+ }
+
+ self.wrap_width = wrap_width;
+ self.rewrap(cx);
+ true
+ }
+
+ fn rewrap(&mut self, cx: &mut ModelContext<Self>) {
+ self.background_task.take();
+ self.interpolated_edits.clear();
+ self.pending_edits.clear();
+
+ if let Some(wrap_width) = self.wrap_width {
+ let mut new_snapshot = self.snapshot.clone();
+ let font_cache = cx.font_cache().clone();
+ let (font_id, font_size) = self.font;
+ let task = cx.background().spawn(async move {
+ let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
+ let tab_snapshot = new_snapshot.tab_snapshot.clone();
+ let range = TabPoint::zero()..tab_snapshot.max_point();
+ let edits = new_snapshot
+ .update(
+ tab_snapshot,
+ &[TabEdit {
+ old: range.clone(),
+ new: range.clone(),
+ }],
+ wrap_width,
+ &mut line_wrapper,
+ )
+ .await;
+ (new_snapshot, edits)
+ });
+
+ match cx
+ .background()
+ .block_with_timeout(Duration::from_millis(5), task)
+ {
+ Ok((snapshot, edits)) => {
+ self.snapshot = snapshot;
+ self.edits_since_sync = self.edits_since_sync.compose(&edits);
+ cx.notify();
+ }
+ Err(wrap_task) => {
+ self.background_task = Some(cx.spawn(|this, mut cx| async move {
+ let (snapshot, edits) = wrap_task.await;
+ this.update(&mut cx, |this, cx| {
+ this.snapshot = snapshot;
+ this.edits_since_sync = this
+ .edits_since_sync
+ .compose(mem::take(&mut this.interpolated_edits).invert())
+ .compose(&edits);
+ this.background_task = None;
+ this.flush_edits(cx);
+ cx.notify();
+ });
+ }));
+ }
+ }
+ } else {
+ let old_rows = self.snapshot.transforms.summary().output.lines.row + 1;
+ self.snapshot.transforms = SumTree::new();
+ let summary = self.snapshot.tab_snapshot.text_summary();
+ if !summary.lines.is_zero() {
+ self.snapshot
+ .transforms
+ .push(Transform::isomorphic(summary), &());
+ }
+ let new_rows = self.snapshot.transforms.summary().output.lines.row + 1;
+ self.snapshot.interpolated = false;
+ self.edits_since_sync = self.edits_since_sync.compose(&Patch::new(vec![WrapEdit {
+ old: 0..old_rows,
+ new: 0..new_rows,
+ }]));
+ }
+ }
+
+ fn flush_edits(&mut self, cx: &mut ModelContext<Self>) {
+ if !self.snapshot.interpolated {
+ let mut to_remove_len = 0;
+ for (tab_snapshot, _) in &self.pending_edits {
+ if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
+ to_remove_len += 1;
+ } else {
+ break;
+ }
+ }
+ self.pending_edits.drain(..to_remove_len);
+ }
+
+ if self.pending_edits.is_empty() {
+ return;
+ }
+
+ if let Some(wrap_width) = self.wrap_width {
+ if self.background_task.is_none() {
+ let pending_edits = self.pending_edits.clone();
+ let mut snapshot = self.snapshot.clone();
+ let font_cache = cx.font_cache().clone();
+ let (font_id, font_size) = self.font;
+ let update_task = cx.background().spawn(async move {
+ let mut line_wrapper = font_cache.line_wrapper(font_id, font_size);
+
+ let mut edits = Patch::default();
+ for (tab_snapshot, tab_edits) in pending_edits {
+ let wrap_edits = snapshot
+ .update(tab_snapshot, &tab_edits, wrap_width, &mut line_wrapper)
+ .await;
+ edits = edits.compose(&wrap_edits);
+ }
+ (snapshot, edits)
+ });
+
+ match cx
+ .background()
+ .block_with_timeout(Duration::from_millis(1), update_task)
+ {
+ Ok((snapshot, output_edits)) => {
+ self.snapshot = snapshot;
+ self.edits_since_sync = self.edits_since_sync.compose(&output_edits);
+ }
+ Err(update_task) => {
+ self.background_task = Some(cx.spawn(|this, mut cx| async move {
+ let (snapshot, edits) = update_task.await;
+ this.update(&mut cx, |this, cx| {
+ this.snapshot = snapshot;
+ this.edits_since_sync = this
+ .edits_since_sync
+ .compose(mem::take(&mut this.interpolated_edits).invert())
+ .compose(&edits);
+ this.background_task = None;
+ this.flush_edits(cx);
+ cx.notify();
+ });
+ }));
+ }
+ }
+ }
+ }
+
+ let was_interpolated = self.snapshot.interpolated;
+ let mut to_remove_len = 0;
+ for (tab_snapshot, edits) in &self.pending_edits {
+ if tab_snapshot.version <= self.snapshot.tab_snapshot.version {
+ to_remove_len += 1;
+ } else {
+ let interpolated_edits = self.snapshot.interpolate(tab_snapshot.clone(), edits);
+ self.edits_since_sync = self.edits_since_sync.compose(&interpolated_edits);
+ self.interpolated_edits = self.interpolated_edits.compose(&interpolated_edits);
+ }
+ }
+
+ if !was_interpolated {
+ self.pending_edits.drain(..to_remove_len);
+ }
+ }
+}
+
+impl WrapSnapshot {
+ fn new(tab_snapshot: TabSnapshot) -> Self {
+ let mut transforms = SumTree::new();
+ let extent = tab_snapshot.text_summary();
+ if !extent.lines.is_zero() {
+ transforms.push(Transform::isomorphic(extent), &());
+ }
+ Self {
+ transforms,
+ tab_snapshot,
+ interpolated: true,
+ }
+ }
+
+ pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
+ self.tab_snapshot.buffer_snapshot()
+ }
+
+ fn interpolate(&mut self, new_tab_snapshot: TabSnapshot, tab_edits: &[TabEdit]) -> Patch<u32> {
+ let mut new_transforms;
+ if tab_edits.is_empty() {
+ new_transforms = self.transforms.clone();
+ } else {
+ let mut old_cursor = self.transforms.cursor::<TabPoint>();
+
+ let mut tab_edits_iter = tab_edits.iter().peekable();
+ new_transforms =
+ old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &());
+
+ while let Some(edit) = tab_edits_iter.next() {
+ if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) {
+ let summary = new_tab_snapshot.text_summary_for_range(
+ TabPoint::from(new_transforms.summary().input.lines)..edit.new.start,
+ );
+ new_transforms.push_or_extend(Transform::isomorphic(summary));
+ }
+
+ if !edit.new.is_empty() {
+ new_transforms.push_or_extend(Transform::isomorphic(
+ new_tab_snapshot.text_summary_for_range(edit.new.clone()),
+ ));
+ }
+
+ old_cursor.seek_forward(&edit.old.end, Bias::Right, &());
+ if let Some(next_edit) = tab_edits_iter.peek() {
+ if next_edit.old.start > old_cursor.end(&()) {
+ if old_cursor.end(&()) > edit.old.end {
+ let summary = self
+ .tab_snapshot
+ .text_summary_for_range(edit.old.end..old_cursor.end(&()));
+ new_transforms.push_or_extend(Transform::isomorphic(summary));
+ }
+
+ old_cursor.next(&());
+ new_transforms.append(
+ old_cursor.slice(&next_edit.old.start, Bias::Right, &()),
+ &(),
+ );
+ }
+ } else {
+ if old_cursor.end(&()) > edit.old.end {
+ let summary = self
+ .tab_snapshot
+ .text_summary_for_range(edit.old.end..old_cursor.end(&()));
+ new_transforms.push_or_extend(Transform::isomorphic(summary));
+ }
+ old_cursor.next(&());
+ new_transforms.append(old_cursor.suffix(&()), &());
+ }
+ }
+ }
+
+ let old_snapshot = mem::replace(
+ self,
+ WrapSnapshot {
+ tab_snapshot: new_tab_snapshot,
+ transforms: new_transforms,
+ interpolated: true,
+ },
+ );
+ self.check_invariants();
+ old_snapshot.compute_edits(tab_edits, self)
+ }
+
+ async fn update(
+ &mut self,
+ new_tab_snapshot: TabSnapshot,
+ tab_edits: &[TabEdit],
+ wrap_width: f32,
+ line_wrapper: &mut LineWrapper,
+ ) -> Patch<u32> {
+ #[derive(Debug)]
+ struct RowEdit {
+ old_rows: Range<u32>,
+ new_rows: Range<u32>,
+ }
+
+ let mut tab_edits_iter = tab_edits.iter().peekable();
+ let mut row_edits = Vec::new();
+ while let Some(edit) = tab_edits_iter.next() {
+ let mut row_edit = RowEdit {
+ old_rows: edit.old.start.row()..edit.old.end.row() + 1,
+ new_rows: edit.new.start.row()..edit.new.end.row() + 1,
+ };
+
+ while let Some(next_edit) = tab_edits_iter.peek() {
+ if next_edit.old.start.row() <= row_edit.old_rows.end {
+ row_edit.old_rows.end = next_edit.old.end.row() + 1;
+ row_edit.new_rows.end = next_edit.new.end.row() + 1;
+ tab_edits_iter.next();
+ } else {
+ break;
+ }
+ }
+
+ row_edits.push(row_edit);
+ }
+
+ let mut new_transforms;
+ if row_edits.is_empty() {
+ new_transforms = self.transforms.clone();
+ } else {
+ let mut row_edits = row_edits.into_iter().peekable();
+ let mut old_cursor = self.transforms.cursor::<TabPoint>();
+
+ new_transforms = old_cursor.slice(
+ &TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0),
+ Bias::Right,
+ &(),
+ );
+
+ while let Some(edit) = row_edits.next() {
+ if edit.new_rows.start > new_transforms.summary().input.lines.row {
+ let summary = new_tab_snapshot.text_summary_for_range(
+ TabPoint(new_transforms.summary().input.lines)
+ ..TabPoint::new(edit.new_rows.start, 0),
+ );
+ new_transforms.push_or_extend(Transform::isomorphic(summary));
+ }
+
+ let mut line = String::new();
+ let mut remaining = None;
+ let mut chunks = new_tab_snapshot.chunks(
+ TabPoint::new(edit.new_rows.start, 0)..new_tab_snapshot.max_point(),
+ false,
+ Highlights::default(),
+ );
+ let mut edit_transforms = Vec::<Transform>::new();
+ for _ in edit.new_rows.start..edit.new_rows.end {
+ while let Some(chunk) =
+ remaining.take().or_else(|| chunks.next().map(|c| c.text))
+ {
+ if let Some(ix) = chunk.find('\n') {
+ line.push_str(&chunk[..ix + 1]);
+ remaining = Some(&chunk[ix + 1..]);
+ break;
+ } else {
+ line.push_str(chunk)
+ }
+ }
+
+ if line.is_empty() {
+ break;
+ }
+
+ let mut prev_boundary_ix = 0;
+ for boundary in line_wrapper.wrap_line(&line, wrap_width) {
+ let wrapped = &line[prev_boundary_ix..boundary.ix];
+ push_isomorphic(&mut edit_transforms, TextSummary::from(wrapped));
+ edit_transforms.push(Transform::wrap(boundary.next_indent));
+ prev_boundary_ix = boundary.ix;
+ }
+
+ if prev_boundary_ix < line.len() {
+ push_isomorphic(
+ &mut edit_transforms,
+ TextSummary::from(&line[prev_boundary_ix..]),
+ );
+ }
+
+ line.clear();
+ yield_now().await;
+ }
+
+ let mut edit_transforms = edit_transforms.into_iter();
+ if let Some(transform) = edit_transforms.next() {
+ new_transforms.push_or_extend(transform);
+ }
+ new_transforms.extend(edit_transforms, &());
+
+ old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &());
+ if let Some(next_edit) = row_edits.peek() {
+ if next_edit.old_rows.start > old_cursor.end(&()).row() {
+ if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
+ let summary = self.tab_snapshot.text_summary_for_range(
+ TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
+ );
+ new_transforms.push_or_extend(Transform::isomorphic(summary));
+ }
+ old_cursor.next(&());
+ new_transforms.append(
+ old_cursor.slice(
+ &TabPoint::new(next_edit.old_rows.start, 0),
+ Bias::Right,
+ &(),
+ ),
+ &(),
+ );
+ }
+ } else {
+ if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) {
+ let summary = self.tab_snapshot.text_summary_for_range(
+ TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()),
+ );
+ new_transforms.push_or_extend(Transform::isomorphic(summary));
+ }
+ old_cursor.next(&());
+ new_transforms.append(old_cursor.suffix(&()), &());
+ }
+ }
+ }
+
+ let old_snapshot = mem::replace(
+ self,
+ WrapSnapshot {
+ tab_snapshot: new_tab_snapshot,
+ transforms: new_transforms,
+ interpolated: false,
+ },
+ );
+ self.check_invariants();
+ old_snapshot.compute_edits(tab_edits, self)
+ }
+
+ fn compute_edits(&self, tab_edits: &[TabEdit], new_snapshot: &WrapSnapshot) -> Patch<u32> {
+ let mut wrap_edits = Vec::new();
+ let mut old_cursor = self.transforms.cursor::<TransformSummary>();
+ let mut new_cursor = new_snapshot.transforms.cursor::<TransformSummary>();
+ for mut tab_edit in tab_edits.iter().cloned() {
+ tab_edit.old.start.0.column = 0;
+ tab_edit.old.end.0 += Point::new(1, 0);
+ tab_edit.new.start.0.column = 0;
+ tab_edit.new.end.0 += Point::new(1, 0);
+
+ old_cursor.seek(&tab_edit.old.start, Bias::Right, &());
+ let mut old_start = old_cursor.start().output.lines;
+ old_start += tab_edit.old.start.0 - old_cursor.start().input.lines;
+
+ old_cursor.seek(&tab_edit.old.end, Bias::Right, &());
+ let mut old_end = old_cursor.start().output.lines;
+ old_end += tab_edit.old.end.0 - old_cursor.start().input.lines;
+
+ new_cursor.seek(&tab_edit.new.start, Bias::Right, &());
+ let mut new_start = new_cursor.start().output.lines;
+ new_start += tab_edit.new.start.0 - new_cursor.start().input.lines;
+
+ new_cursor.seek(&tab_edit.new.end, Bias::Right, &());
+ let mut new_end = new_cursor.start().output.lines;
+ new_end += tab_edit.new.end.0 - new_cursor.start().input.lines;
+
+ wrap_edits.push(WrapEdit {
+ old: old_start.row..old_end.row,
+ new: new_start.row..new_end.row,
+ });
+ }
+
+ consolidate_wrap_edits(&mut wrap_edits);
+ Patch::new(wrap_edits)
+ }
+
+ pub fn chunks<'a>(
+ &'a self,
+ rows: Range<u32>,
+ language_aware: bool,
+ highlights: Highlights<'a>,
+ ) -> WrapChunks<'a> {
+ let output_start = WrapPoint::new(rows.start, 0);
+ let output_end = WrapPoint::new(rows.end, 0);
+ let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+ transforms.seek(&output_start, Bias::Right, &());
+ let mut input_start = TabPoint(transforms.start().1 .0);
+ if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ input_start.0 += output_start.0 - transforms.start().0 .0;
+ }
+ let input_end = self
+ .to_tab_point(output_end)
+ .min(self.tab_snapshot.max_point());
+ WrapChunks {
+ input_chunks: self.tab_snapshot.chunks(
+ input_start..input_end,
+ language_aware,
+ highlights,
+ ),
+ input_chunk: Default::default(),
+ output_position: output_start,
+ max_output_row: rows.end,
+ transforms,
+ }
+ }
+
+ pub fn max_point(&self) -> WrapPoint {
+ WrapPoint(self.transforms.summary().output.lines)
+ }
+
+ pub fn line_len(&self, row: u32) -> u32 {
+ let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+ cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &());
+ if cursor
+ .item()
+ .map_or(false, |transform| transform.is_isomorphic())
+ {
+ let overshoot = row - cursor.start().0.row();
+ let tab_row = cursor.start().1.row() + overshoot;
+ let tab_line_len = self.tab_snapshot.line_len(tab_row);
+ if overshoot == 0 {
+ cursor.start().0.column() + (tab_line_len - cursor.start().1.column())
+ } else {
+ tab_line_len
+ }
+ } else {
+ cursor.start().0.column()
+ }
+ }
+
+ pub fn soft_wrap_indent(&self, row: u32) -> Option<u32> {
+ let mut cursor = self.transforms.cursor::<WrapPoint>();
+ cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &());
+ cursor.item().and_then(|transform| {
+ if transform.is_isomorphic() {
+ None
+ } else {
+ Some(transform.summary.output.lines.column)
+ }
+ })
+ }
+
+ pub fn longest_row(&self) -> u32 {
+ self.transforms.summary().output.longest_row
+ }
+
+ pub fn buffer_rows(&self, start_row: u32) -> WrapBufferRows {
+ let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+ transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &());
+ let mut input_row = transforms.start().1.row();
+ if transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ input_row += start_row - transforms.start().0.row();
+ }
+ let soft_wrapped = transforms.item().map_or(false, |t| !t.is_isomorphic());
+ let mut input_buffer_rows = self.tab_snapshot.buffer_rows(input_row);
+ let input_buffer_row = input_buffer_rows.next().unwrap();
+ WrapBufferRows {
+ transforms,
+ input_buffer_row,
+ input_buffer_rows,
+ output_row: start_row,
+ soft_wrapped,
+ max_output_row: self.max_point().row(),
+ }
+ }
+
+ pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint {
+ let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+ cursor.seek(&point, Bias::Right, &());
+ let mut tab_point = cursor.start().1 .0;
+ if cursor.item().map_or(false, |t| t.is_isomorphic()) {
+ tab_point += point.0 - cursor.start().0 .0;
+ }
+ TabPoint(tab_point)
+ }
+
+ pub fn to_point(&self, point: WrapPoint, bias: Bias) -> Point {
+ self.tab_snapshot.to_point(self.to_tab_point(point), bias)
+ }
+
+ pub fn make_wrap_point(&self, point: Point, bias: Bias) -> WrapPoint {
+ self.tab_point_to_wrap_point(self.tab_snapshot.make_tab_point(point, bias))
+ }
+
+ pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint {
+ let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>();
+ cursor.seek(&point, Bias::Right, &());
+ WrapPoint(cursor.start().1 .0 + (point.0 - cursor.start().0 .0))
+ }
+
+ pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint {
+ if bias == Bias::Left {
+ let mut cursor = self.transforms.cursor::<WrapPoint>();
+ cursor.seek(&point, Bias::Right, &());
+ if cursor.item().map_or(false, |t| !t.is_isomorphic()) {
+ point = *cursor.start();
+ *point.column_mut() -= 1;
+ }
+ }
+
+ self.tab_point_to_wrap_point(self.tab_snapshot.clip_point(self.to_tab_point(point), bias))
+ }
+
+ pub fn prev_row_boundary(&self, mut point: WrapPoint) -> u32 {
+ if self.transforms.is_empty() {
+ return 0;
+ }
+
+ *point.column_mut() = 0;
+
+ let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+ cursor.seek(&point, Bias::Right, &());
+ if cursor.item().is_none() {
+ cursor.prev(&());
+ }
+
+ while let Some(transform) = cursor.item() {
+ if transform.is_isomorphic() && cursor.start().1.column() == 0 {
+ return cmp::min(cursor.end(&()).0.row(), point.row());
+ } else {
+ cursor.prev(&());
+ }
+ }
+
+ unreachable!()
+ }
+
+ pub fn next_row_boundary(&self, mut point: WrapPoint) -> Option<u32> {
+ point.0 += Point::new(1, 0);
+
+ let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>();
+ cursor.seek(&point, Bias::Right, &());
+ while let Some(transform) = cursor.item() {
+ if transform.is_isomorphic() && cursor.start().1.column() == 0 {
+ return Some(cmp::max(cursor.start().0.row(), point.row()));
+ } else {
+ cursor.next(&());
+ }
+ }
+
+ None
+ }
+
+ fn check_invariants(&self) {
+ #[cfg(test)]
+ {
+ assert_eq!(
+ TabPoint::from(self.transforms.summary().input.lines),
+ self.tab_snapshot.max_point()
+ );
+
+ {
+ let mut transforms = self.transforms.cursor::<()>().peekable();
+ while let Some(transform) = transforms.next() {
+ if let Some(next_transform) = transforms.peek() {
+ assert!(transform.is_isomorphic() != next_transform.is_isomorphic());
+ }
+ }
+ }
+
+ let text = language::Rope::from(self.text().as_str());
+ let mut input_buffer_rows = self.tab_snapshot.buffer_rows(0);
+ let mut expected_buffer_rows = Vec::new();
+ let mut prev_tab_row = 0;
+ for display_row in 0..=self.max_point().row() {
+ let tab_point = self.to_tab_point(WrapPoint::new(display_row, 0));
+ if tab_point.row() == prev_tab_row && display_row != 0 {
+ expected_buffer_rows.push(None);
+ } else {
+ expected_buffer_rows.push(input_buffer_rows.next().unwrap());
+ }
+
+ prev_tab_row = tab_point.row();
+ assert_eq!(self.line_len(display_row), text.line_len(display_row));
+ }
+
+ for start_display_row in 0..expected_buffer_rows.len() {
+ assert_eq!(
+ self.buffer_rows(start_display_row as u32)
+ .collect::<Vec<_>>(),
+ &expected_buffer_rows[start_display_row..],
+ "invalid buffer_rows({}..)",
+ start_display_row
+ );
+ }
+ }
+ }
+}
+
+impl<'a> Iterator for WrapChunks<'a> {
+ type Item = Chunk<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.output_position.row() >= self.max_output_row {
+ return None;
+ }
+
+ let transform = self.transforms.item()?;
+ if let Some(display_text) = transform.display_text {
+ let mut start_ix = 0;
+ let mut end_ix = display_text.len();
+ let mut summary = transform.summary.output.lines;
+
+ if self.output_position > self.transforms.start().0 {
+ // Exclude newline starting prior to the desired row.
+ start_ix = 1;
+ summary.row = 0;
+ } else if self.output_position.row() + 1 >= self.max_output_row {
+ // Exclude soft indentation ending after the desired row.
+ end_ix = 1;
+ summary.column = 0;
+ }
+
+ self.output_position.0 += summary;
+ self.transforms.next(&());
+ return Some(Chunk {
+ text: &display_text[start_ix..end_ix],
+ ..self.input_chunk
+ });
+ }
+
+ if self.input_chunk.text.is_empty() {
+ self.input_chunk = self.input_chunks.next().unwrap();
+ }
+
+ let mut input_len = 0;
+ let transform_end = self.transforms.end(&()).0;
+ for c in self.input_chunk.text.chars() {
+ let char_len = c.len_utf8();
+ input_len += char_len;
+ if c == '\n' {
+ *self.output_position.row_mut() += 1;
+ *self.output_position.column_mut() = 0;
+ } else {
+ *self.output_position.column_mut() += char_len as u32;
+ }
+
+ if self.output_position >= transform_end {
+ self.transforms.next(&());
+ break;
+ }
+ }
+
+ let (prefix, suffix) = self.input_chunk.text.split_at(input_len);
+ self.input_chunk.text = suffix;
+ Some(Chunk {
+ text: prefix,
+ ..self.input_chunk
+ })
+ }
+}
+
+impl<'a> Iterator for WrapBufferRows<'a> {
+ type Item = Option<u32>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ if self.output_row > self.max_output_row {
+ return None;
+ }
+
+ let buffer_row = self.input_buffer_row;
+ let soft_wrapped = self.soft_wrapped;
+
+ self.output_row += 1;
+ self.transforms
+ .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left, &());
+ if self.transforms.item().map_or(false, |t| t.is_isomorphic()) {
+ self.input_buffer_row = self.input_buffer_rows.next().unwrap();
+ self.soft_wrapped = false;
+ } else {
+ self.soft_wrapped = true;
+ }
+
+ Some(if soft_wrapped { None } else { buffer_row })
+ }
+}
+
+impl Transform {
+ fn isomorphic(summary: TextSummary) -> Self {
+ #[cfg(test)]
+ assert!(!summary.lines.is_zero());
+
+ Self {
+ summary: TransformSummary {
+ input: summary.clone(),
+ output: summary,
+ },
+ display_text: None,
+ }
+ }
+
+ fn wrap(indent: u32) -> Self {
+ lazy_static! {
+ static ref WRAP_TEXT: String = {
+ let mut wrap_text = String::new();
+ wrap_text.push('\n');
+ wrap_text.extend((0..LineWrapper::MAX_INDENT as usize).map(|_| ' '));
+ wrap_text
+ };
+ }
+
+ Self {
+ summary: TransformSummary {
+ input: TextSummary::default(),
+ output: TextSummary {
+ lines: Point::new(1, indent),
+ first_line_chars: 0,
+ last_line_chars: indent,
+ longest_row: 1,
+ longest_row_chars: indent,
+ },
+ },
+ display_text: Some(&WRAP_TEXT[..1 + indent as usize]),
+ }
+ }
+
+ fn is_isomorphic(&self) -> bool {
+ self.display_text.is_none()
+ }
+}
+
+impl sum_tree::Item for Transform {
+ type Summary = TransformSummary;
+
+ fn summary(&self) -> Self::Summary {
+ self.summary.clone()
+ }
+}
+
+fn push_isomorphic(transforms: &mut Vec<Transform>, summary: TextSummary) {
+ if let Some(last_transform) = transforms.last_mut() {
+ if last_transform.is_isomorphic() {
+ last_transform.summary.input += &summary;
+ last_transform.summary.output += &summary;
+ return;
+ }
+ }
+ transforms.push(Transform::isomorphic(summary));
+}
+
+trait SumTreeExt {
+ fn push_or_extend(&mut self, transform: Transform);
+}
+
+impl SumTreeExt for SumTree<Transform> {
+ fn push_or_extend(&mut self, transform: Transform) {
+ let mut transform = Some(transform);
+ self.update_last(
+ |last_transform| {
+ if last_transform.is_isomorphic() && transform.as_ref().unwrap().is_isomorphic() {
+ let transform = transform.take().unwrap();
+ last_transform.summary.input += &transform.summary.input;
+ last_transform.summary.output += &transform.summary.output;
+ }
+ },
+ &(),
+ );
+
+ if let Some(transform) = transform {
+ self.push(transform, &());
+ }
+ }
+}
+
+impl WrapPoint {
+ pub fn new(row: u32, column: u32) -> Self {
+ Self(Point::new(row, column))
+ }
+
+ pub fn row(self) -> u32 {
+ self.0.row
+ }
+
+ pub fn row_mut(&mut self) -> &mut u32 {
+ &mut self.0.row
+ }
+
+ pub fn column(self) -> u32 {
+ self.0.column
+ }
+
+ pub fn column_mut(&mut self) -> &mut u32 {
+ &mut self.0.column
+ }
+}
+
+impl sum_tree::Summary for TransformSummary {
+ type Context = ();
+
+ fn add_summary(&mut self, other: &Self, _: &()) {
+ self.input += &other.input;
+ self.output += &other.output;
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for TabPoint {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += summary.input.lines;
+ }
+}
+
+impl<'a> sum_tree::SeekTarget<'a, TransformSummary, TransformSummary> for TabPoint {
+ fn cmp(&self, cursor_location: &TransformSummary, _: &()) -> std::cmp::Ordering {
+ Ord::cmp(&self.0, &cursor_location.input.lines)
+ }
+}
+
+impl<'a> sum_tree::Dimension<'a, TransformSummary> for WrapPoint {
+ fn add_summary(&mut self, summary: &'a TransformSummary, _: &()) {
+ self.0 += summary.output.lines;
+ }
+}
+
+fn consolidate_wrap_edits(edits: &mut Vec<WrapEdit>) {
+ let mut i = 1;
+ while i < edits.len() {
+ let edit = edits[i].clone();
+ let prev_edit = &mut edits[i - 1];
+ if prev_edit.old.end >= edit.old.start {
+ prev_edit.old.end = edit.old.end;
+ prev_edit.new.end = edit.new.end;
+ edits.remove(i);
+ continue;
+ }
+ i += 1;
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap},
+ MultiBuffer,
+ };
+ use gpui::test::observe;
+ use rand::prelude::*;
+ use settings::SettingsStore;
+ use smol::stream::StreamExt;
+ use std::{cmp, env, num::NonZeroU32};
+ use text::Rope;
+
+ #[gpui::test(iterations = 100)]
+ async fn test_random_wraps(cx: &mut gpui::TestAppContext, mut rng: StdRng) {
+ init_test(cx);
+
+ cx.foreground().set_block_on_ticks(0..=50);
+ let operations = env::var("OPERATIONS")
+ .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
+ .unwrap_or(10);
+
+ let font_cache = cx.font_cache().clone();
+ let font_system = cx.platform().fonts();
+ let mut wrap_width = if rng.gen_bool(0.1) {
+ None
+ } else {
+ Some(rng.gen_range(0.0..=1000.0))
+ };
+ let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
+ let family_id = font_cache
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = font_cache
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
+ log::info!("Tab size: {}", tab_size);
+ log::info!("Wrap width: {:?}", wrap_width);
+
+ let buffer = cx.update(|cx| {
+ if rng.gen() {
+ MultiBuffer::build_random(&mut rng, cx)
+ } else {
+ let len = rng.gen_range(0..10);
+ let text = util::RandomCharIter::new(&mut rng)
+ .take(len)
+ .collect::<String>();
+ MultiBuffer::build_simple(&text, cx)
+ }
+ });
+ let mut buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx));
+ log::info!("Buffer text: {:?}", buffer_snapshot.text());
+ let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (mut fold_map, fold_snapshot) = FoldMap::new(inlay_snapshot.clone());
+ log::info!("FoldMap text: {:?}", fold_snapshot.text());
+ let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
+ let tabs_snapshot = tab_map.set_max_expansion_column(32);
+ log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+ let mut line_wrapper = LineWrapper::new(font_id, font_size, font_system);
+ let unwrapped_text = tabs_snapshot.text();
+ let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+
+ let (wrap_map, _) =
+ cx.update(|cx| WrapMap::new(tabs_snapshot.clone(), font_id, font_size, wrap_width, cx));
+ let mut notifications = observe(&wrap_map, cx);
+
+ if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ notifications.next().await.unwrap();
+ }
+
+ let (initial_snapshot, _) = wrap_map.update(cx, |map, cx| {
+ assert!(!map.is_rewrapping());
+ map.sync(tabs_snapshot.clone(), Vec::new(), cx)
+ });
+
+ let actual_text = initial_snapshot.text();
+ assert_eq!(
+ actual_text, expected_text,
+ "unwrapped text is: {:?}",
+ unwrapped_text
+ );
+ log::info!("Wrapped text: {:?}", actual_text);
+
+ let mut next_inlay_id = 0;
+ let mut edits = Vec::new();
+ for _i in 0..operations {
+ log::info!("{} ==============================================", _i);
+
+ let mut buffer_edits = Vec::new();
+ match rng.gen_range(0..=100) {
+ 0..=19 => {
+ wrap_width = if rng.gen_bool(0.2) {
+ None
+ } else {
+ Some(rng.gen_range(0.0..=1000.0))
+ };
+ log::info!("Setting wrap width to {:?}", wrap_width);
+ wrap_map.update(cx, |map, cx| map.set_wrap_width(wrap_width, cx));
+ }
+ 20..=39 => {
+ for (fold_snapshot, fold_edits) in fold_map.randomly_mutate(&mut rng) {
+ let (tabs_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (mut snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+ snapshot.check_invariants();
+ snapshot.verify_chunks(&mut rng);
+ edits.push((snapshot, wrap_edits));
+ }
+ }
+ 40..=59 => {
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.randomly_mutate(&mut next_inlay_id, &mut rng);
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ let (tabs_snapshot, tab_edits) =
+ tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ let (mut snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, tab_edits, cx));
+ snapshot.check_invariants();
+ snapshot.verify_chunks(&mut rng);
+ edits.push((snapshot, wrap_edits));
+ }
+ _ => {
+ buffer.update(cx, |buffer, cx| {
+ let subscription = buffer.subscribe();
+ let edit_count = rng.gen_range(1..=5);
+ buffer.randomly_mutate(&mut rng, edit_count, cx);
+ buffer_snapshot = buffer.snapshot(cx);
+ buffer_edits.extend(subscription.consume());
+ });
+ }
+ }
+
+ log::info!("Buffer text: {:?}", buffer_snapshot.text());
+ let (inlay_snapshot, inlay_edits) =
+ inlay_map.sync(buffer_snapshot.clone(), buffer_edits);
+ log::info!("InlayMap text: {:?}", inlay_snapshot.text());
+ let (fold_snapshot, fold_edits) = fold_map.read(inlay_snapshot, inlay_edits);
+ log::info!("FoldMap text: {:?}", fold_snapshot.text());
+ let (tabs_snapshot, tab_edits) = tab_map.sync(fold_snapshot, fold_edits, tab_size);
+ log::info!("TabMap text: {:?}", tabs_snapshot.text());
+
+ let unwrapped_text = tabs_snapshot.text();
+ let expected_text = wrap_text(&unwrapped_text, wrap_width, &mut line_wrapper);
+ let (mut snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), tab_edits, cx));
+ snapshot.check_invariants();
+ snapshot.verify_chunks(&mut rng);
+ edits.push((snapshot, wrap_edits));
+
+ if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) && rng.gen_bool(0.4) {
+ log::info!("Waiting for wrapping to finish");
+ while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ notifications.next().await.unwrap();
+ }
+ wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+ }
+
+ if !wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ let (mut wrapped_snapshot, wrap_edits) =
+ wrap_map.update(cx, |map, cx| map.sync(tabs_snapshot, Vec::new(), cx));
+ let actual_text = wrapped_snapshot.text();
+ let actual_longest_row = wrapped_snapshot.longest_row();
+ log::info!("Wrapping finished: {:?}", actual_text);
+ wrapped_snapshot.check_invariants();
+ wrapped_snapshot.verify_chunks(&mut rng);
+ edits.push((wrapped_snapshot.clone(), wrap_edits));
+ assert_eq!(
+ actual_text, expected_text,
+ "unwrapped text is: {:?}",
+ unwrapped_text
+ );
+
+ let mut summary = TextSummary::default();
+ for (ix, item) in wrapped_snapshot
+ .transforms
+ .items(&())
+ .into_iter()
+ .enumerate()
+ {
+ summary += &item.summary.output;
+ log::info!("{} summary: {:?}", ix, item.summary.output,);
+ }
+
+ if tab_size.get() == 1
+ || !wrapped_snapshot
+ .tab_snapshot
+ .fold_snapshot
+ .text()
+ .contains('\t')
+ {
+ let mut expected_longest_rows = Vec::new();
+ let mut longest_line_len = -1;
+ for (row, line) in expected_text.split('\n').enumerate() {
+ let line_char_count = line.chars().count() as isize;
+ if line_char_count > longest_line_len {
+ expected_longest_rows.clear();
+ longest_line_len = line_char_count;
+ }
+ if line_char_count >= longest_line_len {
+ expected_longest_rows.push(row as u32);
+ }
+ }
+
+ assert!(
+ expected_longest_rows.contains(&actual_longest_row),
+ "incorrect longest row {}. expected {:?} with length {}",
+ actual_longest_row,
+ expected_longest_rows,
+ longest_line_len,
+ )
+ }
+ }
+ }
+
+ let mut initial_text = Rope::from(initial_snapshot.text().as_str());
+ for (snapshot, patch) in edits {
+ let snapshot_text = Rope::from(snapshot.text().as_str());
+ for edit in &patch {
+ let old_start = initial_text.point_to_offset(Point::new(edit.new.start, 0));
+ let old_end = initial_text.point_to_offset(cmp::min(
+ Point::new(edit.new.start + edit.old.len() as u32, 0),
+ initial_text.max_point(),
+ ));
+ let new_start = snapshot_text.point_to_offset(Point::new(edit.new.start, 0));
+ let new_end = snapshot_text.point_to_offset(cmp::min(
+ Point::new(edit.new.end, 0),
+ snapshot_text.max_point(),
+ ));
+ let new_text = snapshot_text
+ .chunks_in_range(new_start..new_end)
+ .collect::<String>();
+
+ initial_text.replace(old_start..old_end, &new_text);
+ }
+ assert_eq!(initial_text.to_string(), snapshot_text.to_string());
+ }
+
+ if wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ log::info!("Waiting for wrapping to finish");
+ while wrap_map.read_with(cx, |map, _| map.is_rewrapping()) {
+ notifications.next().await.unwrap();
+ }
+ }
+ wrap_map.read_with(cx, |map, _| assert!(map.pending_edits.is_empty()));
+ }
+
+ fn init_test(cx: &mut gpui::TestAppContext) {
+ cx.foreground().forbid_parking();
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ });
+ }
+
+ fn wrap_text(
+ unwrapped_text: &str,
+ wrap_width: Option<f32>,
+ line_wrapper: &mut LineWrapper,
+ ) -> String {
+ if let Some(wrap_width) = wrap_width {
+ let mut wrapped_text = String::new();
+ for (row, line) in unwrapped_text.split('\n').enumerate() {
+ if row > 0 {
+ wrapped_text.push('\n')
+ }
+
+ let mut prev_ix = 0;
+ for boundary in line_wrapper.wrap_line(line, wrap_width) {
+ wrapped_text.push_str(&line[prev_ix..boundary.ix]);
+ wrapped_text.push('\n');
+ wrapped_text.push_str(&" ".repeat(boundary.next_indent as usize));
+ prev_ix = boundary.ix;
+ }
+ wrapped_text.push_str(&line[prev_ix..]);
+ }
+ wrapped_text
+ } else {
+ unwrapped_text.to_string()
+ }
+ }
+
+ impl WrapSnapshot {
+ pub fn text(&self) -> String {
+ self.text_chunks(0).collect()
+ }
+
+ pub fn text_chunks(&self, wrap_row: u32) -> impl Iterator<Item = &str> {
+ self.chunks(
+ wrap_row..self.max_point().row() + 1,
+ false,
+ Highlights::default(),
+ )
+ .map(|h| h.text)
+ }
+
+ fn verify_chunks(&mut self, rng: &mut impl Rng) {
+ for _ in 0..5 {
+ let mut end_row = rng.gen_range(0..=self.max_point().row());
+ let start_row = rng.gen_range(0..=end_row);
+ end_row += 1;
+
+ let mut expected_text = self.text_chunks(start_row).collect::<String>();
+ if expected_text.ends_with('\n') {
+ expected_text.push('\n');
+ }
+ let mut expected_text = expected_text
+ .lines()
+ .take((end_row - start_row) as usize)
+ .collect::<Vec<_>>()
+ .join("\n");
+ if end_row <= self.max_point().row() {
+ expected_text.push('\n');
+ }
+
+ let actual_text = self
+ .chunks(start_row..end_row, true, Highlights::default())
+ .map(|c| c.text)
+ .collect::<String>();
+ assert_eq!(
+ expected_text,
+ actual_text,
+ "chunks != highlighted_chunks for rows {:?}",
+ start_row..end_row
+ );
+ }
+ }
+ }
+}
@@ -0,0 +1,10095 @@
+mod blink_manager;
+pub mod display_map;
+mod editor_settings;
+mod element;
+mod inlay_hint_cache;
+
+mod git;
+mod highlight_matching_bracket;
+mod hover_popover;
+pub mod items;
+mod link_go_to_definition;
+mod mouse_context_menu;
+pub mod movement;
+mod persistence;
+pub mod scroll;
+pub mod selections_collection;
+
+#[cfg(test)]
+mod editor_tests;
+#[cfg(any(test, feature = "test-support"))]
+pub mod test;
+
+use ::git::diff::DiffHunk;
+use aho_corasick::AhoCorasick;
+use anyhow::{anyhow, Context, Result};
+use blink_manager::BlinkManager;
+use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings};
+use clock::{Global, ReplicaId};
+use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
+use convert_case::{Case, Casing};
+use copilot::Copilot;
+pub use display_map::DisplayPoint;
+use display_map::*;
+pub use editor_settings::EditorSettings;
+pub use element::{
+ Cursor, EditorElement, HighlightedRange, HighlightedRangeLine, LineWithInvisibles,
+};
+use futures::FutureExt;
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+ actions,
+ color::Color,
+ elements::*,
+ executor,
+ fonts::{self, HighlightStyle, TextStyle},
+ geometry::vector::{vec2f, Vector2F},
+ impl_actions,
+ keymap_matcher::KeymapContext,
+ platform::{CursorStyle, MouseButton},
+ serde_json, AnyElement, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem,
+ CursorRegion, Element, Entity, ModelHandle, MouseRegion, Subscription, Task, View, ViewContext,
+ ViewHandle, WeakViewHandle, WindowContext,
+};
+use highlight_matching_bracket::refresh_matching_bracket_highlights;
+use hover_popover::{hide_hover, HoverState};
+use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy};
+pub use items::MAX_TAB_TITLE_LEN;
+use itertools::Itertools;
+pub use language::{char_kind, CharKind};
+use language::{
+ language_settings::{self, all_language_settings, InlayHintSettings},
+ markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel,
+ Completion, CursorShape, Diagnostic, DiagnosticSeverity, Documentation, File, IndentKind,
+ IndentSize, Language, LanguageRegistry, LanguageServerName, OffsetRangeExt, OffsetUtf16, Point,
+ Selection, SelectionGoal, TransactionId,
+};
+use link_go_to_definition::{
+ hide_link_definition, show_link_definition, GoToDefinitionLink, InlayHighlight,
+ LinkGoToDefinitionState,
+};
+use log::error;
+use lsp::LanguageServerId;
+use movement::TextLayoutDetails;
+use multi_buffer::ToOffsetUtf16;
+pub use multi_buffer::{
+ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset,
+ ToPoint,
+};
+use ordered_float::OrderedFloat;
+use parking_lot::RwLock;
+use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction};
+use rand::{seq::SliceRandom, thread_rng};
+use rpc::proto::{self, PeerId};
+use scroll::{
+ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
+};
+use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
+use serde::{Deserialize, Serialize};
+use settings::SettingsStore;
+use smallvec::SmallVec;
+use snippet::Snippet;
+use std::{
+ any::TypeId,
+ borrow::Cow,
+ cmp::{self, Ordering, Reverse},
+ mem,
+ num::NonZeroU32,
+ ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
+ path::Path,
+ sync::Arc,
+ time::{Duration, Instant},
+};
+pub use sum_tree::Bias;
+use sum_tree::TreeMap;
+use text::Rope;
+use theme::{DiagnosticStyle, Theme, ThemeSettings};
+use util::{post_inc, RangeExt, ResultExt, TryFutureExt};
+use workspace::{ItemNavHistory, SplitDirection, ViewId, Workspace};
+
+use crate::git::diff_hunk_to_display;
+
+const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
+const MAX_LINE_LEN: usize = 1024;
+const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
+const MAX_SELECTION_HISTORY_LEN: usize = 1024;
+const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
+pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250);
+pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
+
+pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
+
+pub fn render_parsed_markdown<Tag: 'static>(
+ parsed: &language::ParsedMarkdown,
+ editor_style: &EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+) -> Text {
+ enum RenderedMarkdown {}
+
+ let parsed = parsed.clone();
+ let view_id = cx.view_id();
+ let code_span_background_color = editor_style.document_highlight_read_background;
+
+ let mut region_id = 0;
+
+ Text::new(parsed.text, editor_style.text.clone())
+ .with_highlights(
+ parsed
+ .highlights
+ .iter()
+ .filter_map(|(range, highlight)| {
+ let highlight = highlight.to_highlight_style(&editor_style.syntax)?;
+ Some((range.clone(), highlight))
+ })
+ .collect::<Vec<_>>(),
+ )
+ .with_custom_runs(parsed.region_ranges, move |ix, bounds, cx| {
+ region_id += 1;
+ let region = parsed.regions[ix].clone();
+
+ if let Some(link) = region.link {
+ cx.scene().push_cursor_region(CursorRegion {
+ bounds,
+ style: CursorStyle::PointingHand,
+ });
+ cx.scene().push_mouse_region(
+ MouseRegion::new::<(RenderedMarkdown, Tag)>(view_id, region_id, bounds)
+ .on_down::<Editor, _>(MouseButton::Left, move |_, _, cx| match &link {
+ markdown::Link::Web { url } => cx.platform().open_url(url),
+ markdown::Link::Path { path } => {
+ if let Some(workspace) = &workspace {
+ _ = workspace.update(cx, |workspace, cx| {
+ workspace.open_abs_path(path.clone(), false, cx).detach();
+ });
+ }
+ }
+ }),
+ );
+ }
+
+ if region.code {
+ cx.scene().push_quad(gpui::Quad {
+ bounds,
+ background: Some(code_span_background_color),
+ border: Default::default(),
+ corner_radii: (2.0).into(),
+ });
+ }
+ })
+ .with_soft_wrap(true)
+}
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct SelectNext {
+ #[serde(default)]
+ pub replace_newest: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct SelectPrevious {
+ #[serde(default)]
+ pub replace_newest: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct SelectAllMatches {
+ #[serde(default)]
+ pub replace_newest: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct SelectToBeginningOfLine {
+ #[serde(default)]
+ stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct MovePageUp {
+ #[serde(default)]
+ center_cursor: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct MovePageDown {
+ #[serde(default)]
+ center_cursor: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct SelectToEndOfLine {
+ #[serde(default)]
+ stop_at_soft_wraps: bool,
+}
+
+#[derive(Clone, Deserialize, PartialEq)]
+pub struct ToggleCodeActions {
+ #[serde(default)]
+ pub deployed_from_indicator: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct ConfirmCompletion {
+ #[serde(default)]
+ pub item_ix: Option<usize>,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct ConfirmCodeAction {
+ #[serde(default)]
+ pub item_ix: Option<usize>,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct ToggleComments {
+ #[serde(default)]
+ pub advance_downwards: bool,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct FoldAt {
+ pub buffer_row: u32,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct UnfoldAt {
+ pub buffer_row: u32,
+}
+
+#[derive(Clone, Default, Deserialize, PartialEq)]
+pub struct GutterHover {
+ pub hovered: bool,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub enum InlayId {
+ Suggestion(usize),
+ Hint(usize),
+}
+
+impl InlayId {
+ fn id(&self) -> usize {
+ match self {
+ Self::Suggestion(id) => *id,
+ Self::Hint(id) => *id,
+ }
+ }
+}
+
+actions!(
+ editor,
+ [
+ Cancel,
+ Backspace,
+ Delete,
+ Newline,
+ NewlineAbove,
+ NewlineBelow,
+ GoToDiagnostic,
+ GoToPrevDiagnostic,
+ GoToHunk,
+ GoToPrevHunk,
+ Indent,
+ Outdent,
+ DeleteLine,
+ DeleteToPreviousWordStart,
+ DeleteToPreviousSubwordStart,
+ DeleteToNextWordEnd,
+ DeleteToNextSubwordEnd,
+ DeleteToBeginningOfLine,
+ DeleteToEndOfLine,
+ CutToEndOfLine,
+ DuplicateLine,
+ MoveLineUp,
+ MoveLineDown,
+ JoinLines,
+ SortLinesCaseSensitive,
+ SortLinesCaseInsensitive,
+ ReverseLines,
+ ShuffleLines,
+ ConvertToUpperCase,
+ ConvertToLowerCase,
+ ConvertToTitleCase,
+ ConvertToSnakeCase,
+ ConvertToKebabCase,
+ ConvertToUpperCamelCase,
+ ConvertToLowerCamelCase,
+ Transpose,
+ Cut,
+ Copy,
+ Paste,
+ Undo,
+ Redo,
+ MoveUp,
+ PageUp,
+ MoveDown,
+ PageDown,
+ MoveLeft,
+ MoveRight,
+ MoveToPreviousWordStart,
+ MoveToPreviousSubwordStart,
+ MoveToNextWordEnd,
+ MoveToNextSubwordEnd,
+ MoveToBeginningOfLine,
+ MoveToEndOfLine,
+ MoveToStartOfParagraph,
+ MoveToEndOfParagraph,
+ MoveToBeginning,
+ MoveToEnd,
+ SelectUp,
+ SelectDown,
+ SelectLeft,
+ SelectRight,
+ SelectToPreviousWordStart,
+ SelectToPreviousSubwordStart,
+ SelectToNextWordEnd,
+ SelectToNextSubwordEnd,
+ SelectToStartOfParagraph,
+ SelectToEndOfParagraph,
+ SelectToBeginning,
+ SelectToEnd,
+ SelectAll,
+ SelectLine,
+ SplitSelectionIntoLines,
+ AddSelectionAbove,
+ AddSelectionBelow,
+ Tab,
+ TabPrev,
+ ShowCharacterPalette,
+ SelectLargerSyntaxNode,
+ SelectSmallerSyntaxNode,
+ GoToDefinition,
+ GoToDefinitionSplit,
+ GoToTypeDefinition,
+ GoToTypeDefinitionSplit,
+ MoveToEnclosingBracket,
+ UndoSelection,
+ RedoSelection,
+ FindAllReferences,
+ Rename,
+ ConfirmRename,
+ Fold,
+ UnfoldLines,
+ FoldSelectedRanges,
+ ShowCompletions,
+ OpenExcerpts,
+ RestartLanguageServer,
+ Hover,
+ Format,
+ ToggleSoftWrap,
+ ToggleInlayHints,
+ RevealInFinder,
+ CopyPath,
+ CopyRelativePath,
+ CopyHighlightJson,
+ ContextMenuFirst,
+ ContextMenuPrev,
+ ContextMenuNext,
+ ContextMenuLast,
+ ]
+);
+
+impl_actions!(
+ editor,
+ [
+ SelectNext,
+ SelectPrevious,
+ SelectAllMatches,
+ SelectToBeginningOfLine,
+ SelectToEndOfLine,
+ ToggleCodeActions,
+ MovePageUp,
+ MovePageDown,
+ ConfirmCompletion,
+ ConfirmCodeAction,
+ ToggleComments,
+ FoldAt,
+ UnfoldAt,
+ GutterHover
+ ]
+);
+
+enum DocumentHighlightRead {}
+enum DocumentHighlightWrite {}
+enum InputComposition {}
+
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum Direction {
+ Prev,
+ Next,
+}
+
+pub fn init_settings(cx: &mut AppContext) {
+ settings::register::<EditorSettings>(cx);
+}
+
+pub fn init(cx: &mut AppContext) {
+ init_settings(cx);
+ cx.add_action(Editor::new_file);
+ cx.add_action(Editor::new_file_in_direction);
+ cx.add_action(Editor::cancel);
+ cx.add_action(Editor::newline);
+ cx.add_action(Editor::newline_above);
+ cx.add_action(Editor::newline_below);
+ cx.add_action(Editor::backspace);
+ cx.add_action(Editor::delete);
+ cx.add_action(Editor::tab);
+ cx.add_action(Editor::tab_prev);
+ cx.add_action(Editor::indent);
+ cx.add_action(Editor::outdent);
+ cx.add_action(Editor::delete_line);
+ cx.add_action(Editor::join_lines);
+ cx.add_action(Editor::sort_lines_case_sensitive);
+ cx.add_action(Editor::sort_lines_case_insensitive);
+ cx.add_action(Editor::reverse_lines);
+ cx.add_action(Editor::shuffle_lines);
+ cx.add_action(Editor::convert_to_upper_case);
+ cx.add_action(Editor::convert_to_lower_case);
+ cx.add_action(Editor::convert_to_title_case);
+ cx.add_action(Editor::convert_to_snake_case);
+ cx.add_action(Editor::convert_to_kebab_case);
+ cx.add_action(Editor::convert_to_upper_camel_case);
+ cx.add_action(Editor::convert_to_lower_camel_case);
+ cx.add_action(Editor::delete_to_previous_word_start);
+ cx.add_action(Editor::delete_to_previous_subword_start);
+ cx.add_action(Editor::delete_to_next_word_end);
+ cx.add_action(Editor::delete_to_next_subword_end);
+ cx.add_action(Editor::delete_to_beginning_of_line);
+ cx.add_action(Editor::delete_to_end_of_line);
+ cx.add_action(Editor::cut_to_end_of_line);
+ cx.add_action(Editor::duplicate_line);
+ cx.add_action(Editor::move_line_up);
+ cx.add_action(Editor::move_line_down);
+ cx.add_action(Editor::transpose);
+ cx.add_action(Editor::cut);
+ cx.add_action(Editor::copy);
+ cx.add_action(Editor::paste);
+ cx.add_action(Editor::undo);
+ cx.add_action(Editor::redo);
+ cx.add_action(Editor::move_up);
+ cx.add_action(Editor::move_page_up);
+ cx.add_action(Editor::move_down);
+ cx.add_action(Editor::move_page_down);
+ cx.add_action(Editor::next_screen);
+ cx.add_action(Editor::move_left);
+ cx.add_action(Editor::move_right);
+ cx.add_action(Editor::move_to_previous_word_start);
+ cx.add_action(Editor::move_to_previous_subword_start);
+ cx.add_action(Editor::move_to_next_word_end);
+ cx.add_action(Editor::move_to_next_subword_end);
+ cx.add_action(Editor::move_to_beginning_of_line);
+ cx.add_action(Editor::move_to_end_of_line);
+ cx.add_action(Editor::move_to_start_of_paragraph);
+ cx.add_action(Editor::move_to_end_of_paragraph);
+ cx.add_action(Editor::move_to_beginning);
+ cx.add_action(Editor::move_to_end);
+ cx.add_action(Editor::select_up);
+ cx.add_action(Editor::select_down);
+ cx.add_action(Editor::select_left);
+ cx.add_action(Editor::select_right);
+ cx.add_action(Editor::select_to_previous_word_start);
+ cx.add_action(Editor::select_to_previous_subword_start);
+ cx.add_action(Editor::select_to_next_word_end);
+ cx.add_action(Editor::select_to_next_subword_end);
+ cx.add_action(Editor::select_to_beginning_of_line);
+ cx.add_action(Editor::select_to_end_of_line);
+ cx.add_action(Editor::select_to_start_of_paragraph);
+ cx.add_action(Editor::select_to_end_of_paragraph);
+ cx.add_action(Editor::select_to_beginning);
+ cx.add_action(Editor::select_to_end);
+ cx.add_action(Editor::select_all);
+ cx.add_action(Editor::select_all_matches);
+ cx.add_action(Editor::select_line);
+ cx.add_action(Editor::split_selection_into_lines);
+ cx.add_action(Editor::add_selection_above);
+ cx.add_action(Editor::add_selection_below);
+ cx.add_action(Editor::select_next);
+ cx.add_action(Editor::select_previous);
+ cx.add_action(Editor::toggle_comments);
+ cx.add_action(Editor::select_larger_syntax_node);
+ cx.add_action(Editor::select_smaller_syntax_node);
+ cx.add_action(Editor::move_to_enclosing_bracket);
+ cx.add_action(Editor::undo_selection);
+ cx.add_action(Editor::redo_selection);
+ cx.add_action(Editor::go_to_diagnostic);
+ cx.add_action(Editor::go_to_prev_diagnostic);
+ cx.add_action(Editor::go_to_hunk);
+ cx.add_action(Editor::go_to_prev_hunk);
+ cx.add_action(Editor::go_to_definition);
+ cx.add_action(Editor::go_to_definition_split);
+ cx.add_action(Editor::go_to_type_definition);
+ cx.add_action(Editor::go_to_type_definition_split);
+ cx.add_action(Editor::fold);
+ cx.add_action(Editor::fold_at);
+ cx.add_action(Editor::unfold_lines);
+ cx.add_action(Editor::unfold_at);
+ cx.add_action(Editor::gutter_hover);
+ cx.add_action(Editor::fold_selected_ranges);
+ cx.add_action(Editor::show_completions);
+ cx.add_action(Editor::toggle_code_actions);
+ cx.add_action(Editor::open_excerpts);
+ cx.add_action(Editor::toggle_soft_wrap);
+ cx.add_action(Editor::toggle_inlay_hints);
+ cx.add_action(Editor::reveal_in_finder);
+ cx.add_action(Editor::copy_path);
+ cx.add_action(Editor::copy_relative_path);
+ cx.add_action(Editor::copy_highlight_json);
+ cx.add_async_action(Editor::format);
+ cx.add_action(Editor::restart_language_server);
+ cx.add_action(Editor::show_character_palette);
+ cx.add_async_action(Editor::confirm_completion);
+ cx.add_async_action(Editor::confirm_code_action);
+ cx.add_async_action(Editor::rename);
+ cx.add_async_action(Editor::confirm_rename);
+ cx.add_async_action(Editor::find_all_references);
+ cx.add_action(Editor::next_copilot_suggestion);
+ cx.add_action(Editor::previous_copilot_suggestion);
+ cx.add_action(Editor::copilot_suggest);
+ cx.add_action(Editor::context_menu_first);
+ cx.add_action(Editor::context_menu_prev);
+ cx.add_action(Editor::context_menu_next);
+ cx.add_action(Editor::context_menu_last);
+
+ hover_popover::init(cx);
+ scroll::actions::init(cx);
+
+ workspace::register_project_item::<Editor>(cx);
+ workspace::register_followable_item::<Editor>(cx);
+ workspace::register_deserializable_item::<Editor>(cx);
+}
+
+trait InvalidationRegion {
+ fn ranges(&self) -> &[Range<Anchor>];
+}
+
+#[derive(Clone, Debug, PartialEq)]
+pub enum SelectPhase {
+ Begin {
+ position: DisplayPoint,
+ add: bool,
+ click_count: usize,
+ },
+ BeginColumnar {
+ position: DisplayPoint,
+ goal_column: u32,
+ },
+ Extend {
+ position: DisplayPoint,
+ click_count: usize,
+ },
+ Update {
+ position: DisplayPoint,
+ goal_column: u32,
+ scroll_position: Vector2F,
+ },
+ End,
+}
+
+#[derive(Clone, Debug)]
+pub enum SelectMode {
+ Character,
+ Word(Range<Anchor>),
+ Line(Range<Anchor>),
+ All,
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum EditorMode {
+ SingleLine,
+ AutoHeight { max_lines: usize },
+ Full,
+}
+
+#[derive(Clone, Debug)]
+pub enum SoftWrap {
+ None,
+ EditorWidth,
+ Column(u32),
+}
+
+#[derive(Clone)]
+pub struct EditorStyle {
+ pub text: TextStyle,
+ pub line_height_scalar: f32,
+ pub placeholder_text: Option<TextStyle>,
+ pub theme: theme::Editor,
+ pub theme_id: usize,
+}
+
+type CompletionId = usize;
+
+type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
+type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
+
+type BackgroundHighlight = (fn(&Theme) -> Color, Vec<Range<Anchor>>);
+type InlayBackgroundHighlight = (fn(&Theme) -> Color, Vec<InlayHighlight>);
+
+pub struct Editor {
+ handle: WeakViewHandle<Self>,
+ buffer: ModelHandle<MultiBuffer>,
+ display_map: ModelHandle<DisplayMap>,
+ pub selections: SelectionsCollection,
+ pub scroll_manager: ScrollManager,
+ columnar_selection_tail: Option<Anchor>,
+ add_selections_state: Option<AddSelectionsState>,
+ select_next_state: Option<SelectNextState>,
+ select_prev_state: Option<SelectNextState>,
+ selection_history: SelectionHistory,
+ autoclose_regions: Vec<AutocloseRegion>,
+ snippet_stack: InvalidationStack<SnippetState>,
+ select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
+ ime_transaction: Option<TransactionId>,
+ active_diagnostics: Option<ActiveDiagnosticGroup>,
+ soft_wrap_mode_override: Option<language_settings::SoftWrap>,
+ get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
+ override_text_style: Option<Box<OverrideTextStyle>>,
+ project: Option<ModelHandle<Project>>,
+ collaboration_hub: Option<Box<dyn CollaborationHub>>,
+ focused: bool,
+ blink_manager: ModelHandle<BlinkManager>,
+ pub show_local_selections: bool,
+ mode: EditorMode,
+ show_gutter: bool,
+ show_wrap_guides: Option<bool>,
+ placeholder_text: Option<Arc<str>>,
+ highlighted_rows: Option<Range<u32>>,
+ background_highlights: BTreeMap<TypeId, BackgroundHighlight>,
+ inlay_background_highlights: TreeMap<Option<TypeId>, InlayBackgroundHighlight>,
+ nav_history: Option<ItemNavHistory>,
+ context_menu: RwLock<Option<ContextMenu>>,
+ mouse_context_menu: ViewHandle<context_menu::ContextMenu>,
+ completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
+ next_completion_id: CompletionId,
+ available_code_actions: Option<(ModelHandle<Buffer>, Arc<[CodeAction]>)>,
+ code_actions_task: Option<Task<()>>,
+ document_highlights_task: Option<Task<()>>,
+ pending_rename: Option<RenameState>,
+ searchable: bool,
+ cursor_shape: CursorShape,
+ collapse_matches: bool,
+ autoindent_mode: Option<AutoindentMode>,
+ workspace: Option<(WeakViewHandle<Workspace>, i64)>,
+ keymap_context_layers: BTreeMap<TypeId, KeymapContext>,
+ input_enabled: bool,
+ read_only: bool,
+ leader_peer_id: Option<PeerId>,
+ remote_id: Option<ViewId>,
+ hover_state: HoverState,
+ gutter_hovered: bool,
+ link_go_to_definition_state: LinkGoToDefinitionState,
+ copilot_state: CopilotState,
+ inlay_hint_cache: InlayHintCache,
+ next_inlay_id: usize,
+ _subscriptions: Vec<Subscription>,
+ pixel_position_of_newest_cursor: Option<Vector2F>,
+}
+
+pub struct EditorSnapshot {
+ pub mode: EditorMode,
+ pub show_gutter: bool,
+ pub display_snapshot: DisplaySnapshot,
+ pub placeholder_text: Option<Arc<str>>,
+ is_focused: bool,
+ scroll_anchor: ScrollAnchor,
+ ongoing_scroll: OngoingScroll,
+}
+
+pub struct RemoteSelection {
+ pub replica_id: ReplicaId,
+ pub selection: Selection<Anchor>,
+ pub cursor_shape: CursorShape,
+ pub peer_id: PeerId,
+ pub line_mode: bool,
+ pub participant_index: Option<ParticipantIndex>,
+}
+
+#[derive(Clone, Debug)]
+struct SelectionHistoryEntry {
+ selections: Arc<[Selection<Anchor>]>,
+ select_next_state: Option<SelectNextState>,
+ select_prev_state: Option<SelectNextState>,
+ add_selections_state: Option<AddSelectionsState>,
+}
+
+enum SelectionHistoryMode {
+ Normal,
+ Undoing,
+ Redoing,
+}
+
+impl Default for SelectionHistoryMode {
+ fn default() -> Self {
+ Self::Normal
+ }
+}
+
+#[derive(Default)]
+struct SelectionHistory {
+ #[allow(clippy::type_complexity)]
+ selections_by_transaction:
+ HashMap<TransactionId, (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)>,
+ mode: SelectionHistoryMode,
+ undo_stack: VecDeque<SelectionHistoryEntry>,
+ redo_stack: VecDeque<SelectionHistoryEntry>,
+}
+
+impl SelectionHistory {
+ fn insert_transaction(
+ &mut self,
+ transaction_id: TransactionId,
+ selections: Arc<[Selection<Anchor>]>,
+ ) {
+ self.selections_by_transaction
+ .insert(transaction_id, (selections, None));
+ }
+
+ #[allow(clippy::type_complexity)]
+ fn transaction(
+ &self,
+ transaction_id: TransactionId,
+ ) -> Option<&(Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+ self.selections_by_transaction.get(&transaction_id)
+ }
+
+ #[allow(clippy::type_complexity)]
+ fn transaction_mut(
+ &mut self,
+ transaction_id: TransactionId,
+ ) -> Option<&mut (Arc<[Selection<Anchor>]>, Option<Arc<[Selection<Anchor>]>>)> {
+ self.selections_by_transaction.get_mut(&transaction_id)
+ }
+
+ fn push(&mut self, entry: SelectionHistoryEntry) {
+ if !entry.selections.is_empty() {
+ match self.mode {
+ SelectionHistoryMode::Normal => {
+ self.push_undo(entry);
+ self.redo_stack.clear();
+ }
+ SelectionHistoryMode::Undoing => self.push_redo(entry),
+ SelectionHistoryMode::Redoing => self.push_undo(entry),
+ }
+ }
+ }
+
+ fn push_undo(&mut self, entry: SelectionHistoryEntry) {
+ if self
+ .undo_stack
+ .back()
+ .map_or(true, |e| e.selections != entry.selections)
+ {
+ self.undo_stack.push_back(entry);
+ if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN {
+ self.undo_stack.pop_front();
+ }
+ }
+ }
+
+ fn push_redo(&mut self, entry: SelectionHistoryEntry) {
+ if self
+ .redo_stack
+ .back()
+ .map_or(true, |e| e.selections != entry.selections)
+ {
+ self.redo_stack.push_back(entry);
+ if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN {
+ self.redo_stack.pop_front();
+ }
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+struct AddSelectionsState {
+ above: bool,
+ stack: Vec<usize>,
+}
+
+#[derive(Clone)]
+struct SelectNextState {
+ query: AhoCorasick,
+ wordwise: bool,
+ done: bool,
+}
+
+impl std::fmt::Debug for SelectNextState {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct(std::any::type_name::<Self>())
+ .field("wordwise", &self.wordwise)
+ .field("done", &self.done)
+ .finish()
+ }
+}
+
+#[derive(Debug)]
+struct AutocloseRegion {
+ selection_id: usize,
+ range: Range<Anchor>,
+ pair: BracketPair,
+}
+
+#[derive(Debug)]
+struct SnippetState {
+ ranges: Vec<Vec<Range<Anchor>>>,
+ active_index: usize,
+}
+
+pub struct RenameState {
+ pub range: Range<Anchor>,
+ pub old_name: Arc<str>,
+ pub editor: ViewHandle<Editor>,
+ block_id: BlockId,
+}
+
+struct InvalidationStack<T>(Vec<T>);
+
+enum ContextMenu {
+ Completions(CompletionsMenu),
+ CodeActions(CodeActionsMenu),
+}
+
+impl ContextMenu {
+ fn select_first(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ if self.visible() {
+ match self {
+ ContextMenu::Completions(menu) => menu.select_first(project, cx),
+ ContextMenu::CodeActions(menu) => menu.select_first(cx),
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ fn select_prev(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ if self.visible() {
+ match self {
+ ContextMenu::Completions(menu) => menu.select_prev(project, cx),
+ ContextMenu::CodeActions(menu) => menu.select_prev(cx),
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ fn select_next(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ if self.visible() {
+ match self {
+ ContextMenu::Completions(menu) => menu.select_next(project, cx),
+ ContextMenu::CodeActions(menu) => menu.select_next(cx),
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ fn select_last(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ if self.visible() {
+ match self {
+ ContextMenu::Completions(menu) => menu.select_last(project, cx),
+ ContextMenu::CodeActions(menu) => menu.select_last(cx),
+ }
+ true
+ } else {
+ false
+ }
+ }
+
+ fn visible(&self) -> bool {
+ match self {
+ ContextMenu::Completions(menu) => menu.visible(),
+ ContextMenu::CodeActions(menu) => menu.visible(),
+ }
+ }
+
+ fn render(
+ &self,
+ cursor_position: DisplayPoint,
+ style: EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> (DisplayPoint, AnyElement<Editor>) {
+ match self {
+ ContextMenu::Completions(menu) => (cursor_position, menu.render(style, workspace, cx)),
+ ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, cx),
+ }
+ }
+}
+
+#[derive(Clone)]
+struct CompletionsMenu {
+ id: CompletionId,
+ initial_position: Anchor,
+ buffer: ModelHandle<Buffer>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ match_candidates: Arc<[StringMatchCandidate]>,
+ matches: Arc<[StringMatch]>,
+ selected_item: usize,
+ list: UniformListState,
+}
+
+impl CompletionsMenu {
+ fn select_first(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ self.selected_item = 0;
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ self.attempt_resolve_selected_completion_documentation(project, cx);
+ cx.notify();
+ }
+
+ fn select_prev(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ if self.selected_item > 0 {
+ self.selected_item -= 1;
+ } else {
+ self.selected_item = self.matches.len() - 1;
+ }
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ self.attempt_resolve_selected_completion_documentation(project, cx);
+ cx.notify();
+ }
+
+ fn select_next(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ if self.selected_item + 1 < self.matches.len() {
+ self.selected_item += 1;
+ } else {
+ self.selected_item = 0;
+ }
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ self.attempt_resolve_selected_completion_documentation(project, cx);
+ cx.notify();
+ }
+
+ fn select_last(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ self.selected_item = self.matches.len() - 1;
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ self.attempt_resolve_selected_completion_documentation(project, cx);
+ cx.notify();
+ }
+
+ fn pre_resolve_completion_documentation(
+ &self,
+ project: Option<ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let settings = settings::get::<EditorSettings>(cx);
+ if !settings.show_completion_documentation {
+ return;
+ }
+
+ let Some(project) = project else {
+ return;
+ };
+ let client = project.read(cx).client();
+ let language_registry = project.read(cx).languages().clone();
+
+ let is_remote = project.read(cx).is_remote();
+ let project_id = project.read(cx).remote_id();
+
+ let completions = self.completions.clone();
+ let completion_indices: Vec<_> = self.matches.iter().map(|m| m.candidate_id).collect();
+
+ cx.spawn(move |this, mut cx| async move {
+ if is_remote {
+ let Some(project_id) = project_id else {
+ log::error!("Remote project without remote_id");
+ return;
+ };
+
+ for completion_index in completion_indices {
+ let completions_guard = completions.read();
+ let completion = &completions_guard[completion_index];
+ if completion.documentation.is_some() {
+ continue;
+ }
+
+ let server_id = completion.server_id;
+ let completion = completion.lsp_completion.clone();
+ drop(completions_guard);
+
+ Self::resolve_completion_documentation_remote(
+ project_id,
+ server_id,
+ completions.clone(),
+ completion_index,
+ completion,
+ client.clone(),
+ language_registry.clone(),
+ )
+ .await;
+
+ _ = this.update(&mut cx, |_, cx| cx.notify());
+ }
+ } else {
+ for completion_index in completion_indices {
+ let completions_guard = completions.read();
+ let completion = &completions_guard[completion_index];
+ if completion.documentation.is_some() {
+ continue;
+ }
+
+ let server_id = completion.server_id;
+ let completion = completion.lsp_completion.clone();
+ drop(completions_guard);
+
+ let server = project.read_with(&mut cx, |project, _| {
+ project.language_server_for_id(server_id)
+ });
+ let Some(server) = server else {
+ return;
+ };
+
+ Self::resolve_completion_documentation_local(
+ server,
+ completions.clone(),
+ completion_index,
+ completion,
+ language_registry.clone(),
+ )
+ .await;
+
+ _ = this.update(&mut cx, |_, cx| cx.notify());
+ }
+ }
+ })
+ .detach();
+ }
+
+ fn attempt_resolve_selected_completion_documentation(
+ &mut self,
+ project: Option<&ModelHandle<Project>>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let settings = settings::get::<EditorSettings>(cx);
+ if !settings.show_completion_documentation {
+ return;
+ }
+
+ let completion_index = self.matches[self.selected_item].candidate_id;
+ let Some(project) = project else {
+ return;
+ };
+ let language_registry = project.read(cx).languages().clone();
+
+ let completions = self.completions.clone();
+ let completions_guard = completions.read();
+ let completion = &completions_guard[completion_index];
+ if completion.documentation.is_some() {
+ return;
+ }
+
+ let server_id = completion.server_id;
+ let completion = completion.lsp_completion.clone();
+ drop(completions_guard);
+
+ if project.read(cx).is_remote() {
+ let Some(project_id) = project.read(cx).remote_id() else {
+ log::error!("Remote project without remote_id");
+ return;
+ };
+
+ let client = project.read(cx).client();
+
+ cx.spawn(move |this, mut cx| async move {
+ Self::resolve_completion_documentation_remote(
+ project_id,
+ server_id,
+ completions.clone(),
+ completion_index,
+ completion,
+ client,
+ language_registry.clone(),
+ )
+ .await;
+
+ _ = this.update(&mut cx, |_, cx| cx.notify());
+ })
+ .detach();
+ } else {
+ let Some(server) = project.read(cx).language_server_for_id(server_id) else {
+ return;
+ };
+
+ cx.spawn(move |this, mut cx| async move {
+ Self::resolve_completion_documentation_local(
+ server,
+ completions,
+ completion_index,
+ completion,
+ language_registry,
+ )
+ .await;
+
+ _ = this.update(&mut cx, |_, cx| cx.notify());
+ })
+ .detach();
+ }
+ }
+
+ async fn resolve_completion_documentation_remote(
+ project_id: u64,
+ server_id: LanguageServerId,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ completion_index: usize,
+ completion: lsp::CompletionItem,
+ client: Arc<Client>,
+ language_registry: Arc<LanguageRegistry>,
+ ) {
+ let request = proto::ResolveCompletionDocumentation {
+ project_id,
+ language_server_id: server_id.0 as u64,
+ lsp_completion: serde_json::to_string(&completion).unwrap().into_bytes(),
+ };
+
+ let Some(response) = client
+ .request(request)
+ .await
+ .context("completion documentation resolve proto request")
+ .log_err()
+ else {
+ return;
+ };
+
+ if response.text.is_empty() {
+ let mut completions = completions.write();
+ let completion = &mut completions[completion_index];
+ completion.documentation = Some(Documentation::Undocumented);
+ }
+
+ let documentation = if response.is_markdown {
+ Documentation::MultiLineMarkdown(
+ markdown::parse_markdown(&response.text, &language_registry, None).await,
+ )
+ } else if response.text.lines().count() <= 1 {
+ Documentation::SingleLine(response.text)
+ } else {
+ Documentation::MultiLinePlainText(response.text)
+ };
+
+ let mut completions = completions.write();
+ let completion = &mut completions[completion_index];
+ completion.documentation = Some(documentation);
+ }
+
+ async fn resolve_completion_documentation_local(
+ server: Arc<lsp::LanguageServer>,
+ completions: Arc<RwLock<Box<[Completion]>>>,
+ completion_index: usize,
+ completion: lsp::CompletionItem,
+ language_registry: Arc<LanguageRegistry>,
+ ) {
+ let can_resolve = server
+ .capabilities()
+ .completion_provider
+ .as_ref()
+ .and_then(|options| options.resolve_provider)
+ .unwrap_or(false);
+ if !can_resolve {
+ return;
+ }
+
+ let request = server.request::<lsp::request::ResolveCompletionItem>(completion);
+ let Some(completion_item) = request.await.log_err() else {
+ return;
+ };
+
+ if let Some(lsp_documentation) = completion_item.documentation {
+ let documentation = language::prepare_completion_documentation(
+ &lsp_documentation,
+ &language_registry,
+ None, // TODO: Try to reasonably work out which language the completion is for
+ )
+ .await;
+
+ let mut completions = completions.write();
+ let completion = &mut completions[completion_index];
+ completion.documentation = Some(documentation);
+ } else {
+ let mut completions = completions.write();
+ let completion = &mut completions[completion_index];
+ completion.documentation = Some(Documentation::Undocumented);
+ }
+ }
+
+ fn visible(&self) -> bool {
+ !self.matches.is_empty()
+ }
+
+ fn render(
+ &self,
+ style: EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> AnyElement<Editor> {
+ enum CompletionTag {}
+
+ let settings = settings::get::<EditorSettings>(cx);
+ let show_completion_documentation = settings.show_completion_documentation;
+
+ let widest_completion_ix = self
+ .matches
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, mat)| {
+ let completions = self.completions.read();
+ let completion = &completions[mat.candidate_id];
+ let documentation = &completion.documentation;
+
+ let mut len = completion.label.text.chars().count();
+ if let Some(Documentation::SingleLine(text)) = documentation {
+ if show_completion_documentation {
+ len += text.chars().count();
+ }
+ }
+
+ len
+ })
+ .map(|(ix, _)| ix);
+
+ let completions = self.completions.clone();
+ let matches = self.matches.clone();
+ let selected_item = self.selected_item;
+
+ let list = UniformList::new(self.list.clone(), matches.len(), cx, {
+ let style = style.clone();
+ move |_, range, items, cx| {
+ let start_ix = range.start;
+ let completions_guard = completions.read();
+
+ for (ix, mat) in matches[range].iter().enumerate() {
+ let item_ix = start_ix + ix;
+ let candidate_id = mat.candidate_id;
+ let completion = &completions_guard[candidate_id];
+
+ let documentation = if show_completion_documentation {
+ &completion.documentation
+ } else {
+ &None
+ };
+
+ items.push(
+ MouseEventHandler::new::<CompletionTag, _>(
+ mat.candidate_id,
+ cx,
+ |state, _| {
+ let item_style = if item_ix == selected_item {
+ style.autocomplete.selected_item
+ } else if state.hovered() {
+ style.autocomplete.hovered_item
+ } else {
+ style.autocomplete.item
+ };
+
+ let completion_label =
+ Text::new(completion.label.text.clone(), style.text.clone())
+ .with_soft_wrap(false)
+ .with_highlights(
+ combine_syntax_and_fuzzy_match_highlights(
+ &completion.label.text,
+ style.text.color.into(),
+ styled_runs_for_code_label(
+ &completion.label,
+ &style.syntax,
+ ),
+ &mat.positions,
+ ),
+ );
+
+ if let Some(Documentation::SingleLine(text)) = documentation {
+ Flex::row()
+ .with_child(completion_label)
+ .with_children((|| {
+ let text_style = TextStyle {
+ color: style.autocomplete.inline_docs_color,
+ font_size: style.text.font_size
+ * style.autocomplete.inline_docs_size_percent,
+ ..style.text.clone()
+ };
+
+ let label = Text::new(text.clone(), text_style)
+ .aligned()
+ .constrained()
+ .dynamically(move |constraint, _, _| {
+ gpui::SizeConstraint {
+ min: constraint.min,
+ max: vec2f(
+ constraint.max.x(),
+ constraint.min.y(),
+ ),
+ }
+ });
+
+ if Some(item_ix) == widest_completion_ix {
+ Some(
+ label
+ .contained()
+ .with_style(
+ style
+ .autocomplete
+ .inline_docs_container,
+ )
+ .into_any(),
+ )
+ } else {
+ Some(label.flex_float().into_any())
+ }
+ })())
+ .into_any()
+ } else {
+ completion_label.into_any()
+ }
+ .contained()
+ .with_style(item_style)
+ .constrained()
+ .dynamically(
+ move |constraint, _, _| {
+ if Some(item_ix) == widest_completion_ix {
+ constraint
+ } else {
+ gpui::SizeConstraint {
+ min: constraint.min,
+ max: constraint.min,
+ }
+ }
+ },
+ )
+ },
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, this, cx| {
+ this.confirm_completion(
+ &ConfirmCompletion {
+ item_ix: Some(item_ix),
+ },
+ cx,
+ )
+ .map(|task| task.detach());
+ })
+ .constrained()
+ .with_min_width(style.autocomplete.completion_min_width)
+ .with_max_width(style.autocomplete.completion_max_width)
+ .into_any(),
+ );
+ }
+ }
+ })
+ .with_width_from_item(widest_completion_ix);
+
+ enum MultiLineDocumentation {}
+
+ Flex::row()
+ .with_child(list.flex(1., false))
+ .with_children({
+ let mat = &self.matches[selected_item];
+ let completions = self.completions.read();
+ let completion = &completions[mat.candidate_id];
+ let documentation = &completion.documentation;
+
+ match documentation {
+ Some(Documentation::MultiLinePlainText(text)) => Some(
+ Flex::column()
+ .scrollable::<MultiLineDocumentation>(0, None, cx)
+ .with_child(
+ Text::new(text.clone(), style.text.clone()).with_soft_wrap(true),
+ )
+ .contained()
+ .with_style(style.autocomplete.alongside_docs_container)
+ .constrained()
+ .with_max_width(style.autocomplete.alongside_docs_max_width)
+ .flex(1., false),
+ ),
+
+ Some(Documentation::MultiLineMarkdown(parsed)) => Some(
+ Flex::column()
+ .scrollable::<MultiLineDocumentation>(0, None, cx)
+ .with_child(render_parsed_markdown::<MultiLineDocumentation>(
+ parsed, &style, workspace, cx,
+ ))
+ .contained()
+ .with_style(style.autocomplete.alongside_docs_container)
+ .constrained()
+ .with_max_width(style.autocomplete.alongside_docs_max_width)
+ .flex(1., false),
+ ),
+
+ _ => None,
+ }
+ })
+ .contained()
+ .with_style(style.autocomplete.container)
+ .into_any()
+ }
+
+ pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
+ let mut matches = if let Some(query) = query {
+ fuzzy::match_strings(
+ &self.match_candidates,
+ query,
+ query.chars().any(|c| c.is_uppercase()),
+ 100,
+ &Default::default(),
+ executor,
+ )
+ .await
+ } else {
+ self.match_candidates
+ .iter()
+ .enumerate()
+ .map(|(candidate_id, candidate)| StringMatch {
+ candidate_id,
+ score: Default::default(),
+ positions: Default::default(),
+ string: candidate.string.clone(),
+ })
+ .collect()
+ };
+
+ // Remove all candidates where the query's start does not match the start of any word in the candidate
+ if let Some(query) = query {
+ if let Some(query_start) = query.chars().next() {
+ matches.retain(|string_match| {
+ split_words(&string_match.string).any(|word| {
+ // Check that the first codepoint of the word as lowercase matches the first
+ // codepoint of the query as lowercase
+ word.chars()
+ .flat_map(|codepoint| codepoint.to_lowercase())
+ .zip(query_start.to_lowercase())
+ .all(|(word_cp, query_cp)| word_cp == query_cp)
+ })
+ });
+ }
+ }
+
+ let completions = self.completions.read();
+ matches.sort_unstable_by_key(|mat| {
+ let completion = &completions[mat.candidate_id];
+ (
+ completion.lsp_completion.sort_text.as_ref(),
+ Reverse(OrderedFloat(mat.score)),
+ completion.sort_key(),
+ )
+ });
+ drop(completions);
+
+ for mat in &mut matches {
+ let completions = self.completions.read();
+ let filter_start = completions[mat.candidate_id].label.filter_range.start;
+ for position in &mut mat.positions {
+ *position += filter_start;
+ }
+ }
+
+ self.matches = matches.into();
+ self.selected_item = 0;
+ }
+}
+
+#[derive(Clone)]
+struct CodeActionsMenu {
+ actions: Arc<[CodeAction]>,
+ buffer: ModelHandle<Buffer>,
+ selected_item: usize,
+ list: UniformListState,
+ deployed_from_indicator: bool,
+}
+
+impl CodeActionsMenu {
+ fn select_first(&mut self, cx: &mut ViewContext<Editor>) {
+ self.selected_item = 0;
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ cx.notify()
+ }
+
+ fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+ if self.selected_item > 0 {
+ self.selected_item -= 1;
+ } else {
+ self.selected_item = self.actions.len() - 1;
+ }
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ cx.notify();
+ }
+
+ fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+ if self.selected_item + 1 < self.actions.len() {
+ self.selected_item += 1;
+ } else {
+ self.selected_item = 0;
+ }
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ cx.notify();
+ }
+
+ fn select_last(&mut self, cx: &mut ViewContext<Editor>) {
+ self.selected_item = self.actions.len() - 1;
+ self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+ cx.notify()
+ }
+
+ fn visible(&self) -> bool {
+ !self.actions.is_empty()
+ }
+
+ fn render(
+ &self,
+ mut cursor_position: DisplayPoint,
+ style: EditorStyle,
+ cx: &mut ViewContext<Editor>,
+ ) -> (DisplayPoint, AnyElement<Editor>) {
+ enum ActionTag {}
+
+ let container_style = style.autocomplete.container;
+ let actions = self.actions.clone();
+ let selected_item = self.selected_item;
+ let element = UniformList::new(
+ self.list.clone(),
+ actions.len(),
+ cx,
+ move |_, range, items, cx| {
+ let start_ix = range.start;
+ for (ix, action) in actions[range].iter().enumerate() {
+ let item_ix = start_ix + ix;
+ items.push(
+ MouseEventHandler::new::<ActionTag, _>(item_ix, cx, |state, _| {
+ let item_style = if item_ix == selected_item {
+ style.autocomplete.selected_item
+ } else if state.hovered() {
+ style.autocomplete.hovered_item
+ } else {
+ style.autocomplete.item
+ };
+
+ Text::new(action.lsp_action.title.clone(), style.text.clone())
+ .with_soft_wrap(false)
+ .contained()
+ .with_style(item_style)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, this, cx| {
+ let workspace = this
+ .workspace
+ .as_ref()
+ .and_then(|(workspace, _)| workspace.upgrade(cx));
+ cx.window_context().defer(move |cx| {
+ if let Some(workspace) = workspace {
+ workspace.update(cx, |workspace, cx| {
+ if let Some(task) = Editor::confirm_code_action(
+ workspace,
+ &ConfirmCodeAction {
+ item_ix: Some(item_ix),
+ },
+ cx,
+ ) {
+ task.detach_and_log_err(cx);
+ }
+ });
+ }
+ });
+ })
+ .into_any(),
+ );
+ }
+ },
+ )
+ .with_width_from_item(
+ self.actions
+ .iter()
+ .enumerate()
+ .max_by_key(|(_, action)| action.lsp_action.title.chars().count())
+ .map(|(ix, _)| ix),
+ )
+ .contained()
+ .with_style(container_style)
+ .into_any();
+
+ if self.deployed_from_indicator {
+ *cursor_position.column_mut() = 0;
+ }
+
+ (cursor_position, element)
+ }
+}
+
+pub struct CopilotState {
+ excerpt_id: Option<ExcerptId>,
+ pending_refresh: Task<Option<()>>,
+ pending_cycling_refresh: Task<Option<()>>,
+ cycled: bool,
+ completions: Vec<copilot::Completion>,
+ active_completion_index: usize,
+ suggestion: Option<Inlay>,
+}
+
+impl Default for CopilotState {
+ fn default() -> Self {
+ Self {
+ excerpt_id: None,
+ pending_cycling_refresh: Task::ready(Some(())),
+ pending_refresh: Task::ready(Some(())),
+ completions: Default::default(),
+ active_completion_index: 0,
+ cycled: false,
+ suggestion: None,
+ }
+ }
+}
+
+impl CopilotState {
+ fn active_completion(&self) -> Option<&copilot::Completion> {
+ self.completions.get(self.active_completion_index)
+ }
+
+ fn text_for_active_completion(
+ &self,
+ cursor: Anchor,
+ buffer: &MultiBufferSnapshot,
+ ) -> Option<&str> {
+ use language::ToOffset as _;
+
+ let completion = self.active_completion()?;
+ let excerpt_id = self.excerpt_id?;
+ let completion_buffer = buffer.buffer_for_excerpt(excerpt_id)?;
+ if excerpt_id != cursor.excerpt_id
+ || !completion.range.start.is_valid(completion_buffer)
+ || !completion.range.end.is_valid(completion_buffer)
+ {
+ return None;
+ }
+
+ let mut completion_range = completion.range.to_offset(&completion_buffer);
+ let prefix_len = Self::common_prefix(
+ completion_buffer.chars_for_range(completion_range.clone()),
+ completion.text.chars(),
+ );
+ completion_range.start += prefix_len;
+ let suffix_len = Self::common_prefix(
+ completion_buffer.reversed_chars_for_range(completion_range.clone()),
+ completion.text[prefix_len..].chars().rev(),
+ );
+ completion_range.end = completion_range.end.saturating_sub(suffix_len);
+
+ if completion_range.is_empty()
+ && completion_range.start == cursor.text_anchor.to_offset(&completion_buffer)
+ {
+ Some(&completion.text[prefix_len..completion.text.len() - suffix_len])
+ } else {
+ None
+ }
+ }
+
+ fn cycle_completions(&mut self, direction: Direction) {
+ match direction {
+ Direction::Prev => {
+ self.active_completion_index = if self.active_completion_index == 0 {
+ self.completions.len().saturating_sub(1)
+ } else {
+ self.active_completion_index - 1
+ };
+ }
+ Direction::Next => {
+ if self.completions.len() == 0 {
+ self.active_completion_index = 0
+ } else {
+ self.active_completion_index =
+ (self.active_completion_index + 1) % self.completions.len();
+ }
+ }
+ }
+ }
+
+ fn push_completion(&mut self, new_completion: copilot::Completion) {
+ for completion in &self.completions {
+ if completion.text == new_completion.text && completion.range == new_completion.range {
+ return;
+ }
+ }
+ self.completions.push(new_completion);
+ }
+
+ fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b: T2) -> usize {
+ a.zip(b)
+ .take_while(|(a, b)| a == b)
+ .map(|(a, _)| a.len_utf8())
+ .sum()
+ }
+}
+
+#[derive(Debug)]
+struct ActiveDiagnosticGroup {
+ primary_range: Range<Anchor>,
+ primary_message: String,
+ blocks: HashMap<BlockId, Diagnostic>,
+ is_valid: bool,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct ClipboardSelection {
+ pub len: usize,
+ pub is_entire_line: bool,
+ pub first_line_indent: u32,
+}
+
+#[derive(Debug)]
+pub struct NavigationData {
+ cursor_anchor: Anchor,
+ cursor_position: Point,
+ scroll_anchor: ScrollAnchor,
+ scroll_top_row: u32,
+}
+
+pub struct EditorCreated(pub ViewHandle<Editor>);
+
+enum GotoDefinitionKind {
+ Symbol,
+ Type,
+}
+
+#[derive(Debug, Clone)]
+enum InlayHintRefreshReason {
+ Toggle(bool),
+ SettingsChange(InlayHintSettings),
+ NewLinesShown,
+ BufferEdited(HashSet<Arc<Language>>),
+ RefreshRequested,
+ ExcerptsRemoved(Vec<ExcerptId>),
+}
+impl InlayHintRefreshReason {
+ fn description(&self) -> &'static str {
+ match self {
+ Self::Toggle(_) => "toggle",
+ Self::SettingsChange(_) => "settings change",
+ Self::NewLinesShown => "new lines shown",
+ Self::BufferEdited(_) => "buffer edited",
+ Self::RefreshRequested => "refresh requested",
+ Self::ExcerptsRemoved(_) => "excerpts removed",
+ }
+ }
+}
+
+impl Editor {
+ pub fn single_line(
+ field_editor_style: Option<Arc<GetFieldEditorTheme>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx)
+ }
+
+ pub fn multi_line(
+ field_editor_style: Option<Arc<GetFieldEditorTheme>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ Self::new(EditorMode::Full, buffer, None, field_editor_style, cx)
+ }
+
+ pub fn auto_height(
+ max_lines: usize,
+ field_editor_style: Option<Arc<GetFieldEditorTheme>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new()));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ Self::new(
+ EditorMode::AutoHeight { max_lines },
+ buffer,
+ None,
+ field_editor_style,
+ cx,
+ )
+ }
+
+ pub fn for_buffer(
+ buffer: ModelHandle<Buffer>,
+ project: Option<ModelHandle<Project>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ Self::new(EditorMode::Full, buffer, project, None, cx)
+ }
+
+ pub fn for_multibuffer(
+ buffer: ModelHandle<MultiBuffer>,
+ project: Option<ModelHandle<Project>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ Self::new(EditorMode::Full, buffer, project, None, cx)
+ }
+
+ pub fn clone(&self, cx: &mut ViewContext<Self>) -> Self {
+ let mut clone = Self::new(
+ self.mode,
+ self.buffer.clone(),
+ self.project.clone(),
+ self.get_field_editor_theme.clone(),
+ cx,
+ );
+ self.display_map.update(cx, |display_map, cx| {
+ let snapshot = display_map.snapshot(cx);
+ clone.display_map.update(cx, |display_map, cx| {
+ display_map.set_state(&snapshot, cx);
+ });
+ });
+ clone.selections.clone_state(&self.selections);
+ clone.scroll_manager.clone_state(&self.scroll_manager);
+ clone.searchable = self.searchable;
+ clone
+ }
+
+ fn new(
+ mode: EditorMode,
+ buffer: ModelHandle<MultiBuffer>,
+ project: Option<ModelHandle<Project>>,
+ get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let editor_view_id = cx.view_id();
+ let display_map = cx.add_model(|cx| {
+ let settings = settings::get::<ThemeSettings>(cx);
+ let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx);
+ DisplayMap::new(
+ buffer.clone(),
+ style.text.font_id,
+ style.text.font_size,
+ None,
+ 2,
+ 1,
+ cx,
+ )
+ });
+
+ let selections = SelectionsCollection::new(display_map.clone(), buffer.clone());
+
+ let blink_manager = cx.add_model(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx));
+
+ let soft_wrap_mode_override =
+ (mode == EditorMode::SingleLine).then(|| language_settings::SoftWrap::None);
+
+ let mut project_subscriptions = Vec::new();
+ if mode == EditorMode::Full {
+ if let Some(project) = project.as_ref() {
+ if buffer.read(cx).is_singleton() {
+ project_subscriptions.push(cx.observe(project, |_, _, cx| {
+ cx.emit(Event::TitleChanged);
+ }));
+ }
+ project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| {
+ if let project::Event::RefreshInlayHints = event {
+ editor.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
+ };
+ }));
+ }
+ }
+
+ let inlay_hint_settings = inlay_hint_settings(
+ selections.newest_anchor().head(),
+ &buffer.read(cx).snapshot(cx),
+ cx,
+ );
+
+ let mut this = Self {
+ handle: cx.weak_handle(),
+ buffer: buffer.clone(),
+ display_map: display_map.clone(),
+ selections,
+ scroll_manager: ScrollManager::new(),
+ columnar_selection_tail: None,
+ add_selections_state: None,
+ select_next_state: None,
+ select_prev_state: None,
+ selection_history: Default::default(),
+ autoclose_regions: Default::default(),
+ snippet_stack: Default::default(),
+ select_larger_syntax_node_stack: Vec::new(),
+ ime_transaction: Default::default(),
+ active_diagnostics: None,
+ soft_wrap_mode_override,
+ get_field_editor_theme,
+ collaboration_hub: project.clone().map(|project| Box::new(project) as _),
+ project,
+ focused: false,
+ blink_manager: blink_manager.clone(),
+ show_local_selections: true,
+ mode,
+ show_gutter: mode == EditorMode::Full,
+ show_wrap_guides: None,
+ placeholder_text: None,
+ highlighted_rows: None,
+ background_highlights: Default::default(),
+ inlay_background_highlights: Default::default(),
+ nav_history: None,
+ context_menu: RwLock::new(None),
+ mouse_context_menu: cx
+ .add_view(|cx| context_menu::ContextMenu::new(editor_view_id, cx)),
+ completion_tasks: Default::default(),
+ next_completion_id: 0,
+ next_inlay_id: 0,
+ available_code_actions: Default::default(),
+ code_actions_task: Default::default(),
+ document_highlights_task: Default::default(),
+ pending_rename: Default::default(),
+ searchable: true,
+ override_text_style: None,
+ cursor_shape: Default::default(),
+ autoindent_mode: Some(AutoindentMode::EachLine),
+ collapse_matches: false,
+ workspace: None,
+ keymap_context_layers: Default::default(),
+ input_enabled: true,
+ read_only: false,
+ leader_peer_id: None,
+ remote_id: None,
+ hover_state: Default::default(),
+ link_go_to_definition_state: Default::default(),
+ copilot_state: Default::default(),
+ inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
+ gutter_hovered: false,
+ pixel_position_of_newest_cursor: None,
+ _subscriptions: vec![
+ cx.observe(&buffer, Self::on_buffer_changed),
+ cx.subscribe(&buffer, Self::on_buffer_event),
+ cx.observe(&display_map, Self::on_display_map_changed),
+ cx.observe(&blink_manager, |_, _, cx| cx.notify()),
+ cx.observe_global::<SettingsStore, _>(Self::settings_changed),
+ cx.observe_window_activation(|editor, active, cx| {
+ editor.blink_manager.update(cx, |blink_manager, cx| {
+ if active {
+ blink_manager.enable(cx);
+ } else {
+ blink_manager.show_cursor(cx);
+ blink_manager.disable(cx);
+ }
+ });
+ }),
+ ],
+ };
+
+ this._subscriptions.extend(project_subscriptions);
+
+ this.end_selection(cx);
+ this.scroll_manager.show_scrollbar(cx);
+
+ let editor_created_event = EditorCreated(cx.handle());
+ cx.emit_global(editor_created_event);
+
+ if mode == EditorMode::Full {
+ let should_auto_hide_scrollbars = cx.platform().should_auto_hide_scrollbars();
+ cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars));
+ }
+
+ this.report_editor_event("open", None, cx);
+ this
+ }
+
+ pub fn new_file(
+ workspace: &mut Workspace,
+ _: &workspace::NewFile,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let project = workspace.project().clone();
+ if project.read(cx).is_remote() {
+ cx.propagate_action();
+ } else if let Some(buffer) = project
+ .update(cx, |project, cx| project.create_buffer("", None, cx))
+ .log_err()
+ {
+ workspace.add_item(
+ Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+ cx,
+ );
+ }
+ }
+
+ pub fn new_file_in_direction(
+ workspace: &mut Workspace,
+ action: &workspace::NewFileInDirection,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let project = workspace.project().clone();
+ if project.read(cx).is_remote() {
+ cx.propagate_action();
+ } else if let Some(buffer) = project
+ .update(cx, |project, cx| project.create_buffer("", None, cx))
+ .log_err()
+ {
+ workspace.split_item(
+ action.0,
+ Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))),
+ cx,
+ );
+ }
+ }
+
+ pub fn replica_id(&self, cx: &AppContext) -> ReplicaId {
+ self.buffer.read(cx).replica_id()
+ }
+
+ pub fn leader_peer_id(&self) -> Option<PeerId> {
+ self.leader_peer_id
+ }
+
+ pub fn buffer(&self) -> &ModelHandle<MultiBuffer> {
+ &self.buffer
+ }
+
+ fn workspace(&self, cx: &AppContext) -> Option<ViewHandle<Workspace>> {
+ self.workspace.as_ref()?.0.upgrade(cx)
+ }
+
+ pub fn title<'a>(&self, cx: &'a AppContext) -> Cow<'a, str> {
+ self.buffer().read(cx).title(cx)
+ }
+
+ pub fn snapshot(&mut self, cx: &mut WindowContext) -> EditorSnapshot {
+ EditorSnapshot {
+ mode: self.mode,
+ show_gutter: self.show_gutter,
+ display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
+ scroll_anchor: self.scroll_manager.anchor(),
+ ongoing_scroll: self.scroll_manager.ongoing_scroll(),
+ placeholder_text: self.placeholder_text.clone(),
+ is_focused: self
+ .handle
+ .upgrade(cx)
+ .map_or(false, |handle| handle.is_focused(cx)),
+ }
+ }
+
+ pub fn language_at<'a, T: ToOffset>(
+ &self,
+ point: T,
+ cx: &'a AppContext,
+ ) -> Option<Arc<Language>> {
+ self.buffer.read(cx).language_at(point, cx)
+ }
+
+ pub fn file_at<'a, T: ToOffset>(&self, point: T, cx: &'a AppContext) -> Option<Arc<dyn File>> {
+ self.buffer.read(cx).read(cx).file_at(point).cloned()
+ }
+
+ pub fn active_excerpt(
+ &self,
+ cx: &AppContext,
+ ) -> Option<(ExcerptId, ModelHandle<Buffer>, Range<text::Anchor>)> {
+ self.buffer
+ .read(cx)
+ .excerpt_containing(self.selections.newest_anchor().head(), cx)
+ }
+
+ pub fn style(&self, cx: &AppContext) -> EditorStyle {
+ build_style(
+ settings::get::<ThemeSettings>(cx),
+ self.get_field_editor_theme.as_deref(),
+ self.override_text_style.as_deref(),
+ cx,
+ )
+ }
+
+ pub fn mode(&self) -> EditorMode {
+ self.mode
+ }
+
+ pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> {
+ self.collaboration_hub.as_deref()
+ }
+
+ pub fn set_collaboration_hub(&mut self, hub: Box<dyn CollaborationHub>) {
+ self.collaboration_hub = Some(hub);
+ }
+
+ pub fn set_placeholder_text(
+ &mut self,
+ placeholder_text: impl Into<Arc<str>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.placeholder_text = Some(placeholder_text.into());
+ cx.notify();
+ }
+
+ pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
+ self.cursor_shape = cursor_shape;
+ cx.notify();
+ }
+
+ pub fn set_collapse_matches(&mut self, collapse_matches: bool) {
+ self.collapse_matches = collapse_matches;
+ }
+
+ pub fn range_for_match<T: std::marker::Copy>(&self, range: &Range<T>) -> Range<T> {
+ if self.collapse_matches {
+ return range.start..range.start;
+ }
+ range.clone()
+ }
+
+ pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext<Self>) {
+ if self.display_map.read(cx).clip_at_line_ends != clip {
+ self.display_map
+ .update(cx, |map, _| map.clip_at_line_ends = clip);
+ }
+ }
+
+ pub fn set_keymap_context_layer<Tag: 'static>(
+ &mut self,
+ context: KeymapContext,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.keymap_context_layers
+ .insert(TypeId::of::<Tag>(), context);
+ cx.notify();
+ }
+
+ pub fn remove_keymap_context_layer<Tag: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+ self.keymap_context_layers.remove(&TypeId::of::<Tag>());
+ cx.notify();
+ }
+
+ pub fn set_input_enabled(&mut self, input_enabled: bool) {
+ self.input_enabled = input_enabled;
+ }
+
+ pub fn set_autoindent(&mut self, autoindent: bool) {
+ if autoindent {
+ self.autoindent_mode = Some(AutoindentMode::EachLine);
+ } else {
+ self.autoindent_mode = None;
+ }
+ }
+
+ pub fn read_only(&self) -> bool {
+ self.read_only
+ }
+
+ pub fn set_read_only(&mut self, read_only: bool) {
+ self.read_only = read_only;
+ }
+
+ pub fn set_field_editor_style(
+ &mut self,
+ style: Option<Arc<GetFieldEditorTheme>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.get_field_editor_theme = style;
+ cx.notify();
+ }
+
+ fn selections_did_change(
+ &mut self,
+ local: bool,
+ old_cursor_position: &Anchor,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if self.focused && self.leader_peer_id.is_none() {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.set_active_selections(
+ &self.selections.disjoint_anchors(),
+ self.selections.line_mode,
+ self.cursor_shape,
+ cx,
+ )
+ });
+ }
+
+ let display_map = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.snapshot(cx));
+ let buffer = &display_map.buffer_snapshot;
+ self.add_selections_state = None;
+ self.select_next_state = None;
+ self.select_prev_state = None;
+ self.select_larger_syntax_node_stack.clear();
+ self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
+ self.snippet_stack
+ .invalidate(&self.selections.disjoint_anchors(), buffer);
+ self.take_rename(false, cx);
+
+ let new_cursor_position = self.selections.newest_anchor().head();
+
+ self.push_to_nav_history(
+ old_cursor_position.clone(),
+ Some(new_cursor_position.to_point(buffer)),
+ cx,
+ );
+
+ if local {
+ let new_cursor_position = self.selections.newest_anchor().head();
+ let mut context_menu = self.context_menu.write();
+ let completion_menu = match context_menu.as_ref() {
+ Some(ContextMenu::Completions(menu)) => Some(menu),
+
+ _ => {
+ *context_menu = None;
+ None
+ }
+ };
+
+ if let Some(completion_menu) = completion_menu {
+ let cursor_position = new_cursor_position.to_offset(buffer);
+ let (word_range, kind) =
+ buffer.surrounding_word(completion_menu.initial_position.clone());
+ if kind == Some(CharKind::Word)
+ && word_range.to_inclusive().contains(&cursor_position)
+ {
+ let mut completion_menu = completion_menu.clone();
+ drop(context_menu);
+
+ let query = Self::completion_query(buffer, cursor_position);
+ cx.spawn(move |this, mut cx| async move {
+ completion_menu
+ .filter(query.as_deref(), cx.background().clone())
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ let mut context_menu = this.context_menu.write();
+ let Some(ContextMenu::Completions(menu)) = context_menu.as_ref() else {
+ return;
+ };
+
+ if menu.id > completion_menu.id {
+ return;
+ }
+
+ *context_menu = Some(ContextMenu::Completions(completion_menu));
+ drop(context_menu);
+ cx.notify();
+ })
+ })
+ .detach();
+
+ self.show_completions(&ShowCompletions, cx);
+ } else {
+ drop(context_menu);
+ self.hide_context_menu(cx);
+ }
+ } else {
+ drop(context_menu);
+ }
+
+ hide_hover(self, cx);
+
+ if old_cursor_position.to_display_point(&display_map).row()
+ != new_cursor_position.to_display_point(&display_map).row()
+ {
+ self.available_code_actions.take();
+ }
+ self.refresh_code_actions(cx);
+ self.refresh_document_highlights(cx);
+ refresh_matching_bracket_highlights(self, cx);
+ self.discard_copilot_suggestion(cx);
+ }
+
+ self.blink_manager.update(cx, BlinkManager::pause_blinking);
+ cx.emit(Event::SelectionsChanged { local });
+ cx.notify();
+ }
+
+ pub fn change_selections<R>(
+ &mut self,
+ autoscroll: Option<Autoscroll>,
+ cx: &mut ViewContext<Self>,
+ change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
+ ) -> R {
+ let old_cursor_position = self.selections.newest_anchor().head();
+ self.push_to_selection_history();
+
+ let (changed, result) = self.selections.change_with(cx, change);
+
+ if changed {
+ if let Some(autoscroll) = autoscroll {
+ self.request_autoscroll(autoscroll, cx);
+ }
+ self.selections_did_change(true, &old_cursor_position, cx);
+ }
+
+ result
+ }
+
+ pub fn edit<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
+ where
+ I: IntoIterator<Item = (Range<S>, T)>,
+ S: ToOffset,
+ T: Into<Arc<str>>,
+ {
+ if self.read_only {
+ return;
+ }
+
+ self.buffer
+ .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
+ }
+
+ pub fn edit_with_autoindent<I, S, T>(&mut self, edits: I, cx: &mut ViewContext<Self>)
+ where
+ I: IntoIterator<Item = (Range<S>, T)>,
+ S: ToOffset,
+ T: Into<Arc<str>>,
+ {
+ if self.read_only {
+ return;
+ }
+
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, self.autoindent_mode.clone(), cx)
+ });
+ }
+
+ pub fn edit_with_block_indent<I, S, T>(
+ &mut self,
+ edits: I,
+ original_indent_columns: Vec<u32>,
+ cx: &mut ViewContext<Self>,
+ ) where
+ I: IntoIterator<Item = (Range<S>, T)>,
+ S: ToOffset,
+ T: Into<Arc<str>>,
+ {
+ if self.read_only {
+ return;
+ }
+
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ edits,
+ Some(AutoindentMode::Block {
+ original_indent_columns,
+ }),
+ cx,
+ )
+ });
+ }
+
+ fn select(&mut self, phase: SelectPhase, cx: &mut ViewContext<Self>) {
+ self.hide_context_menu(cx);
+
+ match phase {
+ SelectPhase::Begin {
+ position,
+ add,
+ click_count,
+ } => self.begin_selection(position, add, click_count, cx),
+ SelectPhase::BeginColumnar {
+ position,
+ goal_column,
+ } => self.begin_columnar_selection(position, goal_column, cx),
+ SelectPhase::Extend {
+ position,
+ click_count,
+ } => self.extend_selection(position, click_count, cx),
+ SelectPhase::Update {
+ position,
+ goal_column,
+ scroll_position,
+ } => self.update_selection(position, goal_column, scroll_position, cx),
+ SelectPhase::End => self.end_selection(cx),
+ }
+ }
+
+ fn extend_selection(
+ &mut self,
+ position: DisplayPoint,
+ click_count: usize,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let tail = self.selections.newest::<usize>(cx).tail();
+ self.begin_selection(position, false, click_count, cx);
+
+ let position = position.to_offset(&display_map, Bias::Left);
+ let tail_anchor = display_map.buffer_snapshot.anchor_before(tail);
+
+ let mut pending_selection = self
+ .selections
+ .pending_anchor()
+ .expect("extend_selection not called with pending selection");
+ if position >= tail {
+ pending_selection.start = tail_anchor;
+ } else {
+ pending_selection.end = tail_anchor;
+ pending_selection.reversed = true;
+ }
+
+ let mut pending_mode = self.selections.pending_mode().unwrap();
+ match &mut pending_mode {
+ SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor,
+ _ => {}
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.set_pending(pending_selection, pending_mode)
+ });
+ }
+
+ fn begin_selection(
+ &mut self,
+ position: DisplayPoint,
+ add: bool,
+ click_count: usize,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if !self.focused {
+ cx.focus_self();
+ }
+
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = &display_map.buffer_snapshot;
+ let newest_selection = self.selections.newest_anchor().clone();
+ let position = display_map.clip_point(position, Bias::Left);
+
+ let start;
+ let end;
+ let mode;
+ let auto_scroll;
+ match click_count {
+ 1 => {
+ start = buffer.anchor_before(position.to_point(&display_map));
+ end = start.clone();
+ mode = SelectMode::Character;
+ auto_scroll = true;
+ }
+ 2 => {
+ let range = movement::surrounding_word(&display_map, position);
+ start = buffer.anchor_before(range.start.to_point(&display_map));
+ end = buffer.anchor_before(range.end.to_point(&display_map));
+ mode = SelectMode::Word(start.clone()..end.clone());
+ auto_scroll = true;
+ }
+ 3 => {
+ let position = display_map
+ .clip_point(position, Bias::Left)
+ .to_point(&display_map);
+ let line_start = display_map.prev_line_boundary(position).0;
+ let next_line_start = buffer.clip_point(
+ display_map.next_line_boundary(position).0 + Point::new(1, 0),
+ Bias::Left,
+ );
+ start = buffer.anchor_before(line_start);
+ end = buffer.anchor_before(next_line_start);
+ mode = SelectMode::Line(start.clone()..end.clone());
+ auto_scroll = true;
+ }
+ _ => {
+ start = buffer.anchor_before(0);
+ end = buffer.anchor_before(buffer.len());
+ mode = SelectMode::All;
+ auto_scroll = false;
+ }
+ }
+
+ self.change_selections(auto_scroll.then(|| Autoscroll::newest()), cx, |s| {
+ if !add {
+ s.clear_disjoint();
+ } else if click_count > 1 {
+ s.delete(newest_selection.id)
+ }
+
+ s.set_pending_anchor_range(start..end, mode);
+ });
+ }
+
+ fn begin_columnar_selection(
+ &mut self,
+ position: DisplayPoint,
+ goal_column: u32,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if !self.focused {
+ cx.focus_self();
+ }
+
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let tail = self.selections.newest::<Point>(cx).tail();
+ self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail));
+
+ self.select_columns(
+ tail.to_display_point(&display_map),
+ position,
+ goal_column,
+ &display_map,
+ cx,
+ );
+ }
+
+ fn update_selection(
+ &mut self,
+ position: DisplayPoint,
+ goal_column: u32,
+ scroll_position: Vector2F,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ if let Some(tail) = self.columnar_selection_tail.as_ref() {
+ let tail = tail.to_display_point(&display_map);
+ self.select_columns(tail, position, goal_column, &display_map, cx);
+ } else if let Some(mut pending) = self.selections.pending_anchor() {
+ let buffer = self.buffer.read(cx).snapshot(cx);
+ let head;
+ let tail;
+ let mode = self.selections.pending_mode().unwrap();
+ match &mode {
+ SelectMode::Character => {
+ head = position.to_point(&display_map);
+ tail = pending.tail().to_point(&buffer);
+ }
+ SelectMode::Word(original_range) => {
+ let original_display_range = original_range.start.to_display_point(&display_map)
+ ..original_range.end.to_display_point(&display_map);
+ let original_buffer_range = original_display_range.start.to_point(&display_map)
+ ..original_display_range.end.to_point(&display_map);
+ if movement::is_inside_word(&display_map, position)
+ || original_display_range.contains(&position)
+ {
+ let word_range = movement::surrounding_word(&display_map, position);
+ if word_range.start < original_display_range.start {
+ head = word_range.start.to_point(&display_map);
+ } else {
+ head = word_range.end.to_point(&display_map);
+ }
+ } else {
+ head = position.to_point(&display_map);
+ }
+
+ if head <= original_buffer_range.start {
+ tail = original_buffer_range.end;
+ } else {
+ tail = original_buffer_range.start;
+ }
+ }
+ SelectMode::Line(original_range) => {
+ let original_range = original_range.to_point(&display_map.buffer_snapshot);
+
+ let position = display_map
+ .clip_point(position, Bias::Left)
+ .to_point(&display_map);
+ let line_start = display_map.prev_line_boundary(position).0;
+ let next_line_start = buffer.clip_point(
+ display_map.next_line_boundary(position).0 + Point::new(1, 0),
+ Bias::Left,
+ );
+
+ if line_start < original_range.start {
+ head = line_start
+ } else {
+ head = next_line_start
+ }
+
+ if head <= original_range.start {
+ tail = original_range.end;
+ } else {
+ tail = original_range.start;
+ }
+ }
+ SelectMode::All => {
+ return;
+ }
+ };
+
+ if head < tail {
+ pending.start = buffer.anchor_before(head);
+ pending.end = buffer.anchor_before(tail);
+ pending.reversed = true;
+ } else {
+ pending.start = buffer.anchor_before(tail);
+ pending.end = buffer.anchor_before(head);
+ pending.reversed = false;
+ }
+
+ self.change_selections(None, cx, |s| {
+ s.set_pending(pending, mode);
+ });
+ } else {
+ error!("update_selection dispatched with no pending selection");
+ return;
+ }
+
+ self.set_scroll_position(scroll_position, cx);
+ cx.notify();
+ }
+
+ fn end_selection(&mut self, cx: &mut ViewContext<Self>) {
+ self.columnar_selection_tail.take();
+ if self.selections.pending_anchor().is_some() {
+ let selections = self.selections.all::<usize>(cx);
+ self.change_selections(None, cx, |s| {
+ s.select(selections);
+ s.clear_pending();
+ });
+ }
+ }
+
+ fn select_columns(
+ &mut self,
+ tail: DisplayPoint,
+ head: DisplayPoint,
+ goal_column: u32,
+ display_map: &DisplaySnapshot,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let start_row = cmp::min(tail.row(), head.row());
+ let end_row = cmp::max(tail.row(), head.row());
+ let start_column = cmp::min(tail.column(), goal_column);
+ let end_column = cmp::max(tail.column(), goal_column);
+ let reversed = start_column < tail.column();
+
+ let selection_ranges = (start_row..=end_row)
+ .filter_map(|row| {
+ if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) {
+ let start = display_map
+ .clip_point(DisplayPoint::new(row, start_column), Bias::Left)
+ .to_point(display_map);
+ let end = display_map
+ .clip_point(DisplayPoint::new(row, end_column), Bias::Right)
+ .to_point(display_map);
+ if reversed {
+ Some(end..start)
+ } else {
+ Some(start..end)
+ }
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+
+ self.change_selections(None, cx, |s| {
+ s.select_ranges(selection_ranges);
+ });
+ cx.notify();
+ }
+
+ pub fn has_pending_nonempty_selection(&self) -> bool {
+ let pending_nonempty_selection = match self.selections.pending_anchor() {
+ Some(Selection { start, end, .. }) => start != end,
+ None => false,
+ };
+ pending_nonempty_selection || self.columnar_selection_tail.is_some()
+ }
+
+ pub fn has_pending_selection(&self) -> bool {
+ self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some()
+ }
+
+ pub fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
+ if self.take_rename(false, cx).is_some() {
+ return;
+ }
+
+ if hide_hover(self, cx) {
+ return;
+ }
+
+ if self.hide_context_menu(cx).is_some() {
+ return;
+ }
+
+ if self.discard_copilot_suggestion(cx) {
+ return;
+ }
+
+ if self.snippet_stack.pop().is_some() {
+ return;
+ }
+
+ if self.mode == EditorMode::Full {
+ if self.active_diagnostics.is_some() {
+ self.dismiss_diagnostics(cx);
+ return;
+ }
+
+ if self.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel()) {
+ return;
+ }
+ }
+
+ cx.propagate_action();
+ }
+
+ pub fn handle_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+ let text: Arc<str> = text.into();
+
+ if self.read_only {
+ return;
+ }
+
+ let selections = self.selections.all_adjusted(cx);
+ let mut brace_inserted = false;
+ let mut edits = Vec::new();
+ let mut new_selections = Vec::with_capacity(selections.len());
+ let mut new_autoclose_regions = Vec::new();
+ let snapshot = self.buffer.read(cx).read(cx);
+
+ for (selection, autoclose_region) in
+ self.selections_with_autoclose_regions(selections, &snapshot)
+ {
+ if let Some(scope) = snapshot.language_scope_at(selection.head()) {
+ // Determine if the inserted text matches the opening or closing
+ // bracket of any of this language's bracket pairs.
+ let mut bracket_pair = None;
+ let mut is_bracket_pair_start = false;
+ if !text.is_empty() {
+ // `text` can be empty when an user is using IME (e.g. Chinese Wubi Simplified)
+ // and they are removing the character that triggered IME popup.
+ for (pair, enabled) in scope.brackets() {
+ if enabled && pair.close && pair.start.ends_with(text.as_ref()) {
+ bracket_pair = Some(pair.clone());
+ is_bracket_pair_start = true;
+ break;
+ } else if pair.end.as_str() == text.as_ref() {
+ bracket_pair = Some(pair.clone());
+ break;
+ }
+ }
+ }
+
+ if let Some(bracket_pair) = bracket_pair {
+ if selection.is_empty() {
+ if is_bracket_pair_start {
+ let prefix_len = bracket_pair.start.len() - text.len();
+
+ // If the inserted text is a suffix of an opening bracket and the
+ // selection is preceded by the rest of the opening bracket, then
+ // insert the closing bracket.
+ let following_text_allows_autoclose = snapshot
+ .chars_at(selection.start)
+ .next()
+ .map_or(true, |c| scope.should_autoclose_before(c));
+ let preceding_text_matches_prefix = prefix_len == 0
+ || (selection.start.column >= (prefix_len as u32)
+ && snapshot.contains_str_at(
+ Point::new(
+ selection.start.row,
+ selection.start.column - (prefix_len as u32),
+ ),
+ &bracket_pair.start[..prefix_len],
+ ));
+ if following_text_allows_autoclose && preceding_text_matches_prefix {
+ let anchor = snapshot.anchor_before(selection.end);
+ new_selections.push((selection.map(|_| anchor), text.len()));
+ new_autoclose_regions.push((
+ anchor,
+ text.len(),
+ selection.id,
+ bracket_pair.clone(),
+ ));
+ edits.push((
+ selection.range(),
+ format!("{}{}", text, bracket_pair.end).into(),
+ ));
+ brace_inserted = true;
+ continue;
+ }
+ }
+
+ if let Some(region) = autoclose_region {
+ // If the selection is followed by an auto-inserted closing bracket,
+ // then don't insert that closing bracket again; just move the selection
+ // past the closing bracket.
+ let should_skip = selection.end == region.range.end.to_point(&snapshot)
+ && text.as_ref() == region.pair.end.as_str();
+ if should_skip {
+ let anchor = snapshot.anchor_after(selection.end);
+ new_selections
+ .push((selection.map(|_| anchor), region.pair.end.len()));
+ continue;
+ }
+ }
+ }
+ // If an opening bracket is 1 character long and is typed while
+ // text is selected, then surround that text with the bracket pair.
+ else if is_bracket_pair_start && bracket_pair.start.chars().count() == 1 {
+ edits.push((selection.start..selection.start, text.clone()));
+ edits.push((
+ selection.end..selection.end,
+ bracket_pair.end.as_str().into(),
+ ));
+ brace_inserted = true;
+ new_selections.push((
+ Selection {
+ id: selection.id,
+ start: snapshot.anchor_after(selection.start),
+ end: snapshot.anchor_before(selection.end),
+ reversed: selection.reversed,
+ goal: selection.goal,
+ },
+ 0,
+ ));
+ continue;
+ }
+ }
+ }
+
+ // If not handling any auto-close operation, then just replace the selected
+ // text with the given input and move the selection to the end of the
+ // newly inserted text.
+ let anchor = snapshot.anchor_after(selection.end);
+ new_selections.push((selection.map(|_| anchor), 0));
+ edits.push((selection.start..selection.end, text.clone()));
+ }
+
+ drop(snapshot);
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, this.autoindent_mode.clone(), cx);
+ });
+
+ let new_anchor_selections = new_selections.iter().map(|e| &e.0);
+ let new_selection_deltas = new_selections.iter().map(|e| e.1);
+ let snapshot = this.buffer.read(cx).read(cx);
+ let new_selections = resolve_multiple::<usize, _>(new_anchor_selections, &snapshot)
+ .zip(new_selection_deltas)
+ .map(|(selection, delta)| Selection {
+ id: selection.id,
+ start: selection.start + delta,
+ end: selection.end + delta,
+ reversed: selection.reversed,
+ goal: SelectionGoal::None,
+ })
+ .collect::<Vec<_>>();
+
+ let mut i = 0;
+ for (position, delta, selection_id, pair) in new_autoclose_regions {
+ let position = position.to_offset(&snapshot) + delta;
+ let start = snapshot.anchor_before(position);
+ let end = snapshot.anchor_after(position);
+ while let Some(existing_state) = this.autoclose_regions.get(i) {
+ match existing_state.range.start.cmp(&start, &snapshot) {
+ Ordering::Less => i += 1,
+ Ordering::Greater => break,
+ Ordering::Equal => match end.cmp(&existing_state.range.end, &snapshot) {
+ Ordering::Less => i += 1,
+ Ordering::Equal => break,
+ Ordering::Greater => break,
+ },
+ }
+ }
+ this.autoclose_regions.insert(
+ i,
+ AutocloseRegion {
+ selection_id,
+ range: start..end,
+ pair,
+ },
+ );
+ }
+
+ drop(snapshot);
+ let had_active_copilot_suggestion = this.has_active_copilot_suggestion(cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
+
+ if !brace_inserted && settings::get::<EditorSettings>(cx).use_on_type_format {
+ if let Some(on_type_format_task) =
+ this.trigger_on_type_formatting(text.to_string(), cx)
+ {
+ on_type_format_task.detach_and_log_err(cx);
+ }
+ }
+
+ if had_active_copilot_suggestion {
+ this.refresh_copilot_suggestions(true, cx);
+ if !this.has_active_copilot_suggestion(cx) {
+ this.trigger_completion_on_input(&text, cx);
+ }
+ } else {
+ this.trigger_completion_on_input(&text, cx);
+ this.refresh_copilot_suggestions(true, cx);
+ }
+ });
+ }
+
+ pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
+ let selections = this.selections.all::<usize>(cx);
+ let multi_buffer = this.buffer.read(cx);
+ let buffer = multi_buffer.snapshot(cx);
+ selections
+ .iter()
+ .map(|selection| {
+ let start_point = selection.start.to_point(&buffer);
+ let mut indent = buffer.indent_size_for_line(start_point.row);
+ indent.len = cmp::min(indent.len, start_point.column);
+ let start = selection.start;
+ let end = selection.end;
+ let is_cursor = start == end;
+ let language_scope = buffer.language_scope_at(start);
+ let (comment_delimiter, insert_extra_newline) = if let Some(language) =
+ &language_scope
+ {
+ let leading_whitespace_len = buffer
+ .reversed_chars_at(start)
+ .take_while(|c| c.is_whitespace() && *c != '\n')
+ .map(|c| c.len_utf8())
+ .sum::<usize>();
+
+ let trailing_whitespace_len = buffer
+ .chars_at(end)
+ .take_while(|c| c.is_whitespace() && *c != '\n')
+ .map(|c| c.len_utf8())
+ .sum::<usize>();
+
+ let insert_extra_newline =
+ language.brackets().any(|(pair, enabled)| {
+ let pair_start = pair.start.trim_end();
+ let pair_end = pair.end.trim_start();
+
+ enabled
+ && pair.newline
+ && buffer.contains_str_at(
+ end + trailing_whitespace_len,
+ pair_end,
+ )
+ && buffer.contains_str_at(
+ (start - leading_whitespace_len)
+ .saturating_sub(pair_start.len()),
+ pair_start,
+ )
+ });
+ // Comment extension on newline is allowed only for cursor selections
+ let comment_delimiter = language.line_comment_prefix().filter(|_| {
+ let is_comment_extension_enabled =
+ multi_buffer.settings_at(0, cx).extend_comment_on_newline;
+ is_cursor && is_comment_extension_enabled
+ });
+ let comment_delimiter = if let Some(delimiter) = comment_delimiter {
+ buffer
+ .buffer_line_for_row(start_point.row)
+ .is_some_and(|(snapshot, range)| {
+ let mut index_of_first_non_whitespace = 0;
+ let line_starts_with_comment = snapshot
+ .chars_for_range(range)
+ .skip_while(|c| {
+ let should_skip = c.is_whitespace();
+ if should_skip {
+ index_of_first_non_whitespace += 1;
+ }
+ should_skip
+ })
+ .take(delimiter.len())
+ .eq(delimiter.chars());
+ let cursor_is_placed_after_comment_marker =
+ index_of_first_non_whitespace + delimiter.len()
+ <= start_point.column as usize;
+ line_starts_with_comment
+ && cursor_is_placed_after_comment_marker
+ })
+ .then(|| delimiter.clone())
+ } else {
+ None
+ };
+ (comment_delimiter, insert_extra_newline)
+ } else {
+ (None, false)
+ };
+
+ let capacity_for_delimiter = comment_delimiter
+ .as_deref()
+ .map(str::len)
+ .unwrap_or_default();
+ let mut new_text =
+ String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
+ new_text.push_str("\n");
+ new_text.extend(indent.chars());
+ if let Some(delimiter) = &comment_delimiter {
+ new_text.push_str(&delimiter);
+ }
+ if insert_extra_newline {
+ new_text = new_text.repeat(2);
+ }
+
+ let anchor = buffer.anchor_after(end);
+ let new_selection = selection.map(|_| anchor);
+ (
+ (start..end, new_text),
+ (insert_extra_newline, new_selection),
+ )
+ })
+ .unzip()
+ };
+
+ this.edit_with_autoindent(edits, cx);
+ let buffer = this.buffer.read(cx).snapshot(cx);
+ let new_selections = selection_fixup_info
+ .into_iter()
+ .map(|(extra_newline_inserted, new_selection)| {
+ let mut cursor = new_selection.end.to_point(&buffer);
+ if extra_newline_inserted {
+ cursor.row -= 1;
+ cursor.column = buffer.line_len(cursor.row);
+ }
+ new_selection.map(|_| cursor)
+ })
+ .collect();
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
+ this.refresh_copilot_suggestions(true, cx);
+ });
+ }
+
+ pub fn newline_above(&mut self, _: &NewlineAbove, cx: &mut ViewContext<Self>) {
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
+
+ let mut edits = Vec::new();
+ let mut rows = Vec::new();
+ let mut rows_inserted = 0;
+
+ for selection in self.selections.all_adjusted(cx) {
+ let cursor = selection.head();
+ let row = cursor.row;
+
+ let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left);
+
+ let newline = "\n".to_string();
+ edits.push((start_of_line..start_of_line, newline));
+
+ rows.push(row + rows_inserted);
+ rows_inserted += 1;
+ }
+
+ self.transact(cx, |editor, cx| {
+ editor.edit(edits, cx);
+
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let mut index = 0;
+ s.move_cursors_with(|map, _, _| {
+ let row = rows[index];
+ index += 1;
+
+ let point = Point::new(row, 0);
+ let boundary = map.next_line_boundary(point).1;
+ let clipped = map.clip_point(boundary, Bias::Left);
+
+ (clipped, SelectionGoal::None)
+ });
+ });
+
+ let mut indent_edits = Vec::new();
+ let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+ for row in rows {
+ let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+ for (row, indent) in indents {
+ if indent.len == 0 {
+ continue;
+ }
+
+ let text = match indent.kind {
+ IndentKind::Space => " ".repeat(indent.len as usize),
+ IndentKind::Tab => "\t".repeat(indent.len as usize),
+ };
+ let point = Point::new(row, 0);
+ indent_edits.push((point..point, text));
+ }
+ }
+ editor.edit(indent_edits, cx);
+ });
+ }
+
+ pub fn newline_below(&mut self, _: &NewlineBelow, cx: &mut ViewContext<Self>) {
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
+
+ let mut edits = Vec::new();
+ let mut rows = Vec::new();
+ let mut rows_inserted = 0;
+
+ for selection in self.selections.all_adjusted(cx) {
+ let cursor = selection.head();
+ let row = cursor.row;
+
+ let point = Point::new(row + 1, 0);
+ let start_of_line = snapshot.clip_point(point, Bias::Left);
+
+ let newline = "\n".to_string();
+ edits.push((start_of_line..start_of_line, newline));
+
+ rows_inserted += 1;
+ rows.push(row + rows_inserted);
+ }
+
+ self.transact(cx, |editor, cx| {
+ editor.edit(edits, cx);
+
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let mut index = 0;
+ s.move_cursors_with(|map, _, _| {
+ let row = rows[index];
+ index += 1;
+
+ let point = Point::new(row, 0);
+ let boundary = map.next_line_boundary(point).1;
+ let clipped = map.clip_point(boundary, Bias::Left);
+
+ (clipped, SelectionGoal::None)
+ });
+ });
+
+ let mut indent_edits = Vec::new();
+ let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx);
+ for row in rows {
+ let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx);
+ for (row, indent) in indents {
+ if indent.len == 0 {
+ continue;
+ }
+
+ let text = match indent.kind {
+ IndentKind::Space => " ".repeat(indent.len as usize),
+ IndentKind::Tab => "\t".repeat(indent.len as usize),
+ };
+ let point = Point::new(row, 0);
+ indent_edits.push((point..point, text));
+ }
+ }
+ editor.edit(indent_edits, cx);
+ });
+ }
+
+ pub fn insert(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+ self.insert_with_autoindent_mode(
+ text,
+ Some(AutoindentMode::Block {
+ original_indent_columns: Vec::new(),
+ }),
+ cx,
+ );
+ }
+
+ fn insert_with_autoindent_mode(
+ &mut self,
+ text: &str,
+ autoindent_mode: Option<AutoindentMode>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if self.read_only {
+ return;
+ }
+
+ let text: Arc<str> = text.into();
+ self.transact(cx, |this, cx| {
+ let old_selections = this.selections.all_adjusted(cx);
+ let selection_anchors = this.buffer.update(cx, |buffer, cx| {
+ let anchors = {
+ let snapshot = buffer.read(cx);
+ old_selections
+ .iter()
+ .map(|s| {
+ let anchor = snapshot.anchor_after(s.head());
+ s.map(|_| anchor)
+ })
+ .collect::<Vec<_>>()
+ };
+ buffer.edit(
+ old_selections
+ .iter()
+ .map(|s| (s.start..s.end, text.clone())),
+ autoindent_mode,
+ cx,
+ );
+ anchors
+ });
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_anchors(selection_anchors);
+ })
+ });
+ }
+
+ fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext<Self>) {
+ if !settings::get::<EditorSettings>(cx).show_completions_on_input {
+ return;
+ }
+
+ let selection = self.selections.newest_anchor();
+ if self
+ .buffer
+ .read(cx)
+ .is_completion_trigger(selection.head(), text, cx)
+ {
+ self.show_completions(&ShowCompletions, cx);
+ } else {
+ self.hide_context_menu(cx);
+ }
+ }
+
+ /// If any empty selections is touching the start of its innermost containing autoclose
+ /// region, expand it to select the brackets.
+ fn select_autoclose_pair(&mut self, cx: &mut ViewContext<Self>) {
+ let selections = self.selections.all::<usize>(cx);
+ let buffer = self.buffer.read(cx).read(cx);
+ let mut new_selections = Vec::new();
+ for (mut selection, region) in self.selections_with_autoclose_regions(selections, &buffer) {
+ if let (Some(region), true) = (region, selection.is_empty()) {
+ let mut range = region.range.to_offset(&buffer);
+ if selection.start == range.start {
+ if range.start >= region.pair.start.len() {
+ range.start -= region.pair.start.len();
+ if buffer.contains_str_at(range.start, ®ion.pair.start) {
+ if buffer.contains_str_at(range.end, ®ion.pair.end) {
+ range.end += region.pair.end.len();
+ selection.start = range.start;
+ selection.end = range.end;
+ }
+ }
+ }
+ }
+ }
+ new_selections.push(selection);
+ }
+
+ drop(buffer);
+ self.change_selections(None, cx, |selections| selections.select(new_selections));
+ }
+
+ /// Iterate the given selections, and for each one, find the smallest surrounding
+ /// autoclose region. This uses the ordering of the selections and the autoclose
+ /// regions to avoid repeated comparisons.
+ fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>(
+ &'a self,
+ selections: impl IntoIterator<Item = Selection<D>>,
+ buffer: &'a MultiBufferSnapshot,
+ ) -> impl Iterator<Item = (Selection<D>, Option<&'a AutocloseRegion>)> {
+ let mut i = 0;
+ let mut regions = self.autoclose_regions.as_slice();
+ selections.into_iter().map(move |selection| {
+ let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer);
+
+ let mut enclosing = None;
+ while let Some(pair_state) = regions.get(i) {
+ if pair_state.range.end.to_offset(buffer) < range.start {
+ regions = ®ions[i + 1..];
+ i = 0;
+ } else if pair_state.range.start.to_offset(buffer) > range.end {
+ break;
+ } else {
+ if pair_state.selection_id == selection.id {
+ enclosing = Some(pair_state);
+ }
+ i += 1;
+ }
+ }
+
+ (selection.clone(), enclosing)
+ })
+ }
+
+ /// Remove any autoclose regions that no longer contain their selection.
+ fn invalidate_autoclose_regions(
+ &mut self,
+ mut selections: &[Selection<Anchor>],
+ buffer: &MultiBufferSnapshot,
+ ) {
+ self.autoclose_regions.retain(|state| {
+ let mut i = 0;
+ while let Some(selection) = selections.get(i) {
+ if selection.end.cmp(&state.range.start, buffer).is_lt() {
+ selections = &selections[1..];
+ continue;
+ }
+ if selection.start.cmp(&state.range.end, buffer).is_gt() {
+ break;
+ }
+ if selection.id == state.selection_id {
+ return true;
+ } else {
+ i += 1;
+ }
+ }
+ false
+ });
+ }
+
+ fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
+ let offset = position.to_offset(buffer);
+ let (word_range, kind) = buffer.surrounding_word(offset);
+ if offset > word_range.start && kind == Some(CharKind::Word) {
+ Some(
+ buffer
+ .text_for_range(word_range.start..offset)
+ .collect::<String>(),
+ )
+ } else {
+ None
+ }
+ }
+
+ pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext<Self>) {
+ self.refresh_inlay_hints(
+ InlayHintRefreshReason::Toggle(!self.inlay_hint_cache.enabled),
+ cx,
+ );
+ }
+
+ pub fn inlay_hints_enabled(&self) -> bool {
+ self.inlay_hint_cache.enabled
+ }
+
+ fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut ViewContext<Self>) {
+ if self.project.is_none() || self.mode != EditorMode::Full {
+ return;
+ }
+
+ let reason_description = reason.description();
+ let (invalidate_cache, required_languages) = match reason {
+ InlayHintRefreshReason::Toggle(enabled) => {
+ self.inlay_hint_cache.enabled = enabled;
+ if enabled {
+ (InvalidationStrategy::RefreshRequested, None)
+ } else {
+ self.inlay_hint_cache.clear();
+ self.splice_inlay_hints(
+ self.visible_inlay_hints(cx)
+ .iter()
+ .map(|inlay| inlay.id)
+ .collect(),
+ Vec::new(),
+ cx,
+ );
+ return;
+ }
+ }
+ InlayHintRefreshReason::SettingsChange(new_settings) => {
+ match self.inlay_hint_cache.update_settings(
+ &self.buffer,
+ new_settings,
+ self.visible_inlay_hints(cx),
+ cx,
+ ) {
+ ControlFlow::Break(Some(InlaySplice {
+ to_remove,
+ to_insert,
+ })) => {
+ self.splice_inlay_hints(to_remove, to_insert, cx);
+ return;
+ }
+ ControlFlow::Break(None) => return,
+ ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None),
+ }
+ }
+ InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => {
+ if let Some(InlaySplice {
+ to_remove,
+ to_insert,
+ }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed)
+ {
+ self.splice_inlay_hints(to_remove, to_insert, cx);
+ }
+ return;
+ }
+ InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None),
+ InlayHintRefreshReason::BufferEdited(buffer_languages) => {
+ (InvalidationStrategy::BufferEdited, Some(buffer_languages))
+ }
+ InlayHintRefreshReason::RefreshRequested => {
+ (InvalidationStrategy::RefreshRequested, None)
+ }
+ };
+
+ if let Some(InlaySplice {
+ to_remove,
+ to_insert,
+ }) = self.inlay_hint_cache.spawn_hint_refresh(
+ reason_description,
+ self.excerpt_visible_offsets(required_languages.as_ref(), cx),
+ invalidate_cache,
+ cx,
+ ) {
+ self.splice_inlay_hints(to_remove, to_insert, cx);
+ }
+ }
+
+ fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec<Inlay> {
+ self.display_map
+ .read(cx)
+ .current_inlays()
+ .filter(move |inlay| {
+ Some(inlay.id) != self.copilot_state.suggestion.as_ref().map(|h| h.id)
+ })
+ .cloned()
+ .collect()
+ }
+
+ pub fn excerpt_visible_offsets(
+ &self,
+ restrict_to_languages: Option<&HashSet<Arc<Language>>>,
+ cx: &mut ViewContext<'_, '_, Editor>,
+ ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
+ let multi_buffer = self.buffer().read(cx);
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+ let multi_buffer_visible_start = self
+ .scroll_manager
+ .anchor()
+ .anchor
+ .to_point(&multi_buffer_snapshot);
+ let multi_buffer_visible_end = multi_buffer_snapshot.clip_point(
+ multi_buffer_visible_start
+ + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0),
+ Bias::Left,
+ );
+ let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end;
+ multi_buffer
+ .range_to_buffer_ranges(multi_buffer_visible_range, cx)
+ .into_iter()
+ .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
+ .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| {
+ let buffer = buffer_handle.read(cx);
+ let language = buffer.language()?;
+ if let Some(restrict_to_languages) = restrict_to_languages {
+ if !restrict_to_languages.contains(language) {
+ return None;
+ }
+ }
+ Some((
+ excerpt_id,
+ (
+ buffer_handle,
+ buffer.version().clone(),
+ excerpt_visible_range,
+ ),
+ ))
+ })
+ .collect()
+ }
+
+ pub fn text_layout_details(&self, cx: &WindowContext) -> TextLayoutDetails {
+ TextLayoutDetails {
+ font_cache: cx.font_cache().clone(),
+ text_layout_cache: cx.text_layout_cache().clone(),
+ editor_style: self.style(cx),
+ }
+ }
+
+ fn splice_inlay_hints(
+ &self,
+ to_remove: Vec<InlayId>,
+ to_insert: Vec<Inlay>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.splice_inlays(to_remove, to_insert, cx);
+ });
+ cx.notify();
+ }
+
+ fn trigger_on_type_formatting(
+ &self,
+ input: String,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ if input.len() != 1 {
+ return None;
+ }
+
+ let project = self.project.as_ref()?;
+ let position = self.selections.newest_anchor().head();
+ let (buffer, buffer_position) = self
+ .buffer
+ .read(cx)
+ .text_anchor_for_position(position.clone(), cx)?;
+
+ // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances,
+ // hence we do LSP request & edit on host side only โย add formats to host's history.
+ let push_to_lsp_host_history = true;
+ // If this is not the host, append its history with new edits.
+ let push_to_client_history = project.read(cx).is_remote();
+
+ let on_type_formatting = project.update(cx, |project, cx| {
+ project.on_type_format(
+ buffer.clone(),
+ buffer_position,
+ input,
+ push_to_lsp_host_history,
+ cx,
+ )
+ });
+ Some(cx.spawn(|editor, mut cx| async move {
+ if let Some(transaction) = on_type_formatting.await? {
+ if push_to_client_history {
+ buffer.update(&mut cx, |buffer, _| {
+ buffer.push_transaction(transaction, Instant::now());
+ });
+ }
+ editor.update(&mut cx, |editor, cx| {
+ editor.refresh_document_highlights(cx);
+ })?;
+ }
+ Ok(())
+ }))
+ }
+
+ fn show_completions(&mut self, _: &ShowCompletions, cx: &mut ViewContext<Self>) {
+ if self.pending_rename.is_some() {
+ return;
+ }
+
+ let project = if let Some(project) = self.project.clone() {
+ project
+ } else {
+ return;
+ };
+
+ let position = self.selections.newest_anchor().head();
+ let (buffer, buffer_position) = if let Some(output) = self
+ .buffer
+ .read(cx)
+ .text_anchor_for_position(position.clone(), cx)
+ {
+ output
+ } else {
+ return;
+ };
+
+ let query = Self::completion_query(&self.buffer.read(cx).read(cx), position.clone());
+ let completions = project.update(cx, |project, cx| {
+ project.completions(&buffer, buffer_position, cx)
+ });
+
+ let id = post_inc(&mut self.next_completion_id);
+ let task = cx.spawn(|this, mut cx| {
+ async move {
+ let menu = if let Some(completions) = completions.await.log_err() {
+ let mut menu = CompletionsMenu {
+ id,
+ initial_position: position,
+ match_candidates: completions
+ .iter()
+ .enumerate()
+ .map(|(id, completion)| {
+ StringMatchCandidate::new(
+ id,
+ completion.label.text[completion.label.filter_range.clone()]
+ .into(),
+ )
+ })
+ .collect(),
+ buffer,
+ completions: Arc::new(RwLock::new(completions.into())),
+ matches: Vec::new().into(),
+ selected_item: 0,
+ list: Default::default(),
+ };
+ menu.filter(query.as_deref(), cx.background()).await;
+ if menu.matches.is_empty() {
+ None
+ } else {
+ _ = this.update(&mut cx, |editor, cx| {
+ menu.pre_resolve_completion_documentation(editor.project.clone(), cx);
+ });
+ Some(menu)
+ }
+ } else {
+ None
+ };
+
+ this.update(&mut cx, |this, cx| {
+ this.completion_tasks.retain(|(task_id, _)| *task_id > id);
+
+ let mut context_menu = this.context_menu.write();
+ match context_menu.as_ref() {
+ None => {}
+
+ Some(ContextMenu::Completions(prev_menu)) => {
+ if prev_menu.id > id {
+ return;
+ }
+ }
+
+ _ => return,
+ }
+
+ if this.focused && menu.is_some() {
+ let menu = menu.unwrap();
+ *context_menu = Some(ContextMenu::Completions(menu));
+ drop(context_menu);
+ this.discard_copilot_suggestion(cx);
+ cx.notify();
+ } else if this.completion_tasks.is_empty() {
+ // If there are no more completion tasks and the last menu was
+ // empty, we should hide it. If it was already hidden, we should
+ // also show the copilot suggestion when available.
+ drop(context_menu);
+ if this.hide_context_menu(cx).is_none() {
+ this.update_visible_copilot_suggestion(cx);
+ }
+ }
+ })?;
+
+ Ok::<_, anyhow::Error>(())
+ }
+ .log_err()
+ });
+ self.completion_tasks.push((id, task));
+ }
+
+ pub fn confirm_completion(
+ &mut self,
+ action: &ConfirmCompletion,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<Task<Result<()>>> {
+ use language::ToOffset as _;
+
+ let completions_menu = if let ContextMenu::Completions(menu) = self.hide_context_menu(cx)? {
+ menu
+ } else {
+ return None;
+ };
+
+ let mat = completions_menu
+ .matches
+ .get(action.item_ix.unwrap_or(completions_menu.selected_item))?;
+ let buffer_handle = completions_menu.buffer;
+ let completions = completions_menu.completions.read();
+ let completion = completions.get(mat.candidate_id)?;
+
+ let snippet;
+ let text;
+ if completion.is_snippet() {
+ snippet = Some(Snippet::parse(&completion.new_text).log_err()?);
+ text = snippet.as_ref().unwrap().text.clone();
+ } else {
+ snippet = None;
+ text = completion.new_text.clone();
+ };
+ let selections = self.selections.all::<usize>(cx);
+ let buffer = buffer_handle.read(cx);
+ let old_range = completion.old_range.to_offset(buffer);
+ let old_text = buffer.text_for_range(old_range.clone()).collect::<String>();
+
+ let newest_selection = self.selections.newest_anchor();
+ if newest_selection.start.buffer_id != Some(buffer_handle.read(cx).remote_id()) {
+ return None;
+ }
+
+ let lookbehind = newest_selection
+ .start
+ .text_anchor
+ .to_offset(buffer)
+ .saturating_sub(old_range.start);
+ let lookahead = old_range
+ .end
+ .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer));
+ let mut common_prefix_len = old_text
+ .bytes()
+ .zip(text.bytes())
+ .take_while(|(a, b)| a == b)
+ .count();
+
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let mut range_to_replace: Option<Range<isize>> = None;
+ let mut ranges = Vec::new();
+ for selection in &selections {
+ if snapshot.contains_str_at(selection.start.saturating_sub(lookbehind), &old_text) {
+ let start = selection.start.saturating_sub(lookbehind);
+ let end = selection.end + lookahead;
+ if selection.id == newest_selection.id {
+ range_to_replace = Some(
+ ((start + common_prefix_len) as isize - selection.start as isize)
+ ..(end as isize - selection.start as isize),
+ );
+ }
+ ranges.push(start + common_prefix_len..end);
+ } else {
+ common_prefix_len = 0;
+ ranges.clear();
+ ranges.extend(selections.iter().map(|s| {
+ if s.id == newest_selection.id {
+ range_to_replace = Some(
+ old_range.start.to_offset_utf16(&snapshot).0 as isize
+ - selection.start as isize
+ ..old_range.end.to_offset_utf16(&snapshot).0 as isize
+ - selection.start as isize,
+ );
+ old_range.clone()
+ } else {
+ s.start..s.end
+ }
+ }));
+ break;
+ }
+ }
+ let text = &text[common_prefix_len..];
+
+ cx.emit(Event::InputHandled {
+ utf16_range_to_replace: range_to_replace,
+ text: text.into(),
+ });
+
+ self.transact(cx, |this, cx| {
+ if let Some(mut snippet) = snippet {
+ snippet.text = text.to_string();
+ for tabstop in snippet.tabstops.iter_mut().flatten() {
+ tabstop.start -= common_prefix_len as isize;
+ tabstop.end -= common_prefix_len as isize;
+ }
+
+ this.insert_snippet(&ranges, snippet, cx).log_err();
+ } else {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ ranges.iter().map(|range| (range.clone(), text)),
+ this.autoindent_mode.clone(),
+ cx,
+ );
+ });
+ }
+
+ this.refresh_copilot_suggestions(true, cx);
+ });
+
+ let project = self.project.clone()?;
+ let apply_edits = project.update(cx, |project, cx| {
+ project.apply_additional_edits_for_completion(
+ buffer_handle,
+ completion.clone(),
+ true,
+ cx,
+ )
+ });
+ Some(cx.foreground().spawn(async move {
+ apply_edits.await?;
+ Ok(())
+ }))
+ }
+
+ pub fn toggle_code_actions(&mut self, action: &ToggleCodeActions, cx: &mut ViewContext<Self>) {
+ let mut context_menu = self.context_menu.write();
+ if matches!(context_menu.as_ref(), Some(ContextMenu::CodeActions(_))) {
+ *context_menu = None;
+ cx.notify();
+ return;
+ }
+ drop(context_menu);
+
+ let deployed_from_indicator = action.deployed_from_indicator;
+ let mut task = self.code_actions_task.take();
+ cx.spawn(|this, mut cx| async move {
+ while let Some(prev_task) = task {
+ prev_task.await;
+ task = this.update(&mut cx, |this, _| this.code_actions_task.take())?;
+ }
+
+ this.update(&mut cx, |this, cx| {
+ if this.focused {
+ if let Some((buffer, actions)) = this.available_code_actions.clone() {
+ this.completion_tasks.clear();
+ this.discard_copilot_suggestion(cx);
+ *this.context_menu.write() =
+ Some(ContextMenu::CodeActions(CodeActionsMenu {
+ buffer,
+ actions,
+ selected_item: Default::default(),
+ list: Default::default(),
+ deployed_from_indicator,
+ }));
+ }
+ }
+ })?;
+
+ Ok::<_, anyhow::Error>(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn confirm_code_action(
+ workspace: &mut Workspace,
+ action: &ConfirmCodeAction,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
+ let actions_menu = if let ContextMenu::CodeActions(menu) =
+ editor.update(cx, |editor, cx| editor.hide_context_menu(cx))?
+ {
+ menu
+ } else {
+ return None;
+ };
+ let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
+ let action = actions_menu.actions.get(action_ix)?.clone();
+ let title = action.lsp_action.title.clone();
+ let buffer = actions_menu.buffer;
+
+ let apply_code_actions = workspace.project().clone().update(cx, |project, cx| {
+ project.apply_code_action(buffer, action, true, cx)
+ });
+ let editor = editor.downgrade();
+ Some(cx.spawn(|workspace, cx| async move {
+ let project_transaction = apply_code_actions.await?;
+ Self::open_project_transaction(&editor, workspace, project_transaction, title, cx).await
+ }))
+ }
+
+ async fn open_project_transaction(
+ this: &WeakViewHandle<Editor>,
+ workspace: WeakViewHandle<Workspace>,
+ transaction: ProjectTransaction,
+ title: String,
+ mut cx: AsyncAppContext,
+ ) -> Result<()> {
+ let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx))?;
+
+ let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
+ entries.sort_unstable_by_key(|(buffer, _)| {
+ buffer.read_with(&cx, |buffer, _| buffer.file().map(|f| f.path().clone()))
+ });
+
+ // If the project transaction's edits are all contained within this editor, then
+ // avoid opening a new editor to display them.
+
+ if let Some((buffer, transaction)) = entries.first() {
+ if entries.len() == 1 {
+ let excerpt = this.read_with(&cx, |editor, cx| {
+ editor
+ .buffer()
+ .read(cx)
+ .excerpt_containing(editor.selections.newest_anchor().head(), cx)
+ })?;
+ if let Some((_, excerpted_buffer, excerpt_range)) = excerpt {
+ if excerpted_buffer == *buffer {
+ let all_edits_within_excerpt = buffer.read_with(&cx, |buffer, _| {
+ let excerpt_range = excerpt_range.to_offset(buffer);
+ buffer
+ .edited_ranges_for_transaction::<usize>(transaction)
+ .all(|range| {
+ excerpt_range.start <= range.start
+ && excerpt_range.end >= range.end
+ })
+ });
+
+ if all_edits_within_excerpt {
+ return Ok(());
+ }
+ }
+ }
+ }
+ } else {
+ return Ok(());
+ }
+
+ let mut ranges_to_highlight = Vec::new();
+ let excerpt_buffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(replica_id).with_title(title);
+ for (buffer_handle, transaction) in &entries {
+ let buffer = buffer_handle.read(cx);
+ ranges_to_highlight.extend(
+ multibuffer.push_excerpts_with_context_lines(
+ buffer_handle.clone(),
+ buffer
+ .edited_ranges_for_transaction::<usize>(transaction)
+ .collect(),
+ 1,
+ cx,
+ ),
+ );
+ }
+ multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx);
+ multibuffer
+ });
+
+ workspace.update(&mut cx, |workspace, cx| {
+ let project = workspace.project().clone();
+ let editor =
+ cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx));
+ workspace.add_item(Box::new(editor.clone()), cx);
+ editor.update(cx, |editor, cx| {
+ editor.highlight_background::<Self>(
+ ranges_to_highlight,
+ |theme| theme.editor.highlighted_line_background,
+ cx,
+ );
+ });
+ })?;
+
+ Ok(())
+ }
+
+ fn refresh_code_actions(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+ let project = self.project.clone()?;
+ let buffer = self.buffer.read(cx);
+ let newest_selection = self.selections.newest_anchor().clone();
+ let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?;
+ let (end_buffer, end) = buffer.text_anchor_for_position(newest_selection.end, cx)?;
+ if start_buffer != end_buffer {
+ return None;
+ }
+
+ self.code_actions_task = Some(cx.spawn(|this, mut cx| async move {
+ cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await;
+
+ let actions = project
+ .update(&mut cx, |project, cx| {
+ project.code_actions(&start_buffer, start..end, cx)
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.available_code_actions = actions.log_err().and_then(|actions| {
+ if actions.is_empty() {
+ None
+ } else {
+ Some((start_buffer, actions.into()))
+ }
+ });
+ cx.notify();
+ })
+ .log_err();
+ }));
+ None
+ }
+
+ fn refresh_document_highlights(&mut self, cx: &mut ViewContext<Self>) -> Option<()> {
+ if self.pending_rename.is_some() {
+ return None;
+ }
+
+ let project = self.project.clone()?;
+ let buffer = self.buffer.read(cx);
+ let newest_selection = self.selections.newest_anchor().clone();
+ let cursor_position = newest_selection.head();
+ let (cursor_buffer, cursor_buffer_position) =
+ buffer.text_anchor_for_position(cursor_position.clone(), cx)?;
+ let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?;
+ if cursor_buffer != tail_buffer {
+ return None;
+ }
+
+ self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move {
+ cx.background()
+ .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT)
+ .await;
+
+ let highlights = project
+ .update(&mut cx, |project, cx| {
+ project.document_highlights(&cursor_buffer, cursor_buffer_position, cx)
+ })
+ .await
+ .log_err();
+
+ if let Some(highlights) = highlights {
+ this.update(&mut cx, |this, cx| {
+ if this.pending_rename.is_some() {
+ return;
+ }
+
+ let buffer_id = cursor_position.buffer_id;
+ let buffer = this.buffer.read(cx);
+ if !buffer
+ .text_anchor_for_position(cursor_position, cx)
+ .map_or(false, |(buffer, _)| buffer == cursor_buffer)
+ {
+ return;
+ }
+
+ let cursor_buffer_snapshot = cursor_buffer.read(cx);
+ let mut write_ranges = Vec::new();
+ let mut read_ranges = Vec::new();
+ for highlight in highlights {
+ for (excerpt_id, excerpt_range) in
+ buffer.excerpts_for_buffer(&cursor_buffer, cx)
+ {
+ let start = highlight
+ .range
+ .start
+ .max(&excerpt_range.context.start, cursor_buffer_snapshot);
+ let end = highlight
+ .range
+ .end
+ .min(&excerpt_range.context.end, cursor_buffer_snapshot);
+ if start.cmp(&end, cursor_buffer_snapshot).is_ge() {
+ continue;
+ }
+
+ let range = Anchor {
+ buffer_id,
+ excerpt_id: excerpt_id.clone(),
+ text_anchor: start,
+ }..Anchor {
+ buffer_id,
+ excerpt_id,
+ text_anchor: end,
+ };
+ if highlight.kind == lsp::DocumentHighlightKind::WRITE {
+ write_ranges.push(range);
+ } else {
+ read_ranges.push(range);
+ }
+ }
+ }
+
+ this.highlight_background::<DocumentHighlightRead>(
+ read_ranges,
+ |theme| theme.editor.document_highlight_read_background,
+ cx,
+ );
+ this.highlight_background::<DocumentHighlightWrite>(
+ write_ranges,
+ |theme| theme.editor.document_highlight_write_background,
+ cx,
+ );
+ cx.notify();
+ })
+ .log_err();
+ }
+ }));
+ None
+ }
+
+ fn refresh_copilot_suggestions(
+ &mut self,
+ debounce: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<()> {
+ let copilot = Copilot::global(cx)?;
+ if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+ self.clear_copilot_suggestions(cx);
+ return None;
+ }
+ self.update_visible_copilot_suggestion(cx);
+
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let cursor = self.selections.newest_anchor().head();
+ if !self.is_copilot_enabled_at(cursor, &snapshot, cx) {
+ self.clear_copilot_suggestions(cx);
+ return None;
+ }
+
+ let (buffer, buffer_position) =
+ self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+ self.copilot_state.pending_refresh = cx.spawn(|this, mut cx| async move {
+ if debounce {
+ cx.background().timer(COPILOT_DEBOUNCE_TIMEOUT).await;
+ }
+
+ let completions = copilot
+ .update(&mut cx, |copilot, cx| {
+ copilot.completions(&buffer, buffer_position, cx)
+ })
+ .await
+ .log_err()
+ .into_iter()
+ .flatten()
+ .collect_vec();
+
+ this.update(&mut cx, |this, cx| {
+ if !completions.is_empty() {
+ this.copilot_state.cycled = false;
+ this.copilot_state.pending_cycling_refresh = Task::ready(None);
+ this.copilot_state.completions.clear();
+ this.copilot_state.active_completion_index = 0;
+ this.copilot_state.excerpt_id = Some(cursor.excerpt_id);
+ for completion in completions {
+ this.copilot_state.push_completion(completion);
+ }
+ this.update_visible_copilot_suggestion(cx);
+ }
+ })
+ .log_err()?;
+ Some(())
+ });
+
+ Some(())
+ }
+
+ fn cycle_copilot_suggestions(
+ &mut self,
+ direction: Direction,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<()> {
+ let copilot = Copilot::global(cx)?;
+ if self.mode != EditorMode::Full || !copilot.read(cx).status().is_authorized() {
+ return None;
+ }
+
+ if self.copilot_state.cycled {
+ self.copilot_state.cycle_completions(direction);
+ self.update_visible_copilot_suggestion(cx);
+ } else {
+ let cursor = self.selections.newest_anchor().head();
+ let (buffer, buffer_position) =
+ self.buffer.read(cx).text_anchor_for_position(cursor, cx)?;
+ self.copilot_state.pending_cycling_refresh = cx.spawn(|this, mut cx| async move {
+ let completions = copilot
+ .update(&mut cx, |copilot, cx| {
+ copilot.completions_cycling(&buffer, buffer_position, cx)
+ })
+ .await;
+
+ this.update(&mut cx, |this, cx| {
+ this.copilot_state.cycled = true;
+ for completion in completions.log_err().into_iter().flatten() {
+ this.copilot_state.push_completion(completion);
+ }
+ this.copilot_state.cycle_completions(direction);
+ this.update_visible_copilot_suggestion(cx);
+ })
+ .log_err()?;
+
+ Some(())
+ });
+ }
+
+ Some(())
+ }
+
+ fn copilot_suggest(&mut self, _: &copilot::Suggest, cx: &mut ViewContext<Self>) {
+ if !self.has_active_copilot_suggestion(cx) {
+ self.refresh_copilot_suggestions(false, cx);
+ return;
+ }
+
+ self.update_visible_copilot_suggestion(cx);
+ }
+
+ fn next_copilot_suggestion(&mut self, _: &copilot::NextSuggestion, cx: &mut ViewContext<Self>) {
+ if self.has_active_copilot_suggestion(cx) {
+ self.cycle_copilot_suggestions(Direction::Next, cx);
+ } else {
+ let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none();
+ if is_copilot_disabled {
+ cx.propagate_action();
+ }
+ }
+ }
+
+ fn previous_copilot_suggestion(
+ &mut self,
+ _: &copilot::PreviousSuggestion,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if self.has_active_copilot_suggestion(cx) {
+ self.cycle_copilot_suggestions(Direction::Prev, cx);
+ } else {
+ let is_copilot_disabled = self.refresh_copilot_suggestions(false, cx).is_none();
+ if is_copilot_disabled {
+ cx.propagate_action();
+ }
+ }
+ }
+
+ fn accept_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
+ if let Some((copilot, completion)) =
+ Copilot::global(cx).zip(self.copilot_state.active_completion())
+ {
+ copilot
+ .update(cx, |copilot, cx| copilot.accept_completion(completion, cx))
+ .detach_and_log_err(cx);
+
+ self.report_copilot_event(Some(completion.uuid.clone()), true, cx)
+ }
+ cx.emit(Event::InputHandled {
+ utf16_range_to_replace: None,
+ text: suggestion.text.to_string().into(),
+ });
+ self.insert_with_autoindent_mode(&suggestion.text.to_string(), None, cx);
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ }
+
+ fn discard_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(suggestion) = self.take_active_copilot_suggestion(cx) {
+ if let Some(copilot) = Copilot::global(cx) {
+ copilot
+ .update(cx, |copilot, cx| {
+ copilot.discard_completions(&self.copilot_state.completions, cx)
+ })
+ .detach_and_log_err(cx);
+
+ self.report_copilot_event(None, false, cx)
+ }
+
+ self.display_map.update(cx, |map, cx| {
+ map.splice_inlays(vec![suggestion.id], Vec::new(), cx)
+ });
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ }
+
+ fn is_copilot_enabled_at(
+ &self,
+ location: Anchor,
+ snapshot: &MultiBufferSnapshot,
+ cx: &mut ViewContext<Self>,
+ ) -> bool {
+ let file = snapshot.file_at(location);
+ let language = snapshot.language_at(location);
+ let settings = all_language_settings(file, cx);
+ settings.copilot_enabled(language, file.map(|f| f.path().as_ref()))
+ }
+
+ fn has_active_copilot_suggestion(&self, cx: &AppContext) -> bool {
+ if let Some(suggestion) = self.copilot_state.suggestion.as_ref() {
+ let buffer = self.buffer.read(cx).read(cx);
+ suggestion.position.is_valid(&buffer)
+ } else {
+ false
+ }
+ }
+
+ fn take_active_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) -> Option<Inlay> {
+ let suggestion = self.copilot_state.suggestion.take()?;
+ self.display_map.update(cx, |map, cx| {
+ map.splice_inlays(vec![suggestion.id], Default::default(), cx);
+ });
+ let buffer = self.buffer.read(cx).read(cx);
+
+ if suggestion.position.is_valid(&buffer) {
+ Some(suggestion)
+ } else {
+ None
+ }
+ }
+
+ fn update_visible_copilot_suggestion(&mut self, cx: &mut ViewContext<Self>) {
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let selection = self.selections.newest_anchor();
+ let cursor = selection.head();
+
+ if self.context_menu.read().is_some()
+ || !self.completion_tasks.is_empty()
+ || selection.start != selection.end
+ {
+ self.discard_copilot_suggestion(cx);
+ } else if let Some(text) = self
+ .copilot_state
+ .text_for_active_completion(cursor, &snapshot)
+ {
+ let text = Rope::from(text);
+ let mut to_remove = Vec::new();
+ if let Some(suggestion) = self.copilot_state.suggestion.take() {
+ to_remove.push(suggestion.id);
+ }
+
+ let suggestion_inlay =
+ Inlay::suggestion(post_inc(&mut self.next_inlay_id), cursor, text);
+ self.copilot_state.suggestion = Some(suggestion_inlay.clone());
+ self.display_map.update(cx, move |map, cx| {
+ map.splice_inlays(to_remove, vec![suggestion_inlay], cx)
+ });
+ cx.notify();
+ } else {
+ self.discard_copilot_suggestion(cx);
+ }
+ }
+
+ fn clear_copilot_suggestions(&mut self, cx: &mut ViewContext<Self>) {
+ self.copilot_state = Default::default();
+ self.discard_copilot_suggestion(cx);
+ }
+
+ pub fn render_code_actions_indicator(
+ &self,
+ style: &EditorStyle,
+ is_active: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<AnyElement<Self>> {
+ if self.available_code_actions.is_some() {
+ enum CodeActions {}
+ Some(
+ MouseEventHandler::new::<CodeActions, _>(0, cx, |state, _| {
+ Svg::new("icons/bolt.svg").with_color(
+ style
+ .code_actions
+ .indicator
+ .in_state(is_active)
+ .style_for(state)
+ .color,
+ )
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .with_padding(Padding::uniform(3.))
+ .on_down(MouseButton::Left, |_, this, cx| {
+ this.toggle_code_actions(
+ &ToggleCodeActions {
+ deployed_from_indicator: true,
+ },
+ cx,
+ );
+ })
+ .into_any(),
+ )
+ } else {
+ None
+ }
+ }
+
+ pub fn render_fold_indicators(
+ &self,
+ fold_data: Vec<Option<(FoldStatus, u32, bool)>>,
+ style: &EditorStyle,
+ gutter_hovered: bool,
+ line_height: f32,
+ gutter_margin: f32,
+ cx: &mut ViewContext<Self>,
+ ) -> Vec<Option<AnyElement<Self>>> {
+ enum FoldIndicators {}
+
+ let style = style.folds.clone();
+
+ fold_data
+ .iter()
+ .enumerate()
+ .map(|(ix, fold_data)| {
+ fold_data
+ .map(|(fold_status, buffer_row, active)| {
+ (active || gutter_hovered || fold_status == FoldStatus::Folded).then(|| {
+ MouseEventHandler::new::<FoldIndicators, _>(
+ ix as usize,
+ cx,
+ |mouse_state, _| {
+ Svg::new(match fold_status {
+ FoldStatus::Folded => style.folded_icon.clone(),
+ FoldStatus::Foldable => style.foldable_icon.clone(),
+ })
+ .with_color(
+ style
+ .indicator
+ .in_state(fold_status == FoldStatus::Folded)
+ .style_for(mouse_state)
+ .color,
+ )
+ .constrained()
+ .with_width(gutter_margin * style.icon_margin_scale)
+ .aligned()
+ .constrained()
+ .with_height(line_height)
+ .with_width(gutter_margin)
+ .aligned()
+ },
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .with_padding(Padding::uniform(3.))
+ .on_click(MouseButton::Left, {
+ move |_, editor, cx| match fold_status {
+ FoldStatus::Folded => {
+ editor.unfold_at(&UnfoldAt { buffer_row }, cx);
+ }
+ FoldStatus::Foldable => {
+ editor.fold_at(&FoldAt { buffer_row }, cx);
+ }
+ }
+ })
+ .into_any()
+ })
+ })
+ .flatten()
+ })
+ .collect()
+ }
+
+ pub fn context_menu_visible(&self) -> bool {
+ self.context_menu
+ .read()
+ .as_ref()
+ .map_or(false, |menu| menu.visible())
+ }
+
+ pub fn render_context_menu(
+ &self,
+ cursor_position: DisplayPoint,
+ style: EditorStyle,
+ cx: &mut ViewContext<Editor>,
+ ) -> Option<(DisplayPoint, AnyElement<Editor>)> {
+ self.context_menu.read().as_ref().map(|menu| {
+ menu.render(
+ cursor_position,
+ style,
+ self.workspace.as_ref().map(|(w, _)| w.clone()),
+ cx,
+ )
+ })
+ }
+
+ fn hide_context_menu(&mut self, cx: &mut ViewContext<Self>) -> Option<ContextMenu> {
+ cx.notify();
+ self.completion_tasks.clear();
+ let context_menu = self.context_menu.write().take();
+ if context_menu.is_some() {
+ self.update_visible_copilot_suggestion(cx);
+ }
+ context_menu
+ }
+
+ pub fn insert_snippet(
+ &mut self,
+ insertion_ranges: &[Range<usize>],
+ snippet: Snippet,
+ cx: &mut ViewContext<Self>,
+ ) -> Result<()> {
+ let tabstops = self.buffer.update(cx, |buffer, cx| {
+ let snippet_text: Arc<str> = snippet.text.clone().into();
+ buffer.edit(
+ insertion_ranges
+ .iter()
+ .cloned()
+ .map(|range| (range, snippet_text.clone())),
+ Some(AutoindentMode::EachLine),
+ cx,
+ );
+
+ let snapshot = &*buffer.read(cx);
+ let snippet = &snippet;
+ snippet
+ .tabstops
+ .iter()
+ .map(|tabstop| {
+ let mut tabstop_ranges = tabstop
+ .iter()
+ .flat_map(|tabstop_range| {
+ let mut delta = 0_isize;
+ insertion_ranges.iter().map(move |insertion_range| {
+ let insertion_start = insertion_range.start as isize + delta;
+ delta +=
+ snippet.text.len() as isize - insertion_range.len() as isize;
+
+ let start = snapshot.anchor_before(
+ (insertion_start + tabstop_range.start) as usize,
+ );
+ let end = snapshot
+ .anchor_after((insertion_start + tabstop_range.end) as usize);
+ start..end
+ })
+ })
+ .collect::<Vec<_>>();
+ tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot));
+ tabstop_ranges
+ })
+ .collect::<Vec<_>>()
+ });
+
+ if let Some(tabstop) = tabstops.first() {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(tabstop.iter().cloned());
+ });
+ self.snippet_stack.push(SnippetState {
+ active_index: 0,
+ ranges: tabstops,
+ });
+ }
+
+ Ok(())
+ }
+
+ pub fn move_to_next_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ self.move_to_snippet_tabstop(Bias::Right, cx)
+ }
+
+ pub fn move_to_prev_snippet_tabstop(&mut self, cx: &mut ViewContext<Self>) -> bool {
+ self.move_to_snippet_tabstop(Bias::Left, cx)
+ }
+
+ pub fn move_to_snippet_tabstop(&mut self, bias: Bias, cx: &mut ViewContext<Self>) -> bool {
+ if let Some(mut snippet) = self.snippet_stack.pop() {
+ match bias {
+ Bias::Left => {
+ if snippet.active_index > 0 {
+ snippet.active_index -= 1;
+ } else {
+ self.snippet_stack.push(snippet);
+ return false;
+ }
+ }
+ Bias::Right => {
+ if snippet.active_index + 1 < snippet.ranges.len() {
+ snippet.active_index += 1;
+ } else {
+ self.snippet_stack.push(snippet);
+ return false;
+ }
+ }
+ }
+ if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_anchor_ranges(current_ranges.iter().cloned())
+ });
+ // If snippet state is not at the last tabstop, push it back on the stack
+ if snippet.active_index + 1 < snippet.ranges.len() {
+ self.snippet_stack.push(snippet);
+ }
+ return true;
+ }
+ }
+
+ false
+ }
+
+ pub fn clear(&mut self, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ this.select_all(&SelectAll, cx);
+ this.insert("", cx);
+ });
+ }
+
+ pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ this.select_autoclose_pair(cx);
+ let mut selections = this.selections.all::<Point>(cx);
+ if !this.selections.line_mode {
+ let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx));
+ for selection in &mut selections {
+ if selection.is_empty() {
+ let old_head = selection.head();
+ let mut new_head =
+ movement::left(&display_map, old_head.to_display_point(&display_map))
+ .to_point(&display_map);
+ if let Some((buffer, line_buffer_range)) = display_map
+ .buffer_snapshot
+ .buffer_line_for_row(old_head.row)
+ {
+ let indent_size =
+ buffer.indent_size_for_line(line_buffer_range.start.row);
+ let indent_len = match indent_size.kind {
+ IndentKind::Space => {
+ buffer.settings_at(line_buffer_range.start, cx).tab_size
+ }
+ IndentKind::Tab => NonZeroU32::new(1).unwrap(),
+ };
+ if old_head.column <= indent_size.len && old_head.column > 0 {
+ let indent_len = indent_len.get();
+ new_head = cmp::min(
+ new_head,
+ Point::new(
+ old_head.row,
+ ((old_head.column - 1) / indent_len) * indent_len,
+ ),
+ );
+ }
+ }
+
+ selection.set_head(new_head, SelectionGoal::None);
+ }
+ }
+ }
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+ this.insert("", cx);
+ this.refresh_copilot_suggestions(true, cx);
+ });
+ }
+
+ pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if selection.is_empty() && !line_mode {
+ let cursor = movement::right(map, selection.head());
+ selection.end = cursor;
+ selection.reversed = true;
+ selection.goal = SelectionGoal::None;
+ }
+ })
+ });
+ this.insert("", cx);
+ this.refresh_copilot_suggestions(true, cx);
+ });
+ }
+
+ pub fn tab_prev(&mut self, _: &TabPrev, cx: &mut ViewContext<Self>) {
+ if self.move_to_prev_snippet_tabstop(cx) {
+ return;
+ }
+
+ self.outdent(&Outdent, cx);
+ }
+
+ pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
+ if self.move_to_next_snippet_tabstop(cx) {
+ return;
+ }
+
+ let mut selections = self.selections.all_adjusted(cx);
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
+ let rows_iter = selections.iter().map(|s| s.head().row);
+ let suggested_indents = snapshot.suggested_indents(rows_iter, cx);
+
+ let mut edits = Vec::new();
+ let mut prev_edited_row = 0;
+ let mut row_delta = 0;
+ for selection in &mut selections {
+ if selection.start.row != prev_edited_row {
+ row_delta = 0;
+ }
+ prev_edited_row = selection.end.row;
+
+ // If the selection is non-empty, then increase the indentation of the selected lines.
+ if !selection.is_empty() {
+ row_delta =
+ Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx);
+ continue;
+ }
+
+ // If the selection is empty and the cursor is in the leading whitespace before the
+ // suggested indentation, then auto-indent the line.
+ let cursor = selection.head();
+ let current_indent = snapshot.indent_size_for_line(cursor.row);
+ if let Some(suggested_indent) = suggested_indents.get(&cursor.row).copied() {
+ if cursor.column < suggested_indent.len
+ && cursor.column <= current_indent.len
+ && current_indent.len <= suggested_indent.len
+ {
+ selection.start = Point::new(cursor.row, suggested_indent.len);
+ selection.end = selection.start;
+ if row_delta == 0 {
+ edits.extend(Buffer::edit_for_indent_size_adjustment(
+ cursor.row,
+ current_indent,
+ suggested_indent,
+ ));
+ row_delta = suggested_indent.len - current_indent.len;
+ }
+ continue;
+ }
+ }
+
+ // Accept copilot suggestion if there is only one selection and the cursor is not
+ // in the leading whitespace.
+ if self.selections.count() == 1
+ && cursor.column >= current_indent.len
+ && self.has_active_copilot_suggestion(cx)
+ {
+ self.accept_copilot_suggestion(cx);
+ return;
+ }
+
+ // Otherwise, insert a hard or soft tab.
+ let settings = buffer.settings_at(cursor, cx);
+ let tab_size = if settings.hard_tabs {
+ IndentSize::tab()
+ } else {
+ let tab_size = settings.tab_size.get();
+ let char_column = snapshot
+ .text_for_range(Point::new(cursor.row, 0)..cursor)
+ .flat_map(str::chars)
+ .count()
+ + row_delta as usize;
+ let chars_to_next_tab_stop = tab_size - (char_column as u32 % tab_size);
+ IndentSize::spaces(chars_to_next_tab_stop)
+ };
+ selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len);
+ selection.end = selection.start;
+ edits.push((cursor..cursor, tab_size.chars().collect::<String>()));
+ row_delta += tab_size.len;
+ }
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+ this.refresh_copilot_suggestions(true, cx);
+ });
+ }
+
+ pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext<Self>) {
+ let mut selections = self.selections.all::<Point>(cx);
+ let mut prev_edited_row = 0;
+ let mut row_delta = 0;
+ let mut edits = Vec::new();
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
+ for selection in &mut selections {
+ if selection.start.row != prev_edited_row {
+ row_delta = 0;
+ }
+ prev_edited_row = selection.end.row;
+
+ row_delta =
+ Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx);
+ }
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+ });
+ }
+
+ fn indent_selection(
+ buffer: &MultiBuffer,
+ snapshot: &MultiBufferSnapshot,
+ selection: &mut Selection<Point>,
+ edits: &mut Vec<(Range<Point>, String)>,
+ delta_for_start_row: u32,
+ cx: &AppContext,
+ ) -> u32 {
+ let settings = buffer.settings_at(selection.start, cx);
+ let tab_size = settings.tab_size.get();
+ let indent_kind = if settings.hard_tabs {
+ IndentKind::Tab
+ } else {
+ IndentKind::Space
+ };
+ let mut start_row = selection.start.row;
+ let mut end_row = selection.end.row + 1;
+
+ // If a selection ends at the beginning of a line, don't indent
+ // that last line.
+ if selection.end.column == 0 {
+ end_row -= 1;
+ }
+
+ // Avoid re-indenting a row that has already been indented by a
+ // previous selection, but still update this selection's column
+ // to reflect that indentation.
+ if delta_for_start_row > 0 {
+ start_row += 1;
+ selection.start.column += delta_for_start_row;
+ if selection.end.row == selection.start.row {
+ selection.end.column += delta_for_start_row;
+ }
+ }
+
+ let mut delta_for_end_row = 0;
+ for row in start_row..end_row {
+ let current_indent = snapshot.indent_size_for_line(row);
+ let indent_delta = match (current_indent.kind, indent_kind) {
+ (IndentKind::Space, IndentKind::Space) => {
+ let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size);
+ IndentSize::spaces(columns_to_next_tab_stop)
+ }
+ (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size),
+ (_, IndentKind::Tab) => IndentSize::tab(),
+ };
+
+ let row_start = Point::new(row, 0);
+ edits.push((
+ row_start..row_start,
+ indent_delta.chars().collect::<String>(),
+ ));
+
+ // Update this selection's endpoints to reflect the indentation.
+ if row == selection.start.row {
+ selection.start.column += indent_delta.len;
+ }
+ if row == selection.end.row {
+ selection.end.column += indent_delta.len;
+ delta_for_end_row = indent_delta.len;
+ }
+ }
+
+ if selection.start.row == selection.end.row {
+ delta_for_start_row + delta_for_end_row
+ } else {
+ delta_for_end_row
+ }
+ }
+
+ pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all::<Point>(cx);
+ let mut deletion_ranges = Vec::new();
+ let mut last_outdent = None;
+ {
+ let buffer = self.buffer.read(cx);
+ let snapshot = buffer.snapshot(cx);
+ for selection in &selections {
+ let settings = buffer.settings_at(selection.start, cx);
+ let tab_size = settings.tab_size.get();
+ let mut rows = selection.spanned_rows(false, &display_map);
+
+ // Avoid re-outdenting a row that has already been outdented by a
+ // previous selection.
+ if let Some(last_row) = last_outdent {
+ if last_row == rows.start {
+ rows.start += 1;
+ }
+ }
+
+ for row in rows {
+ let indent_size = snapshot.indent_size_for_line(row);
+ if indent_size.len > 0 {
+ let deletion_len = match indent_size.kind {
+ IndentKind::Space => {
+ let columns_to_prev_tab_stop = indent_size.len % tab_size;
+ if columns_to_prev_tab_stop == 0 {
+ tab_size
+ } else {
+ columns_to_prev_tab_stop
+ }
+ }
+ IndentKind::Tab => 1,
+ };
+ deletion_ranges.push(Point::new(row, 0)..Point::new(row, deletion_len));
+ last_outdent = Some(row);
+ }
+ }
+ }
+ }
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ let empty_str: Arc<str> = "".into();
+ buffer.edit(
+ deletion_ranges
+ .into_iter()
+ .map(|range| (range, empty_str.clone())),
+ None,
+ cx,
+ );
+ });
+ let selections = this.selections.all::<usize>(cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+ });
+ }
+
+ pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all::<Point>(cx);
+
+ let mut new_cursors = Vec::new();
+ let mut edit_ranges = Vec::new();
+ let mut selections = selections.iter().peekable();
+ while let Some(selection) = selections.next() {
+ let mut rows = selection.spanned_rows(false, &display_map);
+ let goal_display_column = selection.head().to_display_point(&display_map).column();
+
+ // Accumulate contiguous regions of rows that we want to delete.
+ while let Some(next_selection) = selections.peek() {
+ let next_rows = next_selection.spanned_rows(false, &display_map);
+ if next_rows.start <= rows.end {
+ rows.end = next_rows.end;
+ selections.next().unwrap();
+ } else {
+ break;
+ }
+ }
+
+ let buffer = &display_map.buffer_snapshot;
+ let mut edit_start = Point::new(rows.start, 0).to_offset(buffer);
+ let edit_end;
+ let cursor_buffer_row;
+ if buffer.max_point().row >= rows.end {
+ // If there's a line after the range, delete the \n from the end of the row range
+ // and position the cursor on the next line.
+ edit_end = Point::new(rows.end, 0).to_offset(buffer);
+ cursor_buffer_row = rows.end;
+ } else {
+ // If there isn't a line after the range, delete the \n from the line before the
+ // start of the row range and position the cursor there.
+ edit_start = edit_start.saturating_sub(1);
+ edit_end = buffer.len();
+ cursor_buffer_row = rows.start.saturating_sub(1);
+ }
+
+ let mut cursor = Point::new(cursor_buffer_row, 0).to_display_point(&display_map);
+ *cursor.column_mut() =
+ cmp::min(goal_display_column, display_map.line_len(cursor.row()));
+
+ new_cursors.push((
+ selection.id,
+ buffer.anchor_after(cursor.to_point(&display_map)),
+ ));
+ edit_ranges.push(edit_start..edit_end);
+ }
+
+ self.transact(cx, |this, cx| {
+ let buffer = this.buffer.update(cx, |buffer, cx| {
+ let empty_str: Arc<str> = "".into();
+ buffer.edit(
+ edit_ranges
+ .into_iter()
+ .map(|range| (range, empty_str.clone())),
+ None,
+ cx,
+ );
+ buffer.snapshot(cx)
+ });
+ let new_selections = new_cursors
+ .into_iter()
+ .map(|(id, cursor)| {
+ let cursor = cursor.to_point(&buffer);
+ Selection {
+ id,
+ start: cursor,
+ end: cursor,
+ reversed: false,
+ goal: SelectionGoal::None,
+ }
+ })
+ .collect();
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(new_selections);
+ });
+ });
+ }
+
+ pub fn join_lines(&mut self, _: &JoinLines, cx: &mut ViewContext<Self>) {
+ let mut row_ranges = Vec::<Range<u32>>::new();
+ for selection in self.selections.all::<Point>(cx) {
+ let start = selection.start.row;
+ let end = if selection.start.row == selection.end.row {
+ selection.start.row + 1
+ } else {
+ selection.end.row
+ };
+
+ if let Some(last_row_range) = row_ranges.last_mut() {
+ if start <= last_row_range.end {
+ last_row_range.end = end;
+ continue;
+ }
+ }
+ row_ranges.push(start..end);
+ }
+
+ let snapshot = self.buffer.read(cx).snapshot(cx);
+ let mut cursor_positions = Vec::new();
+ for row_range in &row_ranges {
+ let anchor = snapshot.anchor_before(Point::new(
+ row_range.end - 1,
+ snapshot.line_len(row_range.end - 1),
+ ));
+ cursor_positions.push(anchor.clone()..anchor);
+ }
+
+ self.transact(cx, |this, cx| {
+ for row_range in row_ranges.into_iter().rev() {
+ for row in row_range.rev() {
+ let end_of_line = Point::new(row, snapshot.line_len(row));
+ let indent = snapshot.indent_size_for_line(row + 1);
+ let start_of_next_line = Point::new(row + 1, indent.len);
+
+ let replace = if snapshot.line_len(row + 1) > indent.len {
+ " "
+ } else {
+ ""
+ };
+
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx)
+ });
+ }
+ }
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_anchor_ranges(cursor_positions)
+ });
+ });
+ }
+
+ pub fn sort_lines_case_sensitive(
+ &mut self,
+ _: &SortLinesCaseSensitive,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_lines(cx, |lines| lines.sort())
+ }
+
+ pub fn sort_lines_case_insensitive(
+ &mut self,
+ _: &SortLinesCaseInsensitive,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase()))
+ }
+
+ pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext<Self>) {
+ self.manipulate_lines(cx, |lines| lines.reverse())
+ }
+
+ pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext<Self>) {
+ self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng()))
+ }
+
+ fn manipulate_lines<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+ where
+ Fn: FnMut(&mut [&str]),
+ {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = self.buffer.read(cx).snapshot(cx);
+
+ let mut edits = Vec::new();
+
+ let selections = self.selections.all::<Point>(cx);
+ let mut selections = selections.iter().peekable();
+ let mut contiguous_row_selections = Vec::new();
+ let mut new_selections = Vec::new();
+
+ while let Some(selection) = selections.next() {
+ let (start_row, end_row) = consume_contiguous_rows(
+ &mut contiguous_row_selections,
+ selection,
+ &display_map,
+ &mut selections,
+ );
+
+ let start_point = Point::new(start_row, 0);
+ let end_point = Point::new(end_row - 1, buffer.line_len(end_row - 1));
+ let text = buffer
+ .text_for_range(start_point..end_point)
+ .collect::<String>();
+ let mut lines = text.split("\n").collect_vec();
+
+ let lines_len = lines.len();
+ callback(&mut lines);
+
+ // This is a current limitation with selections.
+ // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections.
+ debug_assert!(
+ lines.len() == lines_len,
+ "callback should not change the number of lines"
+ );
+
+ edits.push((start_point..end_point, lines.join("\n")));
+ let start_anchor = buffer.anchor_after(start_point);
+ let end_anchor = buffer.anchor_before(end_point);
+
+ // Make selection and push
+ new_selections.push(Selection {
+ id: selection.id,
+ start: start_anchor.to_offset(&buffer),
+ end: end_anchor.to_offset(&buffer),
+ goal: SelectionGoal::None,
+ reversed: selection.reversed,
+ });
+ }
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(new_selections);
+ });
+
+ this.request_autoscroll(Autoscroll::fit(), cx);
+ });
+ }
+
+ pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_uppercase())
+ }
+
+ pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_lowercase())
+ }
+
+ pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| {
+ // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+ // https://github.com/rutrum/convert-case/issues/16
+ text.split("\n")
+ .map(|line| line.to_case(Case::Title))
+ .join("\n")
+ })
+ }
+
+ pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_case(Case::Snake))
+ }
+
+ pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext<Self>) {
+ self.manipulate_text(cx, |text| text.to_case(Case::Kebab))
+ }
+
+ pub fn convert_to_upper_camel_case(
+ &mut self,
+ _: &ConvertToUpperCamelCase,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_text(cx, |text| {
+ // Hack to get around the fact that to_case crate doesn't support '\n' as a word boundary
+ // https://github.com/rutrum/convert-case/issues/16
+ text.split("\n")
+ .map(|line| line.to_case(Case::UpperCamel))
+ .join("\n")
+ })
+ }
+
+ pub fn convert_to_lower_camel_case(
+ &mut self,
+ _: &ConvertToLowerCamelCase,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.manipulate_text(cx, |text| text.to_case(Case::Camel))
+ }
+
+ fn manipulate_text<Fn>(&mut self, cx: &mut ViewContext<Self>, mut callback: Fn)
+ where
+ Fn: FnMut(&str) -> String,
+ {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = self.buffer.read(cx).snapshot(cx);
+
+ let mut new_selections = Vec::new();
+ let mut edits = Vec::new();
+ let mut selection_adjustment = 0i32;
+
+ for selection in self.selections.all::<usize>(cx) {
+ let selection_is_empty = selection.is_empty();
+
+ let (start, end) = if selection_is_empty {
+ let word_range = movement::surrounding_word(
+ &display_map,
+ selection.start.to_display_point(&display_map),
+ );
+ let start = word_range.start.to_offset(&display_map, Bias::Left);
+ let end = word_range.end.to_offset(&display_map, Bias::Left);
+ (start, end)
+ } else {
+ (selection.start, selection.end)
+ };
+
+ let text = buffer.text_for_range(start..end).collect::<String>();
+ let old_length = text.len() as i32;
+ let text = callback(&text);
+
+ new_selections.push(Selection {
+ start: (start as i32 - selection_adjustment) as usize,
+ end: ((start + text.len()) as i32 - selection_adjustment) as usize,
+ goal: SelectionGoal::None,
+ ..selection
+ });
+
+ selection_adjustment += old_length - text.len() as i32;
+
+ edits.push((start..end, text));
+ }
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(new_selections);
+ });
+
+ this.request_autoscroll(Autoscroll::fit(), cx);
+ });
+ }
+
+ pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = &display_map.buffer_snapshot;
+ let selections = self.selections.all::<Point>(cx);
+
+ let mut edits = Vec::new();
+ let mut selections_iter = selections.iter().peekable();
+ while let Some(selection) = selections_iter.next() {
+ // Avoid duplicating the same lines twice.
+ let mut rows = selection.spanned_rows(false, &display_map);
+
+ while let Some(next_selection) = selections_iter.peek() {
+ let next_rows = next_selection.spanned_rows(false, &display_map);
+ if next_rows.start < rows.end {
+ rows.end = next_rows.end;
+ selections_iter.next().unwrap();
+ } else {
+ break;
+ }
+ }
+
+ // Copy the text from the selected row region and splice it at the start of the region.
+ let start = Point::new(rows.start, 0);
+ let end = Point::new(rows.end - 1, buffer.line_len(rows.end - 1));
+ let text = buffer
+ .text_for_range(start..end)
+ .chain(Some("\n"))
+ .collect::<String>();
+ edits.push((start..start, text));
+ }
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+
+ this.request_autoscroll(Autoscroll::fit(), cx);
+ });
+ }
+
+ pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = self.buffer.read(cx).snapshot(cx);
+
+ let mut edits = Vec::new();
+ let mut unfold_ranges = Vec::new();
+ let mut refold_ranges = Vec::new();
+
+ let selections = self.selections.all::<Point>(cx);
+ let mut selections = selections.iter().peekable();
+ let mut contiguous_row_selections = Vec::new();
+ let mut new_selections = Vec::new();
+
+ while let Some(selection) = selections.next() {
+ // Find all the selections that span a contiguous row range
+ let (start_row, end_row) = consume_contiguous_rows(
+ &mut contiguous_row_selections,
+ selection,
+ &display_map,
+ &mut selections,
+ );
+
+ // Move the text spanned by the row range to be before the line preceding the row range
+ if start_row > 0 {
+ let range_to_move = Point::new(start_row - 1, buffer.line_len(start_row - 1))
+ ..Point::new(end_row - 1, buffer.line_len(end_row - 1));
+ let insertion_point = display_map
+ .prev_line_boundary(Point::new(start_row - 1, 0))
+ .0;
+
+ // Don't move lines across excerpts
+ if buffer
+ .excerpt_boundaries_in_range((
+ Bound::Excluded(insertion_point),
+ Bound::Included(range_to_move.end),
+ ))
+ .next()
+ .is_none()
+ {
+ let text = buffer
+ .text_for_range(range_to_move.clone())
+ .flat_map(|s| s.chars())
+ .skip(1)
+ .chain(['\n'])
+ .collect::<String>();
+
+ edits.push((
+ buffer.anchor_after(range_to_move.start)
+ ..buffer.anchor_before(range_to_move.end),
+ String::new(),
+ ));
+ let insertion_anchor = buffer.anchor_after(insertion_point);
+ edits.push((insertion_anchor..insertion_anchor, text));
+
+ let row_delta = range_to_move.start.row - insertion_point.row + 1;
+
+ // Move selections up
+ new_selections.extend(contiguous_row_selections.drain(..).map(
+ |mut selection| {
+ selection.start.row -= row_delta;
+ selection.end.row -= row_delta;
+ selection
+ },
+ ));
+
+ // Move folds up
+ unfold_ranges.push(range_to_move.clone());
+ for fold in display_map.folds_in_range(
+ buffer.anchor_before(range_to_move.start)
+ ..buffer.anchor_after(range_to_move.end),
+ ) {
+ let mut start = fold.start.to_point(&buffer);
+ let mut end = fold.end.to_point(&buffer);
+ start.row -= row_delta;
+ end.row -= row_delta;
+ refold_ranges.push(start..end);
+ }
+ }
+ }
+
+ // If we didn't move line(s), preserve the existing selections
+ new_selections.append(&mut contiguous_row_selections);
+ }
+
+ self.transact(cx, |this, cx| {
+ this.unfold_ranges(unfold_ranges, true, true, cx);
+ this.buffer.update(cx, |buffer, cx| {
+ for (range, text) in edits {
+ buffer.edit([(range, text)], None, cx);
+ }
+ });
+ this.fold_ranges(refold_ranges, true, cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(new_selections);
+ })
+ });
+ }
+
+ pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = self.buffer.read(cx).snapshot(cx);
+
+ let mut edits = Vec::new();
+ let mut unfold_ranges = Vec::new();
+ let mut refold_ranges = Vec::new();
+
+ let selections = self.selections.all::<Point>(cx);
+ let mut selections = selections.iter().peekable();
+ let mut contiguous_row_selections = Vec::new();
+ let mut new_selections = Vec::new();
+
+ while let Some(selection) = selections.next() {
+ // Find all the selections that span a contiguous row range
+ let (start_row, end_row) = consume_contiguous_rows(
+ &mut contiguous_row_selections,
+ selection,
+ &display_map,
+ &mut selections,
+ );
+
+ // Move the text spanned by the row range to be after the last line of the row range
+ if end_row <= buffer.max_point().row {
+ let range_to_move = Point::new(start_row, 0)..Point::new(end_row, 0);
+ let insertion_point = display_map.next_line_boundary(Point::new(end_row, 0)).0;
+
+ // Don't move lines across excerpt boundaries
+ if buffer
+ .excerpt_boundaries_in_range((
+ Bound::Excluded(range_to_move.start),
+ Bound::Included(insertion_point),
+ ))
+ .next()
+ .is_none()
+ {
+ let mut text = String::from("\n");
+ text.extend(buffer.text_for_range(range_to_move.clone()));
+ text.pop(); // Drop trailing newline
+ edits.push((
+ buffer.anchor_after(range_to_move.start)
+ ..buffer.anchor_before(range_to_move.end),
+ String::new(),
+ ));
+ let insertion_anchor = buffer.anchor_after(insertion_point);
+ edits.push((insertion_anchor..insertion_anchor, text));
+
+ let row_delta = insertion_point.row - range_to_move.end.row + 1;
+
+ // Move selections down
+ new_selections.extend(contiguous_row_selections.drain(..).map(
+ |mut selection| {
+ selection.start.row += row_delta;
+ selection.end.row += row_delta;
+ selection
+ },
+ ));
+
+ // Move folds down
+ unfold_ranges.push(range_to_move.clone());
+ for fold in display_map.folds_in_range(
+ buffer.anchor_before(range_to_move.start)
+ ..buffer.anchor_after(range_to_move.end),
+ ) {
+ let mut start = fold.start.to_point(&buffer);
+ let mut end = fold.end.to_point(&buffer);
+ start.row += row_delta;
+ end.row += row_delta;
+ refold_ranges.push(start..end);
+ }
+ }
+ }
+
+ // If we didn't move line(s), preserve the existing selections
+ new_selections.append(&mut contiguous_row_selections);
+ }
+
+ self.transact(cx, |this, cx| {
+ this.unfold_ranges(unfold_ranges, true, true, cx);
+ this.buffer.update(cx, |buffer, cx| {
+ for (range, text) in edits {
+ buffer.edit([(range, text)], None, cx);
+ }
+ });
+ this.fold_ranges(refold_ranges, true, cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(new_selections));
+ });
+ }
+
+ pub fn transpose(&mut self, _: &Transpose, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
+ self.transact(cx, |this, cx| {
+ let edits = this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let mut edits: Vec<(Range<usize>, String)> = Default::default();
+ let line_mode = s.line_mode;
+ s.move_with(|display_map, selection| {
+ if !selection.is_empty() || line_mode {
+ return;
+ }
+
+ let mut head = selection.head();
+ let mut transpose_offset = head.to_offset(display_map, Bias::Right);
+ if head.column() == display_map.line_len(head.row()) {
+ transpose_offset = display_map
+ .buffer_snapshot
+ .clip_offset(transpose_offset.saturating_sub(1), Bias::Left);
+ }
+
+ if transpose_offset == 0 {
+ return;
+ }
+
+ *head.column_mut() += 1;
+ head = display_map.clip_point(head, Bias::Right);
+ let goal = SelectionGoal::HorizontalPosition(
+ display_map.x_for_point(head, &text_layout_details),
+ );
+ selection.collapse_to(head, goal);
+
+ let transpose_start = display_map
+ .buffer_snapshot
+ .clip_offset(transpose_offset.saturating_sub(1), Bias::Left);
+ if edits.last().map_or(true, |e| e.0.end <= transpose_start) {
+ let transpose_end = display_map
+ .buffer_snapshot
+ .clip_offset(transpose_offset + 1, Bias::Right);
+ if let Some(ch) =
+ display_map.buffer_snapshot.chars_at(transpose_start).next()
+ {
+ edits.push((transpose_start..transpose_offset, String::new()));
+ edits.push((transpose_end..transpose_end, ch.to_string()));
+ }
+ }
+ });
+ edits
+ });
+ this.buffer
+ .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
+ let selections = this.selections.all::<usize>(cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(selections);
+ });
+ });
+ }
+
+ pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
+ let mut text = String::new();
+ let buffer = self.buffer.read(cx).snapshot(cx);
+ let mut selections = self.selections.all::<Point>(cx);
+ let mut clipboard_selections = Vec::with_capacity(selections.len());
+ {
+ let max_point = buffer.max_point();
+ let mut is_first = true;
+ for selection in &mut selections {
+ let is_entire_line = selection.is_empty() || self.selections.line_mode;
+ if is_entire_line {
+ selection.start = Point::new(selection.start.row, 0);
+ selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0));
+ selection.goal = SelectionGoal::None;
+ }
+ if is_first {
+ is_first = false;
+ } else {
+ text += "\n";
+ }
+ let mut len = 0;
+ for chunk in buffer.text_for_range(selection.start..selection.end) {
+ text.push_str(chunk);
+ len += chunk.len();
+ }
+ clipboard_selections.push(ClipboardSelection {
+ len,
+ is_entire_line,
+ first_line_indent: buffer.indent_size_for_line(selection.start.row).len,
+ });
+ }
+ }
+
+ self.transact(cx, |this, cx| {
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(selections);
+ });
+ this.insert("", cx);
+ cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
+ });
+ }
+
+ pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
+ let selections = self.selections.all::<Point>(cx);
+ let buffer = self.buffer.read(cx).read(cx);
+ let mut text = String::new();
+
+ let mut clipboard_selections = Vec::with_capacity(selections.len());
+ {
+ let max_point = buffer.max_point();
+ let mut is_first = true;
+ for selection in selections.iter() {
+ let mut start = selection.start;
+ let mut end = selection.end;
+ let is_entire_line = selection.is_empty() || self.selections.line_mode;
+ if is_entire_line {
+ start = Point::new(start.row, 0);
+ end = cmp::min(max_point, Point::new(end.row + 1, 0));
+ }
+ if is_first {
+ is_first = false;
+ } else {
+ text += "\n";
+ }
+ let mut len = 0;
+ for chunk in buffer.text_for_range(start..end) {
+ text.push_str(chunk);
+ len += chunk.len();
+ }
+ clipboard_selections.push(ClipboardSelection {
+ len,
+ is_entire_line,
+ first_line_indent: buffer.indent_size_for_line(start.row).len,
+ });
+ }
+ }
+
+ cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections));
+ }
+
+ pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ if let Some(item) = cx.read_from_clipboard() {
+ let clipboard_text = Cow::Borrowed(item.text());
+ if let Some(mut clipboard_selections) = item.metadata::<Vec<ClipboardSelection>>() {
+ let old_selections = this.selections.all::<usize>(cx);
+ let all_selections_were_entire_line =
+ clipboard_selections.iter().all(|s| s.is_entire_line);
+ let first_selection_indent_column =
+ clipboard_selections.first().map(|s| s.first_line_indent);
+ if clipboard_selections.len() != old_selections.len() {
+ clipboard_selections.drain(..);
+ }
+
+ this.buffer.update(cx, |buffer, cx| {
+ let snapshot = buffer.read(cx);
+ let mut start_offset = 0;
+ let mut edits = Vec::new();
+ let mut original_indent_columns = Vec::new();
+ let line_mode = this.selections.line_mode;
+ for (ix, selection) in old_selections.iter().enumerate() {
+ let to_insert;
+ let entire_line;
+ let original_indent_column;
+ if let Some(clipboard_selection) = clipboard_selections.get(ix) {
+ let end_offset = start_offset + clipboard_selection.len;
+ to_insert = &clipboard_text[start_offset..end_offset];
+ entire_line = clipboard_selection.is_entire_line;
+ start_offset = end_offset + 1;
+ original_indent_column =
+ Some(clipboard_selection.first_line_indent);
+ } else {
+ to_insert = clipboard_text.as_str();
+ entire_line = all_selections_were_entire_line;
+ original_indent_column = first_selection_indent_column
+ }
+
+ // If the corresponding selection was empty when this slice of the
+ // clipboard text was written, then the entire line containing the
+ // selection was copied. If this selection is also currently empty,
+ // then paste the line before the current line of the buffer.
+ let range = if selection.is_empty() && !line_mode && entire_line {
+ let column = selection.start.to_point(&snapshot).column as usize;
+ let line_start = selection.start - column;
+ line_start..line_start
+ } else {
+ selection.range()
+ };
+
+ edits.push((range, to_insert));
+ original_indent_columns.extend(original_indent_column);
+ }
+ drop(snapshot);
+
+ buffer.edit(
+ edits,
+ Some(AutoindentMode::Block {
+ original_indent_columns,
+ }),
+ cx,
+ );
+ });
+
+ let selections = this.selections.all::<usize>(cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+ } else {
+ this.insert(&clipboard_text, cx);
+ }
+ }
+ });
+ }
+
+ pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext<Self>) {
+ if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) {
+ if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() {
+ self.change_selections(None, cx, |s| {
+ s.select_anchors(selections.to_vec());
+ });
+ }
+ self.request_autoscroll(Autoscroll::fit(), cx);
+ self.unmark_text(cx);
+ self.refresh_copilot_suggestions(true, cx);
+ cx.emit(Event::Edited);
+ }
+ }
+
+ pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext<Self>) {
+ if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) {
+ if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned()
+ {
+ self.change_selections(None, cx, |s| {
+ s.select_anchors(selections.to_vec());
+ });
+ }
+ self.request_autoscroll(Autoscroll::fit(), cx);
+ self.unmark_text(cx);
+ self.refresh_copilot_suggestions(true, cx);
+ cx.emit(Event::Edited);
+ }
+ }
+
+ pub fn finalize_last_transaction(&mut self, cx: &mut ViewContext<Self>) {
+ self.buffer
+ .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx));
+ }
+
+ pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext<Self>) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ let cursor = if selection.is_empty() && !line_mode {
+ movement::left(map, selection.start)
+ } else {
+ selection.start
+ };
+ selection.collapse_to(cursor, SelectionGoal::None);
+ });
+ })
+ }
+
+ pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext<Self>) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None));
+ })
+ }
+
+ pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext<Self>) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ let cursor = if selection.is_empty() && !line_mode {
+ movement::right(map, selection.end)
+ } else {
+ selection.end
+ };
+ selection.collapse_to(cursor, SelectionGoal::None)
+ });
+ })
+ }
+
+ pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext<Self>) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
+ })
+ }
+
+ pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
+ if self.take_rename(true, cx).is_some() {
+ return;
+ }
+
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ let text_layout_details = &self.text_layout_details(cx);
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if !selection.is_empty() && !line_mode {
+ selection.goal = SelectionGoal::None;
+ }
+ let (cursor, goal) = movement::up(
+ map,
+ selection.start,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
+ selection.collapse_to(cursor, goal);
+ });
+ })
+ }
+
+ pub fn move_page_up(&mut self, action: &MovePageUp, cx: &mut ViewContext<Self>) {
+ if self.take_rename(true, cx).is_some() {
+ return;
+ }
+
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ let row_count = if let Some(row_count) = self.visible_line_count() {
+ row_count as u32 - 1
+ } else {
+ return;
+ };
+
+ let autoscroll = if action.center_cursor {
+ Autoscroll::center()
+ } else {
+ Autoscroll::fit()
+ };
+
+ let text_layout_details = &self.text_layout_details(cx);
+
+ self.change_selections(Some(autoscroll), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if !selection.is_empty() && !line_mode {
+ selection.goal = SelectionGoal::None;
+ }
+ let (cursor, goal) = movement::up_by_rows(
+ map,
+ selection.end,
+ row_count,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
+ selection.collapse_to(cursor, goal);
+ });
+ });
+ }
+
+ pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, goal| {
+ movement::up(map, head, goal, false, &text_layout_details)
+ })
+ })
+ }
+
+ pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
+ self.take_rename(true, cx);
+
+ if self.mode == EditorMode::SingleLine {
+ cx.propagate_action();
+ return;
+ }
+
+ let text_layout_details = &self.text_layout_details(cx);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if !selection.is_empty() && !line_mode {
+ selection.goal = SelectionGoal::None;
+ }
+ let (cursor, goal) = movement::down(
+ map,
+ selection.end,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
+ selection.collapse_to(cursor, goal);
+ });
+ });
+ }
+
+ pub fn move_page_down(&mut self, action: &MovePageDown, cx: &mut ViewContext<Self>) {
+ if self.take_rename(true, cx).is_some() {
+ return;
+ }
+
+ if self
+ .context_menu
+ .write()
+ .as_mut()
+ .map(|menu| menu.select_last(self.project.as_ref(), cx))
+ .unwrap_or(false)
+ {
+ return;
+ }
+
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ let row_count = if let Some(row_count) = self.visible_line_count() {
+ row_count as u32 - 1
+ } else {
+ return;
+ };
+
+ let autoscroll = if action.center_cursor {
+ Autoscroll::center()
+ } else {
+ Autoscroll::fit()
+ };
+
+ let text_layout_details = &self.text_layout_details(cx);
+ self.change_selections(Some(autoscroll), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if !selection.is_empty() && !line_mode {
+ selection.goal = SelectionGoal::None;
+ }
+ let (cursor, goal) = movement::down_by_rows(
+ map,
+ selection.end,
+ row_count,
+ selection.goal,
+ false,
+ &text_layout_details,
+ );
+ selection.collapse_to(cursor, goal);
+ });
+ });
+ }
+
+ pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, goal| {
+ movement::down(map, head, goal, false, &text_layout_details)
+ })
+ });
+ }
+
+ pub fn context_menu_first(&mut self, _: &ContextMenuFirst, cx: &mut ViewContext<Self>) {
+ if let Some(context_menu) = self.context_menu.write().as_mut() {
+ context_menu.select_first(self.project.as_ref(), cx);
+ }
+ }
+
+ pub fn context_menu_prev(&mut self, _: &ContextMenuPrev, cx: &mut ViewContext<Self>) {
+ if let Some(context_menu) = self.context_menu.write().as_mut() {
+ context_menu.select_prev(self.project.as_ref(), cx);
+ }
+ }
+
+ pub fn context_menu_next(&mut self, _: &ContextMenuNext, cx: &mut ViewContext<Self>) {
+ if let Some(context_menu) = self.context_menu.write().as_mut() {
+ context_menu.select_next(self.project.as_ref(), cx);
+ }
+ }
+
+ pub fn context_menu_last(&mut self, _: &ContextMenuLast, cx: &mut ViewContext<Self>) {
+ if let Some(context_menu) = self.context_menu.write().as_mut() {
+ context_menu.select_last(self.project.as_ref(), cx);
+ }
+ }
+
+ pub fn move_to_previous_word_start(
+ &mut self,
+ _: &MoveToPreviousWordStart,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|map, head, _| {
+ (
+ movement::previous_word_start(map, head),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn move_to_previous_subword_start(
+ &mut self,
+ _: &MoveToPreviousSubwordStart,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|map, head, _| {
+ (
+ movement::previous_subword_start(map, head),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn select_to_previous_word_start(
+ &mut self,
+ _: &SelectToPreviousWordStart,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (
+ movement::previous_word_start(map, head),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn select_to_previous_subword_start(
+ &mut self,
+ _: &SelectToPreviousSubwordStart,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (
+ movement::previous_subword_start(map, head),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn delete_to_previous_word_start(
+ &mut self,
+ _: &DeleteToPreviousWordStart,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.transact(cx, |this, cx| {
+ this.select_autoclose_pair(cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if selection.is_empty() && !line_mode {
+ let cursor = movement::previous_word_start(map, selection.head());
+ selection.set_head(cursor, SelectionGoal::None);
+ }
+ });
+ });
+ this.insert("", cx);
+ });
+ }
+
+ pub fn delete_to_previous_subword_start(
+ &mut self,
+ _: &DeleteToPreviousSubwordStart,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.transact(cx, |this, cx| {
+ this.select_autoclose_pair(cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if selection.is_empty() && !line_mode {
+ let cursor = movement::previous_subword_start(map, selection.head());
+ selection.set_head(cursor, SelectionGoal::None);
+ }
+ });
+ });
+ this.insert("", cx);
+ });
+ }
+
+ pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext<Self>) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|map, head, _| {
+ (movement::next_word_end(map, head), SelectionGoal::None)
+ });
+ })
+ }
+
+ pub fn move_to_next_subword_end(
+ &mut self,
+ _: &MoveToNextSubwordEnd,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|map, head, _| {
+ (movement::next_subword_end(map, head), SelectionGoal::None)
+ });
+ })
+ }
+
+ pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext<Self>) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (movement::next_word_end(map, head), SelectionGoal::None)
+ });
+ })
+ }
+
+ pub fn select_to_next_subword_end(
+ &mut self,
+ _: &SelectToNextSubwordEnd,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (movement::next_subword_end(map, head), SelectionGoal::None)
+ });
+ })
+ }
+
+ pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let line_mode = s.line_mode;
+ s.move_with(|map, selection| {
+ if selection.is_empty() && !line_mode {
+ let cursor = movement::next_word_end(map, selection.head());
+ selection.set_head(cursor, SelectionGoal::None);
+ }
+ });
+ });
+ this.insert("", cx);
+ });
+ }
+
+ pub fn delete_to_next_subword_end(
+ &mut self,
+ _: &DeleteToNextSubwordEnd,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.transact(cx, |this, cx| {
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_with(|map, selection| {
+ if selection.is_empty() {
+ let cursor = movement::next_subword_end(map, selection.head());
+ selection.set_head(cursor, SelectionGoal::None);
+ }
+ });
+ });
+ this.insert("", cx);
+ });
+ }
+
+ pub fn move_to_beginning_of_line(
+ &mut self,
+ _: &MoveToBeginningOfLine,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|map, head, _| {
+ (
+ movement::indented_line_beginning(map, head, true),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn select_to_beginning_of_line(
+ &mut self,
+ action: &SelectToBeginningOfLine,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (
+ movement::indented_line_beginning(map, head, action.stop_at_soft_wraps),
+ SelectionGoal::None,
+ )
+ });
+ });
+ }
+
+ pub fn delete_to_beginning_of_line(
+ &mut self,
+ _: &DeleteToBeginningOfLine,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.transact(cx, |this, cx| {
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_with(|_, selection| {
+ selection.reversed = true;
+ });
+ });
+
+ this.select_to_beginning_of_line(
+ &SelectToBeginningOfLine {
+ stop_at_soft_wraps: false,
+ },
+ cx,
+ );
+ this.backspace(&Backspace, cx);
+ });
+ }
+
+ pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext<Self>) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|map, head, _| {
+ (movement::line_end(map, head, true), SelectionGoal::None)
+ });
+ })
+ }
+
+ pub fn select_to_end_of_line(
+ &mut self,
+ action: &SelectToEndOfLine,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (
+ movement::line_end(map, head, action.stop_at_soft_wraps),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ this.select_to_end_of_line(
+ &SelectToEndOfLine {
+ stop_at_soft_wraps: false,
+ },
+ cx,
+ );
+ this.delete(&Delete, cx);
+ });
+ }
+
+ pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ this.select_to_end_of_line(
+ &SelectToEndOfLine {
+ stop_at_soft_wraps: false,
+ },
+ cx,
+ );
+ this.cut(&Cut, cx);
+ });
+ }
+
+ pub fn move_to_start_of_paragraph(
+ &mut self,
+ _: &MoveToStartOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_with(|map, selection| {
+ selection.collapse_to(
+ movement::start_of_paragraph(map, selection.head(), 1),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn move_to_end_of_paragraph(
+ &mut self,
+ _: &MoveToEndOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_with(|map, selection| {
+ selection.collapse_to(
+ movement::end_of_paragraph(map, selection.head(), 1),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn select_to_start_of_paragraph(
+ &mut self,
+ _: &SelectToStartOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (
+ movement::start_of_paragraph(map, head, 1),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn select_to_end_of_paragraph(
+ &mut self,
+ _: &SelectToEndOfParagraph,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_heads_with(|map, head, _| {
+ (
+ movement::end_of_paragraph(map, head, 1),
+ SelectionGoal::None,
+ )
+ });
+ })
+ }
+
+ pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext<Self>) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(vec![0..0]);
+ });
+ }
+
+ pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext<Self>) {
+ let mut selection = self.selections.last::<Point>(cx);
+ selection.set_head(Point::zero(), SelectionGoal::None);
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(vec![selection]);
+ });
+ }
+
+ pub fn move_to_end(&mut self, _: &MoveToEnd, cx: &mut ViewContext<Self>) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ let cursor = self.buffer.read(cx).read(cx).len();
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(vec![cursor..cursor])
+ });
+ }
+
+ pub fn set_nav_history(&mut self, nav_history: Option<ItemNavHistory>) {
+ self.nav_history = nav_history;
+ }
+
+ pub fn nav_history(&self) -> Option<&ItemNavHistory> {
+ self.nav_history.as_ref()
+ }
+
+ fn push_to_nav_history(
+ &mut self,
+ cursor_anchor: Anchor,
+ new_position: Option<Point>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(nav_history) = self.nav_history.as_mut() {
+ let buffer = self.buffer.read(cx).read(cx);
+ let cursor_position = cursor_anchor.to_point(&buffer);
+ let scroll_state = self.scroll_manager.anchor();
+ let scroll_top_row = scroll_state.top_row(&buffer);
+ drop(buffer);
+
+ if let Some(new_position) = new_position {
+ let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs();
+ if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA {
+ return;
+ }
+ }
+
+ nav_history.push(
+ Some(NavigationData {
+ cursor_anchor,
+ cursor_position,
+ scroll_anchor: scroll_state,
+ scroll_top_row,
+ }),
+ cx,
+ );
+ }
+ }
+
+ pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext<Self>) {
+ let buffer = self.buffer.read(cx).snapshot(cx);
+ let mut selection = self.selections.first::<usize>(cx);
+ selection.set_head(buffer.len(), SelectionGoal::None);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(vec![selection]);
+ });
+ }
+
+ pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
+ let end = self.buffer.read(cx).read(cx).len();
+ self.change_selections(None, cx, |s| {
+ s.select_ranges(vec![0..end]);
+ });
+ }
+
+ pub fn select_line(&mut self, _: &SelectLine, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let mut selections = self.selections.all::<Point>(cx);
+ let max_point = display_map.buffer_snapshot.max_point();
+ for selection in &mut selections {
+ let rows = selection.spanned_rows(true, &display_map);
+ selection.start = Point::new(rows.start, 0);
+ selection.end = cmp::min(max_point, Point::new(rows.end, 0));
+ selection.reversed = false;
+ }
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(selections);
+ });
+ }
+
+ pub fn split_selection_into_lines(
+ &mut self,
+ _: &SplitSelectionIntoLines,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let mut to_unfold = Vec::new();
+ let mut new_selection_ranges = Vec::new();
+ {
+ let selections = self.selections.all::<Point>(cx);
+ let buffer = self.buffer.read(cx).read(cx);
+ for selection in selections {
+ for row in selection.start.row..selection.end.row {
+ let cursor = Point::new(row, buffer.line_len(row));
+ new_selection_ranges.push(cursor..cursor);
+ }
+ new_selection_ranges.push(selection.end..selection.end);
+ to_unfold.push(selection.start..selection.end);
+ }
+ }
+ self.unfold_ranges(to_unfold, true, true, cx);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(new_selection_ranges);
+ });
+ }
+
+ pub fn add_selection_above(&mut self, _: &AddSelectionAbove, cx: &mut ViewContext<Self>) {
+ self.add_selection(true, cx);
+ }
+
+ pub fn add_selection_below(&mut self, _: &AddSelectionBelow, cx: &mut ViewContext<Self>) {
+ self.add_selection(false, cx);
+ }
+
+ fn add_selection(&mut self, above: bool, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let mut selections = self.selections.all::<Point>(cx);
+ let text_layout_details = self.text_layout_details(cx);
+ let mut state = self.add_selections_state.take().unwrap_or_else(|| {
+ let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone();
+ let range = oldest_selection.display_range(&display_map).sorted();
+
+ let start_x = display_map.x_for_point(range.start, &text_layout_details);
+ let end_x = display_map.x_for_point(range.end, &text_layout_details);
+ let positions = start_x.min(end_x)..start_x.max(end_x);
+
+ selections.clear();
+ let mut stack = Vec::new();
+ for row in range.start.row()..=range.end.row() {
+ if let Some(selection) = self.selections.build_columnar_selection(
+ &display_map,
+ row,
+ &positions,
+ oldest_selection.reversed,
+ &text_layout_details,
+ ) {
+ stack.push(selection.id);
+ selections.push(selection);
+ }
+ }
+
+ if above {
+ stack.reverse();
+ }
+
+ AddSelectionsState { above, stack }
+ });
+
+ let last_added_selection = *state.stack.last().unwrap();
+ let mut new_selections = Vec::new();
+ if above == state.above {
+ let end_row = if above {
+ 0
+ } else {
+ display_map.max_point().row()
+ };
+
+ 'outer: for selection in selections {
+ if selection.id == last_added_selection {
+ let range = selection.display_range(&display_map).sorted();
+ debug_assert_eq!(range.start.row(), range.end.row());
+ let mut row = range.start.row();
+ let positions = if let SelectionGoal::HorizontalRange { start, end } =
+ selection.goal
+ {
+ start..end
+ } else {
+ let start_x = display_map.x_for_point(range.start, &text_layout_details);
+ let end_x = display_map.x_for_point(range.end, &text_layout_details);
+
+ start_x.min(end_x)..start_x.max(end_x)
+ };
+
+ while row != end_row {
+ if above {
+ row -= 1;
+ } else {
+ row += 1;
+ }
+
+ if let Some(new_selection) = self.selections.build_columnar_selection(
+ &display_map,
+ row,
+ &positions,
+ selection.reversed,
+ &text_layout_details,
+ ) {
+ state.stack.push(new_selection.id);
+ if above {
+ new_selections.push(new_selection);
+ new_selections.push(selection);
+ } else {
+ new_selections.push(selection);
+ new_selections.push(new_selection);
+ }
+
+ continue 'outer;
+ }
+ }
+ }
+
+ new_selections.push(selection);
+ }
+ } else {
+ new_selections = selections;
+ new_selections.retain(|s| s.id != last_added_selection);
+ state.stack.pop();
+ }
+
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(new_selections);
+ });
+ if state.stack.len() > 1 {
+ self.add_selections_state = Some(state);
+ }
+ }
+
+ pub fn select_next_match_internal(
+ &mut self,
+ display_map: &DisplaySnapshot,
+ replace_newest: bool,
+ autoscroll: Option<Autoscroll>,
+ cx: &mut ViewContext<Self>,
+ ) -> Result<()> {
+ fn select_next_match_ranges(
+ this: &mut Editor,
+ range: Range<usize>,
+ replace_newest: bool,
+ auto_scroll: Option<Autoscroll>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ this.unfold_ranges([range.clone()], false, true, cx);
+ this.change_selections(auto_scroll, cx, |s| {
+ if replace_newest {
+ s.delete(s.newest_anchor().id);
+ }
+ s.insert_range(range.clone());
+ });
+ }
+
+ let buffer = &display_map.buffer_snapshot;
+ let mut selections = self.selections.all::<usize>(cx);
+ if let Some(mut select_next_state) = self.select_next_state.take() {
+ let query = &select_next_state.query;
+ if !select_next_state.done {
+ let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
+ let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
+ let mut next_selected_range = None;
+
+ let bytes_after_last_selection =
+ buffer.bytes_in_range(last_selection.end..buffer.len());
+ let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start);
+ let query_matches = query
+ .stream_find_iter(bytes_after_last_selection)
+ .map(|result| (last_selection.end, result))
+ .chain(
+ query
+ .stream_find_iter(bytes_before_first_selection)
+ .map(|result| (0, result)),
+ );
+
+ for (start_offset, query_match) in query_matches {
+ let query_match = query_match.unwrap(); // can only fail due to I/O
+ let offset_range =
+ start_offset + query_match.start()..start_offset + query_match.end();
+ let display_range = offset_range.start.to_display_point(&display_map)
+ ..offset_range.end.to_display_point(&display_map);
+
+ if !select_next_state.wordwise
+ || (!movement::is_inside_word(&display_map, display_range.start)
+ && !movement::is_inside_word(&display_map, display_range.end))
+ {
+ if selections
+ .iter()
+ .find(|selection| selection.range().overlaps(&offset_range))
+ .is_none()
+ {
+ next_selected_range = Some(offset_range);
+ break;
+ }
+ }
+ }
+
+ if let Some(next_selected_range) = next_selected_range {
+ select_next_match_ranges(
+ self,
+ next_selected_range,
+ replace_newest,
+ autoscroll,
+ cx,
+ );
+ } else {
+ select_next_state.done = true;
+ }
+ }
+
+ self.select_next_state = Some(select_next_state);
+ } else if selections.len() == 1 {
+ let selection = selections.last_mut().unwrap();
+ if selection.start == selection.end {
+ let word_range = movement::surrounding_word(
+ &display_map,
+ selection.start.to_display_point(&display_map),
+ );
+ selection.start = word_range.start.to_offset(&display_map, Bias::Left);
+ selection.end = word_range.end.to_offset(&display_map, Bias::Left);
+ selection.goal = SelectionGoal::None;
+ selection.reversed = false;
+
+ let query = buffer
+ .text_for_range(selection.start..selection.end)
+ .collect::<String>();
+
+ let is_empty = query.is_empty();
+ let select_state = SelectNextState {
+ query: AhoCorasick::new(&[query])?,
+ wordwise: true,
+ done: is_empty,
+ };
+ select_next_match_ranges(
+ self,
+ selection.start..selection.end,
+ replace_newest,
+ autoscroll,
+ cx,
+ );
+ self.select_next_state = Some(select_state);
+ } else {
+ let query = buffer
+ .text_for_range(selection.start..selection.end)
+ .collect::<String>();
+ self.select_next_state = Some(SelectNextState {
+ query: AhoCorasick::new(&[query])?,
+ wordwise: false,
+ done: false,
+ });
+ self.select_next_match_internal(display_map, replace_newest, autoscroll, cx)?;
+ }
+ }
+ Ok(())
+ }
+
+ pub fn select_all_matches(
+ &mut self,
+ action: &SelectAllMatches,
+ cx: &mut ViewContext<Self>,
+ ) -> Result<()> {
+ self.push_to_selection_history();
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ loop {
+ self.select_next_match_internal(&display_map, action.replace_newest, None, cx)?;
+
+ if self
+ .select_next_state
+ .as_ref()
+ .map(|selection_state| selection_state.done)
+ .unwrap_or(true)
+ {
+ break;
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext<Self>) -> Result<()> {
+ self.push_to_selection_history();
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ self.select_next_match_internal(
+ &display_map,
+ action.replace_newest,
+ Some(Autoscroll::newest()),
+ cx,
+ )?;
+ Ok(())
+ }
+
+ pub fn select_previous(
+ &mut self,
+ action: &SelectPrevious,
+ cx: &mut ViewContext<Self>,
+ ) -> Result<()> {
+ self.push_to_selection_history();
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = &display_map.buffer_snapshot;
+ let mut selections = self.selections.all::<usize>(cx);
+ if let Some(mut select_prev_state) = self.select_prev_state.take() {
+ let query = &select_prev_state.query;
+ if !select_prev_state.done {
+ let first_selection = selections.iter().min_by_key(|s| s.id).unwrap();
+ let last_selection = selections.iter().max_by_key(|s| s.id).unwrap();
+ let mut next_selected_range = None;
+ // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer.
+ let bytes_before_last_selection =
+ buffer.reversed_bytes_in_range(0..last_selection.start);
+ let bytes_after_first_selection =
+ buffer.reversed_bytes_in_range(first_selection.end..buffer.len());
+ let query_matches = query
+ .stream_find_iter(bytes_before_last_selection)
+ .map(|result| (last_selection.start, result))
+ .chain(
+ query
+ .stream_find_iter(bytes_after_first_selection)
+ .map(|result| (buffer.len(), result)),
+ );
+ for (end_offset, query_match) in query_matches {
+ let query_match = query_match.unwrap(); // can only fail due to I/O
+ let offset_range =
+ end_offset - query_match.end()..end_offset - query_match.start();
+ let display_range = offset_range.start.to_display_point(&display_map)
+ ..offset_range.end.to_display_point(&display_map);
+
+ if !select_prev_state.wordwise
+ || (!movement::is_inside_word(&display_map, display_range.start)
+ && !movement::is_inside_word(&display_map, display_range.end))
+ {
+ next_selected_range = Some(offset_range);
+ break;
+ }
+ }
+
+ if let Some(next_selected_range) = next_selected_range {
+ self.unfold_ranges([next_selected_range.clone()], false, true, cx);
+ self.change_selections(Some(Autoscroll::newest()), cx, |s| {
+ if action.replace_newest {
+ s.delete(s.newest_anchor().id);
+ }
+ s.insert_range(next_selected_range);
+ });
+ } else {
+ select_prev_state.done = true;
+ }
+ }
+
+ self.select_prev_state = Some(select_prev_state);
+ } else if selections.len() == 1 {
+ let selection = selections.last_mut().unwrap();
+ if selection.start == selection.end {
+ let word_range = movement::surrounding_word(
+ &display_map,
+ selection.start.to_display_point(&display_map),
+ );
+ selection.start = word_range.start.to_offset(&display_map, Bias::Left);
+ selection.end = word_range.end.to_offset(&display_map, Bias::Left);
+ selection.goal = SelectionGoal::None;
+ selection.reversed = false;
+
+ let query = buffer
+ .text_for_range(selection.start..selection.end)
+ .collect::<String>();
+ let query = query.chars().rev().collect::<String>();
+ let select_state = SelectNextState {
+ query: AhoCorasick::new(&[query])?,
+ wordwise: true,
+ done: false,
+ };
+ self.unfold_ranges([selection.start..selection.end], false, true, cx);
+ self.change_selections(Some(Autoscroll::newest()), cx, |s| {
+ s.select(selections);
+ });
+ self.select_prev_state = Some(select_state);
+ } else {
+ let query = buffer
+ .text_for_range(selection.start..selection.end)
+ .collect::<String>();
+ let query = query.chars().rev().collect::<String>();
+ self.select_prev_state = Some(SelectNextState {
+ query: AhoCorasick::new(&[query])?,
+ wordwise: false,
+ done: false,
+ });
+ self.select_previous(action, cx)?;
+ }
+ }
+ Ok(())
+ }
+
+ pub fn toggle_comments(&mut self, action: &ToggleComments, cx: &mut ViewContext<Self>) {
+ let text_layout_details = &self.text_layout_details(cx);
+ self.transact(cx, |this, cx| {
+ let mut selections = this.selections.all::<Point>(cx);
+ let mut edits = Vec::new();
+ let mut selection_edit_ranges = Vec::new();
+ let mut last_toggled_row = None;
+ let snapshot = this.buffer.read(cx).read(cx);
+ let empty_str: Arc<str> = "".into();
+ let mut suffixes_inserted = Vec::new();
+
+ fn comment_prefix_range(
+ snapshot: &MultiBufferSnapshot,
+ row: u32,
+ comment_prefix: &str,
+ comment_prefix_whitespace: &str,
+ ) -> Range<Point> {
+ let start = Point::new(row, snapshot.indent_size_for_line(row).len);
+
+ let mut line_bytes = snapshot
+ .bytes_in_range(start..snapshot.max_point())
+ .flatten()
+ .copied();
+
+ // If this line currently begins with the line comment prefix, then record
+ // the range containing the prefix.
+ if line_bytes
+ .by_ref()
+ .take(comment_prefix.len())
+ .eq(comment_prefix.bytes())
+ {
+ // Include any whitespace that matches the comment prefix.
+ let matching_whitespace_len = line_bytes
+ .zip(comment_prefix_whitespace.bytes())
+ .take_while(|(a, b)| a == b)
+ .count() as u32;
+ let end = Point::new(
+ start.row,
+ start.column + comment_prefix.len() as u32 + matching_whitespace_len,
+ );
+ start..end
+ } else {
+ start..start
+ }
+ }
+
+ fn comment_suffix_range(
+ snapshot: &MultiBufferSnapshot,
+ row: u32,
+ comment_suffix: &str,
+ comment_suffix_has_leading_space: bool,
+ ) -> Range<Point> {
+ let end = Point::new(row, snapshot.line_len(row));
+ let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32);
+
+ let mut line_end_bytes = snapshot
+ .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end)
+ .flatten()
+ .copied();
+
+ let leading_space_len = if suffix_start_column > 0
+ && line_end_bytes.next() == Some(b' ')
+ && comment_suffix_has_leading_space
+ {
+ 1
+ } else {
+ 0
+ };
+
+ // If this line currently begins with the line comment prefix, then record
+ // the range containing the prefix.
+ if line_end_bytes.by_ref().eq(comment_suffix.bytes()) {
+ let start = Point::new(end.row, suffix_start_column - leading_space_len);
+ start..end
+ } else {
+ end..end
+ }
+ }
+
+ // TODO: Handle selections that cross excerpts
+ for selection in &mut selections {
+ let start_column = snapshot.indent_size_for_line(selection.start.row).len;
+ let language = if let Some(language) =
+ snapshot.language_scope_at(Point::new(selection.start.row, start_column))
+ {
+ language
+ } else {
+ continue;
+ };
+
+ selection_edit_ranges.clear();
+
+ // If multiple selections contain a given row, avoid processing that
+ // row more than once.
+ let mut start_row = selection.start.row;
+ if last_toggled_row == Some(start_row) {
+ start_row += 1;
+ }
+ let end_row =
+ if selection.end.row > selection.start.row && selection.end.column == 0 {
+ selection.end.row - 1
+ } else {
+ selection.end.row
+ };
+ last_toggled_row = Some(end_row);
+
+ if start_row > end_row {
+ continue;
+ }
+
+ // If the language has line comments, toggle those.
+ if let Some(full_comment_prefix) = language.line_comment_prefix() {
+ // Split the comment prefix's trailing whitespace into a separate string,
+ // as that portion won't be used for detecting if a line is a comment.
+ let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+ let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
+ let mut all_selection_lines_are_comments = true;
+
+ for row in start_row..=end_row {
+ if snapshot.is_line_blank(row) && start_row < end_row {
+ continue;
+ }
+
+ let prefix_range = comment_prefix_range(
+ snapshot.deref(),
+ row,
+ comment_prefix,
+ comment_prefix_whitespace,
+ );
+ if prefix_range.is_empty() {
+ all_selection_lines_are_comments = false;
+ }
+ selection_edit_ranges.push(prefix_range);
+ }
+
+ if all_selection_lines_are_comments {
+ edits.extend(
+ selection_edit_ranges
+ .iter()
+ .cloned()
+ .map(|range| (range, empty_str.clone())),
+ );
+ } else {
+ let min_column = selection_edit_ranges
+ .iter()
+ .map(|r| r.start.column)
+ .min()
+ .unwrap_or(0);
+ edits.extend(selection_edit_ranges.iter().map(|range| {
+ let position = Point::new(range.start.row, min_column);
+ (position..position, full_comment_prefix.clone())
+ }));
+ }
+ } else if let Some((full_comment_prefix, comment_suffix)) =
+ language.block_comment_delimiters()
+ {
+ let comment_prefix = full_comment_prefix.trim_end_matches(' ');
+ let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..];
+ let prefix_range = comment_prefix_range(
+ snapshot.deref(),
+ start_row,
+ comment_prefix,
+ comment_prefix_whitespace,
+ );
+ let suffix_range = comment_suffix_range(
+ snapshot.deref(),
+ end_row,
+ comment_suffix.trim_start_matches(' '),
+ comment_suffix.starts_with(' '),
+ );
+
+ if prefix_range.is_empty() || suffix_range.is_empty() {
+ edits.push((
+ prefix_range.start..prefix_range.start,
+ full_comment_prefix.clone(),
+ ));
+ edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone()));
+ suffixes_inserted.push((end_row, comment_suffix.len()));
+ } else {
+ edits.push((prefix_range, empty_str.clone()));
+ edits.push((suffix_range, empty_str.clone()));
+ }
+ } else {
+ continue;
+ }
+ }
+
+ drop(snapshot);
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, None, cx);
+ });
+
+ // Adjust selections so that they end before any comment suffixes that
+ // were inserted.
+ let mut suffixes_inserted = suffixes_inserted.into_iter().peekable();
+ let mut selections = this.selections.all::<Point>(cx);
+ let snapshot = this.buffer.read(cx).read(cx);
+ for selection in &mut selections {
+ while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() {
+ match row.cmp(&selection.end.row) {
+ Ordering::Less => {
+ suffixes_inserted.next();
+ continue;
+ }
+ Ordering::Greater => break,
+ Ordering::Equal => {
+ if selection.end.column == snapshot.line_len(row) {
+ if selection.is_empty() {
+ selection.start.column -= suffix_len as u32;
+ }
+ selection.end.column -= suffix_len as u32;
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ drop(snapshot);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+
+ let selections = this.selections.all::<Point>(cx);
+ let selections_on_single_row = selections.windows(2).all(|selections| {
+ selections[0].start.row == selections[1].start.row
+ && selections[0].end.row == selections[1].end.row
+ && selections[0].start.row == selections[0].end.row
+ });
+ let selections_selecting = selections
+ .iter()
+ .any(|selection| selection.start != selection.end);
+ let advance_downwards = action.advance_downwards
+ && selections_on_single_row
+ && !selections_selecting
+ && this.mode != EditorMode::SingleLine;
+
+ if advance_downwards {
+ let snapshot = this.buffer.read(cx).snapshot(cx);
+
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_cursors_with(|display_snapshot, display_point, _| {
+ let mut point = display_point.to_point(display_snapshot);
+ point.row += 1;
+ point = snapshot.clip_point(point, Bias::Left);
+ let display_point = point.to_display_point(display_snapshot);
+ let goal = SelectionGoal::HorizontalPosition(
+ display_snapshot.x_for_point(display_point, &text_layout_details),
+ );
+ (display_point, goal)
+ })
+ });
+ }
+ });
+ }
+
+ pub fn select_larger_syntax_node(
+ &mut self,
+ _: &SelectLargerSyntaxNode,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = self.buffer.read(cx).snapshot(cx);
+ let old_selections = self.selections.all::<usize>(cx).into_boxed_slice();
+
+ let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
+ let mut selected_larger_node = false;
+ let new_selections = old_selections
+ .iter()
+ .map(|selection| {
+ let old_range = selection.start..selection.end;
+ let mut new_range = old_range.clone();
+ while let Some(containing_range) =
+ buffer.range_for_syntax_ancestor(new_range.clone())
+ {
+ new_range = containing_range;
+ if !display_map.intersects_fold(new_range.start)
+ && !display_map.intersects_fold(new_range.end)
+ {
+ break;
+ }
+ }
+
+ selected_larger_node |= new_range != old_range;
+ Selection {
+ id: selection.id,
+ start: new_range.start,
+ end: new_range.end,
+ goal: SelectionGoal::None,
+ reversed: selection.reversed,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ if selected_larger_node {
+ stack.push(old_selections);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(new_selections);
+ });
+ }
+ self.select_larger_syntax_node_stack = stack;
+ }
+
+ pub fn select_smaller_syntax_node(
+ &mut self,
+ _: &SelectSmallerSyntaxNode,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let mut stack = mem::take(&mut self.select_larger_syntax_node_stack);
+ if let Some(selections) = stack.pop() {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(selections.to_vec());
+ });
+ }
+ self.select_larger_syntax_node_stack = stack;
+ }
+
+ pub fn move_to_enclosing_bracket(
+ &mut self,
+ _: &MoveToEnclosingBracket,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.move_offsets_with(|snapshot, selection| {
+ let Some(enclosing_bracket_ranges) =
+ snapshot.enclosing_bracket_ranges(selection.start..selection.end)
+ else {
+ return;
+ };
+
+ let mut best_length = usize::MAX;
+ let mut best_inside = false;
+ let mut best_in_bracket_range = false;
+ let mut best_destination = None;
+ for (open, close) in enclosing_bracket_ranges {
+ let close = close.to_inclusive();
+ let length = close.end() - open.start;
+ let inside = selection.start >= open.end && selection.end <= *close.start();
+ let in_bracket_range = open.to_inclusive().contains(&selection.head())
+ || close.contains(&selection.head());
+
+ // If best is next to a bracket and current isn't, skip
+ if !in_bracket_range && best_in_bracket_range {
+ continue;
+ }
+
+ // Prefer smaller lengths unless best is inside and current isn't
+ if length > best_length && (best_inside || !inside) {
+ continue;
+ }
+
+ best_length = length;
+ best_inside = inside;
+ best_in_bracket_range = in_bracket_range;
+ best_destination = Some(
+ if close.contains(&selection.start) && close.contains(&selection.end) {
+ if inside {
+ open.end
+ } else {
+ open.start
+ }
+ } else {
+ if inside {
+ *close.start()
+ } else {
+ *close.end()
+ }
+ },
+ );
+ }
+
+ if let Some(destination) = best_destination {
+ selection.collapse_to(destination, SelectionGoal::None);
+ }
+ })
+ });
+ }
+
+ pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext<Self>) {
+ self.end_selection(cx);
+ self.selection_history.mode = SelectionHistoryMode::Undoing;
+ if let Some(entry) = self.selection_history.undo_stack.pop_back() {
+ self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
+ self.select_next_state = entry.select_next_state;
+ self.select_prev_state = entry.select_prev_state;
+ self.add_selections_state = entry.add_selections_state;
+ self.request_autoscroll(Autoscroll::newest(), cx);
+ }
+ self.selection_history.mode = SelectionHistoryMode::Normal;
+ }
+
+ pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext<Self>) {
+ self.end_selection(cx);
+ self.selection_history.mode = SelectionHistoryMode::Redoing;
+ if let Some(entry) = self.selection_history.redo_stack.pop_back() {
+ self.change_selections(None, cx, |s| s.select_anchors(entry.selections.to_vec()));
+ self.select_next_state = entry.select_next_state;
+ self.select_prev_state = entry.select_prev_state;
+ self.add_selections_state = entry.add_selections_state;
+ self.request_autoscroll(Autoscroll::newest(), cx);
+ }
+ self.selection_history.mode = SelectionHistoryMode::Normal;
+ }
+
+ fn go_to_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext<Self>) {
+ self.go_to_diagnostic_impl(Direction::Next, cx)
+ }
+
+ fn go_to_prev_diagnostic(&mut self, _: &GoToPrevDiagnostic, cx: &mut ViewContext<Self>) {
+ self.go_to_diagnostic_impl(Direction::Prev, cx)
+ }
+
+ pub fn go_to_diagnostic_impl(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
+ let buffer = self.buffer.read(cx).snapshot(cx);
+ let selection = self.selections.newest::<usize>(cx);
+
+ // If there is an active Diagnostic Popover. Jump to it's diagnostic instead.
+ if direction == Direction::Next {
+ if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
+ let (group_id, jump_to) = popover.activation_info();
+ if self.activate_diagnostics(group_id, cx) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let mut new_selection = s.newest_anchor().clone();
+ new_selection.collapse_to(jump_to, SelectionGoal::None);
+ s.select_anchors(vec![new_selection.clone()]);
+ });
+ }
+ return;
+ }
+ }
+
+ let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
+ active_diagnostics
+ .primary_range
+ .to_offset(&buffer)
+ .to_inclusive()
+ });
+ let mut search_start = if let Some(active_primary_range) = active_primary_range.as_ref() {
+ if active_primary_range.contains(&selection.head()) {
+ *active_primary_range.end()
+ } else {
+ selection.head()
+ }
+ } else {
+ selection.head()
+ };
+
+ loop {
+ let mut diagnostics = if direction == Direction::Prev {
+ buffer.diagnostics_in_range::<_, usize>(0..search_start, true)
+ } else {
+ buffer.diagnostics_in_range::<_, usize>(search_start..buffer.len(), false)
+ };
+ let group = diagnostics.find_map(|entry| {
+ if entry.diagnostic.is_primary
+ && entry.diagnostic.severity <= DiagnosticSeverity::WARNING
+ && !entry.range.is_empty()
+ && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end())
+ && !entry.range.contains(&search_start)
+ {
+ Some((entry.range, entry.diagnostic.group_id))
+ } else {
+ None
+ }
+ });
+
+ if let Some((primary_range, group_id)) = group {
+ if self.activate_diagnostics(group_id, cx) {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select(vec![Selection {
+ id: selection.id,
+ start: primary_range.start,
+ end: primary_range.start,
+ reversed: false,
+ goal: SelectionGoal::None,
+ }]);
+ });
+ }
+ break;
+ } else {
+ // Cycle around to the start of the buffer, potentially moving back to the start of
+ // the currently active diagnostic.
+ active_primary_range.take();
+ if direction == Direction::Prev {
+ if search_start == buffer.len() {
+ break;
+ } else {
+ search_start = buffer.len();
+ }
+ } else if search_start == 0 {
+ break;
+ } else {
+ search_start = 0;
+ }
+ }
+ }
+ }
+
+ fn go_to_hunk(&mut self, _: &GoToHunk, cx: &mut ViewContext<Self>) {
+ let snapshot = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.snapshot(cx));
+ let selection = self.selections.newest::<Point>(cx);
+
+ if !self.seek_in_direction(
+ &snapshot,
+ selection.head(),
+ false,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range((selection.head().row + 1)..u32::MAX),
+ cx,
+ ) {
+ let wrapped_point = Point::zero();
+ self.seek_in_direction(
+ &snapshot,
+ wrapped_point,
+ true,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range((wrapped_point.row + 1)..u32::MAX),
+ cx,
+ );
+ }
+ }
+
+ fn go_to_prev_hunk(&mut self, _: &GoToPrevHunk, cx: &mut ViewContext<Self>) {
+ let snapshot = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.snapshot(cx));
+ let selection = self.selections.newest::<Point>(cx);
+
+ if !self.seek_in_direction(
+ &snapshot,
+ selection.head(),
+ false,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range_rev(0..selection.head().row),
+ cx,
+ ) {
+ let wrapped_point = snapshot.buffer_snapshot.max_point();
+ self.seek_in_direction(
+ &snapshot,
+ wrapped_point,
+ true,
+ snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range_rev(0..wrapped_point.row),
+ cx,
+ );
+ }
+ }
+
+ fn seek_in_direction(
+ &mut self,
+ snapshot: &DisplaySnapshot,
+ initial_point: Point,
+ is_wrapped: bool,
+ hunks: impl Iterator<Item = DiffHunk<u32>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ let display_point = initial_point.to_display_point(snapshot);
+ let mut hunks = hunks
+ .map(|hunk| diff_hunk_to_display(hunk, &snapshot))
+ .filter(|hunk| {
+ if is_wrapped {
+ true
+ } else {
+ !hunk.contains_display_row(display_point.row())
+ }
+ })
+ .dedup();
+
+ if let Some(hunk) = hunks.next() {
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ let row = hunk.start_display_row();
+ let point = DisplayPoint::new(row, 0);
+ s.select_display_ranges([point..point]);
+ });
+
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn go_to_definition(&mut self, _: &GoToDefinition, cx: &mut ViewContext<Self>) {
+ self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, cx);
+ }
+
+ pub fn go_to_type_definition(&mut self, _: &GoToTypeDefinition, cx: &mut ViewContext<Self>) {
+ self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, cx);
+ }
+
+ pub fn go_to_definition_split(&mut self, _: &GoToDefinitionSplit, cx: &mut ViewContext<Self>) {
+ self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, cx);
+ }
+
+ pub fn go_to_type_definition_split(
+ &mut self,
+ _: &GoToTypeDefinitionSplit,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, cx);
+ }
+
+ fn go_to_definition_of_kind(
+ &mut self,
+ kind: GotoDefinitionKind,
+ split: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let Some(workspace) = self.workspace(cx) else {
+ return;
+ };
+ let buffer = self.buffer.read(cx);
+ let head = self.selections.newest::<usize>(cx).head();
+ let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) {
+ text_anchor
+ } else {
+ return;
+ };
+
+ let project = workspace.read(cx).project().clone();
+ let definitions = project.update(cx, |project, cx| match kind {
+ GotoDefinitionKind::Symbol => project.definition(&buffer, head, cx),
+ GotoDefinitionKind::Type => project.type_definition(&buffer, head, cx),
+ });
+
+ cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move {
+ let definitions = definitions.await?;
+ editor.update(&mut cx, |editor, cx| {
+ editor.navigate_to_definitions(
+ definitions
+ .into_iter()
+ .map(GoToDefinitionLink::Text)
+ .collect(),
+ split,
+ cx,
+ );
+ })?;
+ Ok::<(), anyhow::Error>(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn navigate_to_definitions(
+ &mut self,
+ mut definitions: Vec<GoToDefinitionLink>,
+ split: bool,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let Some(workspace) = self.workspace(cx) else {
+ return;
+ };
+ let pane = workspace.read(cx).active_pane().clone();
+ // If there is one definition, just open it directly
+ if definitions.len() == 1 {
+ let definition = definitions.pop().unwrap();
+ let target_task = match definition {
+ GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))),
+ GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+ self.compute_target_location(lsp_location, server_id, cx)
+ }
+ };
+ cx.spawn(|editor, mut cx| async move {
+ let target = target_task.await.context("target resolution task")?;
+ if let Some(target) = target {
+ editor.update(&mut cx, |editor, cx| {
+ let range = target.range.to_offset(target.buffer.read(cx));
+ let range = editor.range_for_match(&range);
+ if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([range]);
+ });
+ } else {
+ cx.window_context().defer(move |cx| {
+ let target_editor: ViewHandle<Self> =
+ workspace.update(cx, |workspace, cx| {
+ if split {
+ workspace.split_project_item(target.buffer.clone(), cx)
+ } else {
+ workspace.open_project_item(target.buffer.clone(), cx)
+ }
+ });
+ target_editor.update(cx, |target_editor, cx| {
+ // When selecting a definition in a different buffer, disable the nav history
+ // to avoid creating a history entry at the previous cursor location.
+ pane.update(cx, |pane, _| pane.disable_history());
+ target_editor.change_selections(
+ Some(Autoscroll::fit()),
+ cx,
+ |s| {
+ s.select_ranges([range]);
+ },
+ );
+ pane.update(cx, |pane, _| pane.enable_history());
+ });
+ });
+ }
+ })
+ } else {
+ Ok(())
+ }
+ })
+ .detach_and_log_err(cx);
+ } else if !definitions.is_empty() {
+ let replica_id = self.replica_id(cx);
+ cx.spawn(|editor, mut cx| async move {
+ let (title, location_tasks) = editor
+ .update(&mut cx, |editor, cx| {
+ let title = definitions
+ .iter()
+ .find_map(|definition| match definition {
+ GoToDefinitionLink::Text(link) => {
+ link.origin.as_ref().map(|origin| {
+ let buffer = origin.buffer.read(cx);
+ format!(
+ "Definitions for {}",
+ buffer
+ .text_for_range(origin.range.clone())
+ .collect::<String>()
+ )
+ })
+ }
+ GoToDefinitionLink::InlayHint(_, _) => None,
+ })
+ .unwrap_or("Definitions".to_string());
+ let location_tasks = definitions
+ .into_iter()
+ .map(|definition| match definition {
+ GoToDefinitionLink::Text(link) => {
+ Task::Ready(Some(Ok(Some(link.target))))
+ }
+ GoToDefinitionLink::InlayHint(lsp_location, server_id) => {
+ editor.compute_target_location(lsp_location, server_id, cx)
+ }
+ })
+ .collect::<Vec<_>>();
+ (title, location_tasks)
+ })
+ .context("location tasks preparation")?;
+
+ let locations = futures::future::join_all(location_tasks)
+ .await
+ .into_iter()
+ .filter_map(|location| location.transpose())
+ .collect::<Result<_>>()
+ .context("location tasks")?;
+ workspace.update(&mut cx, |workspace, cx| {
+ Self::open_locations_in_multibuffer(
+ workspace, locations, replica_id, title, split, cx,
+ )
+ });
+
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+
+ fn compute_target_location(
+ &self,
+ lsp_location: lsp::Location,
+ server_id: LanguageServerId,
+ cx: &mut ViewContext<Editor>,
+ ) -> Task<anyhow::Result<Option<Location>>> {
+ let Some(project) = self.project.clone() else {
+ return Task::Ready(Some(Ok(None)));
+ };
+
+ cx.spawn(move |editor, mut cx| async move {
+ let location_task = editor.update(&mut cx, |editor, cx| {
+ project.update(cx, |project, cx| {
+ let language_server_name =
+ editor.buffer.read(cx).as_singleton().and_then(|buffer| {
+ project
+ .language_server_for_buffer(buffer.read(cx), server_id, cx)
+ .map(|(_, lsp_adapter)| {
+ LanguageServerName(Arc::from(lsp_adapter.name()))
+ })
+ });
+ language_server_name.map(|language_server_name| {
+ project.open_local_buffer_via_lsp(
+ lsp_location.uri.clone(),
+ server_id,
+ language_server_name,
+ cx,
+ )
+ })
+ })
+ })?;
+ let location = match location_task {
+ Some(task) => Some({
+ let target_buffer_handle = task.await.context("open local buffer")?;
+ let range = {
+ target_buffer_handle.update(&mut cx, |target_buffer, _| {
+ let target_start = target_buffer.clip_point_utf16(
+ point_from_lsp(lsp_location.range.start),
+ Bias::Left,
+ );
+ let target_end = target_buffer.clip_point_utf16(
+ point_from_lsp(lsp_location.range.end),
+ Bias::Left,
+ );
+ target_buffer.anchor_after(target_start)
+ ..target_buffer.anchor_before(target_end)
+ })
+ };
+ Location {
+ buffer: target_buffer_handle,
+ range,
+ }
+ }),
+ None => None,
+ };
+ Ok(location)
+ })
+ }
+
+ pub fn find_all_references(
+ workspace: &mut Workspace,
+ _: &FindAllReferences,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ let active_item = workspace.active_item(cx)?;
+ let editor_handle = active_item.act_as::<Self>(cx)?;
+
+ let editor = editor_handle.read(cx);
+ let buffer = editor.buffer.read(cx);
+ let head = editor.selections.newest::<usize>(cx).head();
+ let (buffer, head) = buffer.text_anchor_for_position(head, cx)?;
+ let replica_id = editor.replica_id(cx);
+
+ let project = workspace.project().clone();
+ let references = project.update(cx, |project, cx| project.references(&buffer, head, cx));
+ Some(cx.spawn_labeled(
+ "Finding All References...",
+ |workspace, mut cx| async move {
+ let locations = references.await?;
+ if locations.is_empty() {
+ return Ok(());
+ }
+
+ workspace.update(&mut cx, |workspace, cx| {
+ let title = locations
+ .first()
+ .as_ref()
+ .map(|location| {
+ let buffer = location.buffer.read(cx);
+ format!(
+ "References to `{}`",
+ buffer
+ .text_for_range(location.range.clone())
+ .collect::<String>()
+ )
+ })
+ .unwrap();
+ Self::open_locations_in_multibuffer(
+ workspace, locations, replica_id, title, false, cx,
+ );
+ })?;
+
+ Ok(())
+ },
+ ))
+ }
+
+ /// Opens a multibuffer with the given project locations in it
+ pub fn open_locations_in_multibuffer(
+ workspace: &mut Workspace,
+ mut locations: Vec<Location>,
+ replica_id: ReplicaId,
+ title: String,
+ split: bool,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ // If there are multiple definitions, open them in a multibuffer
+ locations.sort_by_key(|location| location.buffer.read(cx).remote_id());
+ let mut locations = locations.into_iter().peekable();
+ let mut ranges_to_highlight = Vec::new();
+
+ let excerpt_buffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(replica_id);
+ while let Some(location) = locations.next() {
+ let buffer = location.buffer.read(cx);
+ let mut ranges_for_buffer = Vec::new();
+ let range = location.range.to_offset(buffer);
+ ranges_for_buffer.push(range.clone());
+
+ while let Some(next_location) = locations.peek() {
+ if next_location.buffer == location.buffer {
+ ranges_for_buffer.push(next_location.range.to_offset(buffer));
+ locations.next();
+ } else {
+ break;
+ }
+ }
+
+ ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end)));
+ ranges_to_highlight.extend(multibuffer.push_excerpts_with_context_lines(
+ location.buffer.clone(),
+ ranges_for_buffer,
+ 1,
+ cx,
+ ))
+ }
+
+ multibuffer.with_title(title)
+ });
+
+ let editor = cx.add_view(|cx| {
+ Editor::for_multibuffer(excerpt_buffer, Some(workspace.project().clone()), cx)
+ });
+ editor.update(cx, |editor, cx| {
+ editor.highlight_background::<Self>(
+ ranges_to_highlight,
+ |theme| theme.editor.highlighted_line_background,
+ cx,
+ );
+ });
+ if split {
+ workspace.split_item(SplitDirection::Right, Box::new(editor), cx);
+ } else {
+ workspace.add_item(Box::new(editor), cx);
+ }
+ }
+
+ pub fn rename(&mut self, _: &Rename, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+ use language::ToOffset as _;
+
+ let project = self.project.clone()?;
+ let selection = self.selections.newest_anchor().clone();
+ let (cursor_buffer, cursor_buffer_position) = self
+ .buffer
+ .read(cx)
+ .text_anchor_for_position(selection.head(), cx)?;
+ let (tail_buffer, _) = self
+ .buffer
+ .read(cx)
+ .text_anchor_for_position(selection.tail(), cx)?;
+ if tail_buffer != cursor_buffer {
+ return None;
+ }
+
+ let snapshot = cursor_buffer.read(cx).snapshot();
+ let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot);
+ let prepare_rename = project.update(cx, |project, cx| {
+ project.prepare_rename(cursor_buffer, cursor_buffer_offset, cx)
+ });
+
+ Some(cx.spawn(|this, mut cx| async move {
+ let rename_range = if let Some(range) = prepare_rename.await? {
+ Some(range)
+ } else {
+ this.update(&mut cx, |this, cx| {
+ let buffer = this.buffer.read(cx).snapshot(cx);
+ let mut buffer_highlights = this
+ .document_highlights_for_position(selection.head(), &buffer)
+ .filter(|highlight| {
+ highlight.start.excerpt_id == selection.head().excerpt_id
+ && highlight.end.excerpt_id == selection.head().excerpt_id
+ });
+ buffer_highlights
+ .next()
+ .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor)
+ })?
+ };
+ if let Some(rename_range) = rename_range {
+ let rename_buffer_range = rename_range.to_offset(&snapshot);
+ let cursor_offset_in_rename_range =
+ cursor_buffer_offset.saturating_sub(rename_buffer_range.start);
+
+ this.update(&mut cx, |this, cx| {
+ this.take_rename(false, cx);
+ let style = this.style(cx);
+ let buffer = this.buffer.read(cx).read(cx);
+ let cursor_offset = selection.head().to_offset(&buffer);
+ let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range);
+ let rename_end = rename_start + rename_buffer_range.len();
+ let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end);
+ let mut old_highlight_id = None;
+ let old_name: Arc<str> = buffer
+ .chunks(rename_start..rename_end, true)
+ .map(|chunk| {
+ if old_highlight_id.is_none() {
+ old_highlight_id = chunk.syntax_highlight_id;
+ }
+ chunk.text
+ })
+ .collect::<String>()
+ .into();
+
+ drop(buffer);
+
+ // Position the selection in the rename editor so that it matches the current selection.
+ this.show_local_selections = false;
+ let rename_editor = cx.add_view(|cx| {
+ let mut editor = Editor::single_line(None, cx);
+ if let Some(old_highlight_id) = old_highlight_id {
+ editor.override_text_style =
+ Some(Box::new(move |style| old_highlight_id.style(&style.syntax)));
+ }
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.edit([(0..0, old_name.clone())], None, cx)
+ });
+ editor.select_all(&SelectAll, cx);
+ editor
+ });
+
+ let ranges = this
+ .clear_background_highlights::<DocumentHighlightWrite>(cx)
+ .into_iter()
+ .flat_map(|(_, ranges)| ranges.into_iter())
+ .chain(
+ this.clear_background_highlights::<DocumentHighlightRead>(cx)
+ .into_iter()
+ .flat_map(|(_, ranges)| ranges.into_iter()),
+ )
+ .collect();
+
+ this.highlight_text::<Rename>(
+ ranges,
+ HighlightStyle {
+ fade_out: Some(style.rename_fade),
+ ..Default::default()
+ },
+ cx,
+ );
+ cx.focus(&rename_editor);
+ let block_id = this.insert_blocks(
+ [BlockProperties {
+ style: BlockStyle::Flex,
+ position: range.start.clone(),
+ height: 1,
+ render: Arc::new({
+ let editor = rename_editor.clone();
+ move |cx: &mut BlockContext| {
+ ChildView::new(&editor, cx)
+ .contained()
+ .with_padding_left(cx.anchor_x)
+ .into_any()
+ }
+ }),
+ disposition: BlockDisposition::Below,
+ }],
+ Some(Autoscroll::fit()),
+ cx,
+ )[0];
+ this.pending_rename = Some(RenameState {
+ range,
+ old_name,
+ editor: rename_editor,
+ block_id,
+ });
+ })?;
+ }
+
+ Ok(())
+ }))
+ }
+
+ pub fn confirm_rename(
+ workspace: &mut Workspace,
+ _: &ConfirmRename,
+ cx: &mut ViewContext<Workspace>,
+ ) -> Option<Task<Result<()>>> {
+ let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
+
+ let (buffer, range, old_name, new_name) = editor.update(cx, |editor, cx| {
+ let rename = editor.take_rename(false, cx)?;
+ let buffer = editor.buffer.read(cx);
+ let (start_buffer, start) =
+ buffer.text_anchor_for_position(rename.range.start.clone(), cx)?;
+ let (end_buffer, end) =
+ buffer.text_anchor_for_position(rename.range.end.clone(), cx)?;
+ if start_buffer == end_buffer {
+ let new_name = rename.editor.read(cx).text(cx);
+ Some((start_buffer, start..end, rename.old_name, new_name))
+ } else {
+ None
+ }
+ })?;
+
+ let rename = workspace.project().clone().update(cx, |project, cx| {
+ project.perform_rename(buffer.clone(), range.start, new_name.clone(), true, cx)
+ });
+
+ let editor = editor.downgrade();
+ Some(cx.spawn(|workspace, mut cx| async move {
+ let project_transaction = rename.await?;
+ Self::open_project_transaction(
+ &editor,
+ workspace,
+ project_transaction,
+ format!("Rename: {} โ {}", old_name, new_name),
+ cx.clone(),
+ )
+ .await?;
+
+ editor.update(&mut cx, |editor, cx| {
+ editor.refresh_document_highlights(cx);
+ })?;
+ Ok(())
+ }))
+ }
+
+ fn take_rename(
+ &mut self,
+ moving_cursor: bool,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<RenameState> {
+ let rename = self.pending_rename.take()?;
+ self.remove_blocks(
+ [rename.block_id].into_iter().collect(),
+ Some(Autoscroll::fit()),
+ cx,
+ );
+ self.clear_highlights::<Rename>(cx);
+ self.show_local_selections = true;
+
+ if moving_cursor {
+ let rename_editor = rename.editor.read(cx);
+ let cursor_in_rename_editor = rename_editor.selections.newest::<usize>(cx).head();
+
+ // Update the selection to match the position of the selection inside
+ // the rename editor.
+ let snapshot = self.buffer.read(cx).read(cx);
+ let rename_range = rename.range.to_offset(&snapshot);
+ let cursor_in_editor = snapshot
+ .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left)
+ .min(rename_range.end);
+ drop(snapshot);
+
+ self.change_selections(None, cx, |s| {
+ s.select_ranges(vec![cursor_in_editor..cursor_in_editor])
+ });
+ } else {
+ self.refresh_document_highlights(cx);
+ }
+
+ Some(rename)
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn pending_rename(&self) -> Option<&RenameState> {
+ self.pending_rename.as_ref()
+ }
+
+ fn format(&mut self, _: &Format, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
+ let project = match &self.project {
+ Some(project) => project.clone(),
+ None => return None,
+ };
+
+ Some(self.perform_format(project, FormatTrigger::Manual, cx))
+ }
+
+ fn perform_format(
+ &mut self,
+ project: ModelHandle<Project>,
+ trigger: FormatTrigger,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ let buffer = self.buffer().clone();
+ let buffers = buffer.read(cx).all_buffers();
+
+ let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
+ let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
+
+ cx.spawn(|_, mut cx| async move {
+ let transaction = futures::select_biased! {
+ _ = timeout => {
+ log::warn!("timed out waiting for formatting");
+ None
+ }
+ transaction = format.log_err().fuse() => transaction,
+ };
+
+ buffer.update(&mut cx, |buffer, cx| {
+ if let Some(transaction) = transaction {
+ if !buffer.is_singleton() {
+ buffer.push_transaction(&transaction.0, cx);
+ }
+ }
+
+ cx.notify();
+ });
+
+ Ok(())
+ })
+ }
+
+ fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext<Self>) {
+ if let Some(project) = self.project.clone() {
+ self.buffer.update(cx, |multi_buffer, cx| {
+ project.update(cx, |project, cx| {
+ project.restart_language_servers_for_buffers(multi_buffer.all_buffers(), cx);
+ });
+ })
+ }
+ }
+
+ fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
+ cx.show_character_palette();
+ }
+
+ fn refresh_active_diagnostics(&mut self, cx: &mut ViewContext<Editor>) {
+ if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
+ let buffer = self.buffer.read(cx).snapshot(cx);
+ let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
+ let is_valid = buffer
+ .diagnostics_in_range::<_, usize>(active_diagnostics.primary_range.clone(), false)
+ .any(|entry| {
+ entry.diagnostic.is_primary
+ && !entry.range.is_empty()
+ && entry.range.start == primary_range_start
+ && entry.diagnostic.message == active_diagnostics.primary_message
+ });
+
+ if is_valid != active_diagnostics.is_valid {
+ active_diagnostics.is_valid = is_valid;
+ let mut new_styles = HashMap::default();
+ for (block_id, diagnostic) in &active_diagnostics.blocks {
+ new_styles.insert(
+ *block_id,
+ diagnostic_block_renderer(diagnostic.clone(), is_valid),
+ );
+ }
+ self.display_map
+ .update(cx, |display_map, _| display_map.replace_blocks(new_styles));
+ }
+ }
+ }
+
+ fn activate_diagnostics(&mut self, group_id: usize, cx: &mut ViewContext<Self>) -> bool {
+ self.dismiss_diagnostics(cx);
+ self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
+ let buffer = self.buffer.read(cx).snapshot(cx);
+
+ let mut primary_range = None;
+ let mut primary_message = None;
+ let mut group_end = Point::zero();
+ let diagnostic_group = buffer
+ .diagnostic_group::<Point>(group_id)
+ .map(|entry| {
+ if entry.range.end > group_end {
+ group_end = entry.range.end;
+ }
+ if entry.diagnostic.is_primary {
+ primary_range = Some(entry.range.clone());
+ primary_message = Some(entry.diagnostic.message.clone());
+ }
+ entry
+ })
+ .collect::<Vec<_>>();
+ let primary_range = primary_range?;
+ let primary_message = primary_message?;
+ let primary_range =
+ buffer.anchor_after(primary_range.start)..buffer.anchor_before(primary_range.end);
+
+ let blocks = display_map
+ .insert_blocks(
+ diagnostic_group.iter().map(|entry| {
+ let diagnostic = entry.diagnostic.clone();
+ let message_height = diagnostic.message.lines().count() as u8;
+ BlockProperties {
+ style: BlockStyle::Fixed,
+ position: buffer.anchor_after(entry.range.start),
+ height: message_height,
+ render: diagnostic_block_renderer(diagnostic, true),
+ disposition: BlockDisposition::Below,
+ }
+ }),
+ cx,
+ )
+ .into_iter()
+ .zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic))
+ .collect();
+
+ Some(ActiveDiagnosticGroup {
+ primary_range,
+ primary_message,
+ blocks,
+ is_valid: true,
+ })
+ });
+ self.active_diagnostics.is_some()
+ }
+
+ fn dismiss_diagnostics(&mut self, cx: &mut ViewContext<Self>) {
+ if let Some(active_diagnostic_group) = self.active_diagnostics.take() {
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx);
+ });
+ cx.notify();
+ }
+ }
+
+ pub fn set_selections_from_remote(
+ &mut self,
+ selections: Vec<Selection<Anchor>>,
+ pending_selection: Option<Selection<Anchor>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let old_cursor_position = self.selections.newest_anchor().head();
+ self.selections.change_with(cx, |s| {
+ s.select_anchors(selections);
+ if let Some(pending_selection) = pending_selection {
+ s.set_pending(pending_selection, SelectMode::Character);
+ } else {
+ s.clear_pending();
+ }
+ });
+ self.selections_did_change(false, &old_cursor_position, cx);
+ }
+
+ fn push_to_selection_history(&mut self) {
+ self.selection_history.push(SelectionHistoryEntry {
+ selections: self.selections.disjoint_anchors(),
+ select_next_state: self.select_next_state.clone(),
+ select_prev_state: self.select_prev_state.clone(),
+ add_selections_state: self.add_selections_state.clone(),
+ });
+ }
+
+ pub fn transact(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ update: impl FnOnce(&mut Self, &mut ViewContext<Self>),
+ ) -> Option<TransactionId> {
+ self.start_transaction_at(Instant::now(), cx);
+ update(self, cx);
+ self.end_transaction_at(Instant::now(), cx)
+ }
+
+ fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext<Self>) {
+ self.end_selection(cx);
+ if let Some(tx_id) = self
+ .buffer
+ .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx))
+ {
+ self.selection_history
+ .insert_transaction(tx_id, self.selections.disjoint_anchors());
+ }
+ }
+
+ fn end_transaction_at(
+ &mut self,
+ now: Instant,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<TransactionId> {
+ if let Some(tx_id) = self
+ .buffer
+ .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx))
+ {
+ if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) {
+ *end_selections = Some(self.selections.disjoint_anchors());
+ } else {
+ error!("unexpectedly ended a transaction that wasn't started by this editor");
+ }
+
+ cx.emit(Event::Edited);
+ Some(tx_id)
+ } else {
+ None
+ }
+ }
+
+ pub fn fold(&mut self, _: &Fold, cx: &mut ViewContext<Self>) {
+ let mut fold_ranges = Vec::new();
+
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ let selections = self.selections.all_adjusted(cx);
+ for selection in selections {
+ let range = selection.range().sorted();
+ let buffer_start_row = range.start.row;
+
+ for row in (0..=range.end.row).rev() {
+ let fold_range = display_map.foldable_range(row);
+
+ if let Some(fold_range) = fold_range {
+ if fold_range.end.row >= buffer_start_row {
+ fold_ranges.push(fold_range);
+ if row <= range.start.row {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ self.fold_ranges(fold_ranges, true, cx);
+ }
+
+ pub fn fold_at(&mut self, fold_at: &FoldAt, cx: &mut ViewContext<Self>) {
+ let buffer_row = fold_at.buffer_row;
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ if let Some(fold_range) = display_map.foldable_range(buffer_row) {
+ let autoscroll = self
+ .selections
+ .all::<Point>(cx)
+ .iter()
+ .any(|selection| fold_range.overlaps(&selection.range()));
+
+ self.fold_ranges(std::iter::once(fold_range), autoscroll, cx);
+ }
+ }
+
+ pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let buffer = &display_map.buffer_snapshot;
+ let selections = self.selections.all::<Point>(cx);
+ let ranges = selections
+ .iter()
+ .map(|s| {
+ let range = s.display_range(&display_map).sorted();
+ let mut start = range.start.to_point(&display_map);
+ let mut end = range.end.to_point(&display_map);
+ start.column = 0;
+ end.column = buffer.line_len(end.row);
+ start..end
+ })
+ .collect::<Vec<_>>();
+
+ self.unfold_ranges(ranges, true, true, cx);
+ }
+
+ pub fn unfold_at(&mut self, unfold_at: &UnfoldAt, cx: &mut ViewContext<Self>) {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ let intersection_range = Point::new(unfold_at.buffer_row, 0)
+ ..Point::new(
+ unfold_at.buffer_row,
+ display_map.buffer_snapshot.line_len(unfold_at.buffer_row),
+ );
+
+ let autoscroll = self
+ .selections
+ .all::<Point>(cx)
+ .iter()
+ .any(|selection| selection.range().overlaps(&intersection_range));
+
+ self.unfold_ranges(std::iter::once(intersection_range), true, autoscroll, cx)
+ }
+
+ pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext<Self>) {
+ let selections = self.selections.all::<Point>(cx);
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let line_mode = self.selections.line_mode;
+ let ranges = selections.into_iter().map(|s| {
+ if line_mode {
+ let start = Point::new(s.start.row, 0);
+ let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row));
+ start..end
+ } else {
+ s.start..s.end
+ }
+ });
+ self.fold_ranges(ranges, true, cx);
+ }
+
+ pub fn fold_ranges<T: ToOffset + Clone>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ auto_scroll: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let mut ranges = ranges.into_iter().peekable();
+ if ranges.peek().is_some() {
+ self.display_map.update(cx, |map, cx| map.fold(ranges, cx));
+
+ if auto_scroll {
+ self.request_autoscroll(Autoscroll::fit(), cx);
+ }
+
+ cx.notify();
+ }
+ }
+
+ pub fn unfold_ranges<T: ToOffset + Clone>(
+ &mut self,
+ ranges: impl IntoIterator<Item = Range<T>>,
+ inclusive: bool,
+ auto_scroll: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let mut ranges = ranges.into_iter().peekable();
+ if ranges.peek().is_some() {
+ self.display_map
+ .update(cx, |map, cx| map.unfold(ranges, inclusive, cx));
+ if auto_scroll {
+ self.request_autoscroll(Autoscroll::fit(), cx);
+ }
+
+ cx.notify();
+ }
+ }
+
+ pub fn gutter_hover(
+ &mut self,
+ GutterHover { hovered }: &GutterHover,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.gutter_hovered = *hovered;
+ cx.notify();
+ }
+
+ pub fn insert_blocks(
+ &mut self,
+ blocks: impl IntoIterator<Item = BlockProperties<Anchor>>,
+ autoscroll: Option<Autoscroll>,
+ cx: &mut ViewContext<Self>,
+ ) -> Vec<BlockId> {
+ let blocks = self
+ .display_map
+ .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx));
+ if let Some(autoscroll) = autoscroll {
+ self.request_autoscroll(autoscroll, cx);
+ }
+ blocks
+ }
+
+ pub fn replace_blocks(
+ &mut self,
+ blocks: HashMap<BlockId, RenderBlock>,
+ autoscroll: Option<Autoscroll>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.display_map
+ .update(cx, |display_map, _| display_map.replace_blocks(blocks));
+ if let Some(autoscroll) = autoscroll {
+ self.request_autoscroll(autoscroll, cx);
+ }
+ }
+
+ pub fn remove_blocks(
+ &mut self,
+ block_ids: HashSet<BlockId>,
+ autoscroll: Option<Autoscroll>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.display_map.update(cx, |display_map, cx| {
+ display_map.remove_blocks(block_ids, cx)
+ });
+ if let Some(autoscroll) = autoscroll {
+ self.request_autoscroll(autoscroll, cx);
+ }
+ }
+
+ pub fn longest_row(&self, cx: &mut AppContext) -> u32 {
+ self.display_map
+ .update(cx, |map, cx| map.snapshot(cx))
+ .longest_row()
+ }
+
+ pub fn max_point(&self, cx: &mut AppContext) -> DisplayPoint {
+ self.display_map
+ .update(cx, |map, cx| map.snapshot(cx))
+ .max_point()
+ }
+
+ pub fn text(&self, cx: &AppContext) -> String {
+ self.buffer.read(cx).read(cx).text()
+ }
+
+ pub fn set_text(&mut self, text: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+ self.transact(cx, |this, cx| {
+ this.buffer
+ .read(cx)
+ .as_singleton()
+ .expect("you can only call set_text on editors for singleton buffers")
+ .update(cx, |buffer, cx| buffer.set_text(text, cx));
+ });
+ }
+
+ pub fn display_text(&self, cx: &mut AppContext) -> String {
+ self.display_map
+ .update(cx, |map, cx| map.snapshot(cx))
+ .text()
+ }
+
+ pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> {
+ let mut wrap_guides = smallvec::smallvec![];
+
+ if self.show_wrap_guides == Some(false) {
+ return wrap_guides;
+ }
+
+ let settings = self.buffer.read(cx).settings_at(0, cx);
+ if settings.show_wrap_guides {
+ if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) {
+ wrap_guides.push((soft_wrap as usize, true));
+ }
+ wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
+ }
+
+ wrap_guides
+ }
+
+ pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap {
+ let settings = self.buffer.read(cx).settings_at(0, cx);
+ let mode = self
+ .soft_wrap_mode_override
+ .unwrap_or_else(|| settings.soft_wrap);
+ match mode {
+ language_settings::SoftWrap::None => SoftWrap::None,
+ language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
+ language_settings::SoftWrap::PreferredLineLength => {
+ SoftWrap::Column(settings.preferred_line_length)
+ }
+ }
+ }
+
+ pub fn set_soft_wrap_mode(
+ &mut self,
+ mode: language_settings::SoftWrap,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.soft_wrap_mode_override = Some(mode);
+ cx.notify();
+ }
+
+ pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut AppContext) -> bool {
+ self.display_map
+ .update(cx, |map, cx| map.set_wrap_width(width, cx))
+ }
+
+ pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, cx: &mut ViewContext<Self>) {
+ if self.soft_wrap_mode_override.is_some() {
+ self.soft_wrap_mode_override.take();
+ } else {
+ let soft_wrap = match self.soft_wrap_mode(cx) {
+ SoftWrap::None => language_settings::SoftWrap::EditorWidth,
+ SoftWrap::EditorWidth | SoftWrap::Column(_) => language_settings::SoftWrap::None,
+ };
+ self.soft_wrap_mode_override = Some(soft_wrap);
+ }
+ cx.notify();
+ }
+
+ pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
+ self.show_gutter = show_gutter;
+ cx.notify();
+ }
+
+ pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext<Self>) {
+ self.show_wrap_guides = Some(show_gutter);
+ cx.notify();
+ }
+
+ pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext<Self>) {
+ if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+ if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
+ cx.reveal_path(&file.abs_path(cx));
+ }
+ }
+ }
+
+ pub fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
+ if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+ if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
+ if let Some(path) = file.abs_path(cx).to_str() {
+ cx.write_to_clipboard(ClipboardItem::new(path.to_string()));
+ }
+ }
+ }
+ }
+
+ pub fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
+ if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+ if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) {
+ if let Some(path) = file.path().to_str() {
+ cx.write_to_clipboard(ClipboardItem::new(path.to_string()));
+ }
+ }
+ }
+ }
+
+ pub fn highlight_rows(&mut self, rows: Option<Range<u32>>) {
+ self.highlighted_rows = rows;
+ }
+
+ pub fn highlighted_rows(&self) -> Option<Range<u32>> {
+ self.highlighted_rows.clone()
+ }
+
+ pub fn highlight_background<T: 'static>(
+ &mut self,
+ ranges: Vec<Range<Anchor>>,
+ color_fetcher: fn(&Theme) -> Color,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.background_highlights
+ .insert(TypeId::of::<T>(), (color_fetcher, ranges));
+ cx.notify();
+ }
+
+ pub fn highlight_inlay_background<T: 'static>(
+ &mut self,
+ ranges: Vec<InlayHighlight>,
+ color_fetcher: fn(&Theme) -> Color,
+ cx: &mut ViewContext<Self>,
+ ) {
+ // TODO: no actual highlights happen for inlays currently, find a way to do that
+ self.inlay_background_highlights
+ .insert(Some(TypeId::of::<T>()), (color_fetcher, ranges));
+ cx.notify();
+ }
+
+ pub fn clear_background_highlights<T: 'static>(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<BackgroundHighlight> {
+ let text_highlights = self.background_highlights.remove(&TypeId::of::<T>());
+ let inlay_highlights = self
+ .inlay_background_highlights
+ .remove(&Some(TypeId::of::<T>()));
+ if text_highlights.is_some() || inlay_highlights.is_some() {
+ cx.notify();
+ }
+ text_highlights
+ }
+
+ #[cfg(feature = "test-support")]
+ pub fn all_text_background_highlights(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> Vec<(Range<DisplayPoint>, Color)> {
+ let snapshot = self.snapshot(cx);
+ let buffer = &snapshot.buffer_snapshot;
+ let start = buffer.anchor_before(0);
+ let end = buffer.anchor_after(buffer.len());
+ let theme = theme::current(cx);
+ self.background_highlights_in_range(start..end, &snapshot, theme.as_ref())
+ }
+
+ fn document_highlights_for_position<'a>(
+ &'a self,
+ position: Anchor,
+ buffer: &'a MultiBufferSnapshot,
+ ) -> impl 'a + Iterator<Item = &Range<Anchor>> {
+ let read_highlights = self
+ .background_highlights
+ .get(&TypeId::of::<DocumentHighlightRead>())
+ .map(|h| &h.1);
+ let write_highlights = self
+ .background_highlights
+ .get(&TypeId::of::<DocumentHighlightWrite>())
+ .map(|h| &h.1);
+ let left_position = position.bias_left(buffer);
+ let right_position = position.bias_right(buffer);
+ read_highlights
+ .into_iter()
+ .chain(write_highlights)
+ .flat_map(move |ranges| {
+ let start_ix = match ranges.binary_search_by(|probe| {
+ let cmp = probe.end.cmp(&left_position, buffer);
+ if cmp.is_ge() {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ }) {
+ Ok(i) | Err(i) => i,
+ };
+
+ let right_position = right_position.clone();
+ ranges[start_ix..]
+ .iter()
+ .take_while(move |range| range.start.cmp(&right_position, buffer).is_le())
+ })
+ }
+
+ pub fn background_highlights_in_range(
+ &self,
+ search_range: Range<Anchor>,
+ display_snapshot: &DisplaySnapshot,
+ theme: &Theme,
+ ) -> Vec<(Range<DisplayPoint>, Color)> {
+ let mut results = Vec::new();
+ for (color_fetcher, ranges) in self.background_highlights.values() {
+ let color = color_fetcher(theme);
+ let start_ix = match ranges.binary_search_by(|probe| {
+ let cmp = probe
+ .end
+ .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
+ if cmp.is_gt() {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ }) {
+ Ok(i) | Err(i) => i,
+ };
+ for range in &ranges[start_ix..] {
+ if range
+ .start
+ .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+ .is_ge()
+ {
+ break;
+ }
+
+ let start = range.start.to_display_point(&display_snapshot);
+ let end = range.end.to_display_point(&display_snapshot);
+ results.push((start..end, color))
+ }
+ }
+ results
+ }
+
+ pub fn background_highlight_row_ranges<T: 'static>(
+ &self,
+ search_range: Range<Anchor>,
+ display_snapshot: &DisplaySnapshot,
+ count: usize,
+ ) -> Vec<RangeInclusive<DisplayPoint>> {
+ let mut results = Vec::new();
+ let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::<T>()) else {
+ return vec![];
+ };
+
+ let start_ix = match ranges.binary_search_by(|probe| {
+ let cmp = probe
+ .end
+ .cmp(&search_range.start, &display_snapshot.buffer_snapshot);
+ if cmp.is_gt() {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ }) {
+ Ok(i) | Err(i) => i,
+ };
+ let mut push_region = |start: Option<Point>, end: Option<Point>| {
+ if let (Some(start_display), Some(end_display)) = (start, end) {
+ results.push(
+ start_display.to_display_point(display_snapshot)
+ ..=end_display.to_display_point(display_snapshot),
+ );
+ }
+ };
+ let mut start_row: Option<Point> = None;
+ let mut end_row: Option<Point> = None;
+ if ranges.len() > count {
+ return Vec::new();
+ }
+ for range in &ranges[start_ix..] {
+ if range
+ .start
+ .cmp(&search_range.end, &display_snapshot.buffer_snapshot)
+ .is_ge()
+ {
+ break;
+ }
+ let end = range.end.to_point(&display_snapshot.buffer_snapshot);
+ if let Some(current_row) = &end_row {
+ if end.row == current_row.row {
+ continue;
+ }
+ }
+ let start = range.start.to_point(&display_snapshot.buffer_snapshot);
+ if start_row.is_none() {
+ assert_eq!(end_row, None);
+ start_row = Some(start);
+ end_row = Some(end);
+ continue;
+ }
+ if let Some(current_end) = end_row.as_mut() {
+ if start.row > current_end.row + 1 {
+ push_region(start_row, end_row);
+ start_row = Some(start);
+ end_row = Some(end);
+ } else {
+ // Merge two hunks.
+ *current_end = end;
+ }
+ } else {
+ unreachable!();
+ }
+ }
+ // We might still have a hunk that was not rendered (if there was a search hit on the last line)
+ push_region(start_row, end_row);
+ results
+ }
+
+ pub fn highlight_text<T: 'static>(
+ &mut self,
+ ranges: Vec<Range<Anchor>>,
+ style: HighlightStyle,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.display_map.update(cx, |map, _| {
+ map.highlight_text(TypeId::of::<T>(), ranges, style)
+ });
+ cx.notify();
+ }
+
+ pub fn highlight_inlays<T: 'static>(
+ &mut self,
+ highlights: Vec<InlayHighlight>,
+ style: HighlightStyle,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.display_map.update(cx, |map, _| {
+ map.highlight_inlays(TypeId::of::<T>(), highlights, style)
+ });
+ cx.notify();
+ }
+
+ pub fn text_highlights<'a, T: 'static>(
+ &'a self,
+ cx: &'a AppContext,
+ ) -> Option<(HighlightStyle, &'a [Range<Anchor>])> {
+ self.display_map.read(cx).text_highlights(TypeId::of::<T>())
+ }
+
+ pub fn clear_highlights<T: 'static>(&mut self, cx: &mut ViewContext<Self>) {
+ let cleared = self
+ .display_map
+ .update(cx, |map, _| map.clear_highlights(TypeId::of::<T>()));
+ if cleared {
+ cx.notify();
+ }
+ }
+
+ pub fn show_local_cursors(&self, cx: &AppContext) -> bool {
+ self.blink_manager.read(cx).visible() && self.focused
+ }
+
+ fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
+ cx.notify();
+ }
+
+ fn on_buffer_event(
+ &mut self,
+ multibuffer: ModelHandle<MultiBuffer>,
+ event: &multi_buffer::Event,
+ cx: &mut ViewContext<Self>,
+ ) {
+ match event {
+ multi_buffer::Event::Edited {
+ sigleton_buffer_edited,
+ } => {
+ self.refresh_active_diagnostics(cx);
+ self.refresh_code_actions(cx);
+ if self.has_active_copilot_suggestion(cx) {
+ self.update_visible_copilot_suggestion(cx);
+ }
+ cx.emit(Event::BufferEdited);
+
+ if *sigleton_buffer_edited {
+ if let Some(project) = &self.project {
+ let project = project.read(cx);
+ let languages_affected = multibuffer
+ .read(cx)
+ .all_buffers()
+ .into_iter()
+ .filter_map(|buffer| {
+ let buffer = buffer.read(cx);
+ let language = buffer.language()?;
+ if project.is_local()
+ && project.language_servers_for_buffer(buffer, cx).count() == 0
+ {
+ None
+ } else {
+ Some(language)
+ }
+ })
+ .cloned()
+ .collect::<HashSet<_>>();
+ if !languages_affected.is_empty() {
+ self.refresh_inlay_hints(
+ InlayHintRefreshReason::BufferEdited(languages_affected),
+ cx,
+ );
+ }
+ }
+ }
+ }
+ multi_buffer::Event::ExcerptsAdded {
+ buffer,
+ predecessor,
+ excerpts,
+ } => {
+ cx.emit(Event::ExcerptsAdded {
+ buffer: buffer.clone(),
+ predecessor: *predecessor,
+ excerpts: excerpts.clone(),
+ });
+ self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+ }
+ multi_buffer::Event::ExcerptsRemoved { ids } => {
+ self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx);
+ cx.emit(Event::ExcerptsRemoved { ids: ids.clone() })
+ }
+ multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed),
+ multi_buffer::Event::DirtyChanged => cx.emit(Event::DirtyChanged),
+ multi_buffer::Event::Saved => cx.emit(Event::Saved),
+ multi_buffer::Event::FileHandleChanged => cx.emit(Event::TitleChanged),
+ multi_buffer::Event::Reloaded => cx.emit(Event::TitleChanged),
+ multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged),
+ multi_buffer::Event::Closed => cx.emit(Event::Closed),
+ multi_buffer::Event::DiagnosticsUpdated => {
+ self.refresh_active_diagnostics(cx);
+ }
+ _ => {}
+ };
+ }
+
+ fn on_display_map_changed(&mut self, _: ModelHandle<DisplayMap>, cx: &mut ViewContext<Self>) {
+ cx.notify();
+ }
+
+ fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
+ self.refresh_copilot_suggestions(true, cx);
+ self.refresh_inlay_hints(
+ InlayHintRefreshReason::SettingsChange(inlay_hint_settings(
+ self.selections.newest_anchor().head(),
+ &self.buffer.read(cx).snapshot(cx),
+ cx,
+ )),
+ cx,
+ );
+ }
+
+ pub fn set_searchable(&mut self, searchable: bool) {
+ self.searchable = searchable;
+ }
+
+ pub fn searchable(&self) -> bool {
+ self.searchable
+ }
+
+ fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext<Workspace>) {
+ let active_item = workspace.active_item(cx);
+ let editor_handle = if let Some(editor) = active_item
+ .as_ref()
+ .and_then(|item| item.act_as::<Self>(cx))
+ {
+ editor
+ } else {
+ cx.propagate_action();
+ return;
+ };
+
+ let editor = editor_handle.read(cx);
+ let buffer = editor.buffer.read(cx);
+ if buffer.is_singleton() {
+ cx.propagate_action();
+ return;
+ }
+
+ let mut new_selections_by_buffer = HashMap::default();
+ for selection in editor.selections.all::<usize>(cx) {
+ for (buffer, mut range, _) in
+ buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
+ {
+ if selection.reversed {
+ mem::swap(&mut range.start, &mut range.end);
+ }
+ new_selections_by_buffer
+ .entry(buffer)
+ .or_insert(Vec::new())
+ .push(range)
+ }
+ }
+
+ editor_handle.update(cx, |editor, cx| {
+ editor.push_to_nav_history(editor.selections.newest_anchor().head(), None, cx);
+ });
+ let pane = workspace.active_pane().clone();
+ pane.update(cx, |pane, _| pane.disable_history());
+
+ // We defer the pane interaction because we ourselves are a workspace item
+ // and activating a new item causes the pane to call a method on us reentrantly,
+ // which panics if we're on the stack.
+ cx.defer(move |workspace, cx| {
+ for (buffer, ranges) in new_selections_by_buffer.into_iter() {
+ let editor = workspace.open_project_item::<Self>(buffer, cx);
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
+ s.select_ranges(ranges);
+ });
+ });
+ }
+
+ pane.update(cx, |pane, _| pane.enable_history());
+ });
+ }
+
+ fn jump(
+ workspace: &mut Workspace,
+ path: ProjectPath,
+ position: Point,
+ anchor: language::Anchor,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let editor = workspace.open_path(path, None, true, cx);
+ cx.spawn(|_, mut cx| async move {
+ let editor = editor
+ .await?
+ .downcast::<Editor>()
+ .ok_or_else(|| anyhow!("opened item was not an editor"))?
+ .downgrade();
+ editor.update(&mut cx, |editor, cx| {
+ let buffer = editor
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?;
+ let buffer = buffer.read(cx);
+ let cursor = if buffer.can_resolve(&anchor) {
+ language::ToPoint::to_point(&anchor, buffer)
+ } else {
+ buffer.clip_point(position, Bias::Left)
+ };
+
+ let nav_history = editor.nav_history.take();
+ editor.change_selections(Some(Autoscroll::newest()), cx, |s| {
+ s.select_ranges([cursor..cursor]);
+ });
+ editor.nav_history = nav_history;
+
+ anyhow::Ok(())
+ })??;
+
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+
+ fn marked_text_ranges(&self, cx: &AppContext) -> Option<Vec<Range<OffsetUtf16>>> {
+ let snapshot = self.buffer.read(cx).read(cx);
+ let (_, ranges) = self.text_highlights::<InputComposition>(cx)?;
+ Some(
+ ranges
+ .iter()
+ .map(move |range| {
+ range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot)
+ })
+ .collect(),
+ )
+ }
+
+ fn selection_replacement_ranges(
+ &self,
+ range: Range<OffsetUtf16>,
+ cx: &AppContext,
+ ) -> Vec<Range<OffsetUtf16>> {
+ let selections = self.selections.all::<OffsetUtf16>(cx);
+ let newest_selection = selections
+ .iter()
+ .max_by_key(|selection| selection.id)
+ .unwrap();
+ let start_delta = range.start.0 as isize - newest_selection.start.0 as isize;
+ let end_delta = range.end.0 as isize - newest_selection.end.0 as isize;
+ let snapshot = self.buffer.read(cx).read(cx);
+ selections
+ .into_iter()
+ .map(|mut selection| {
+ selection.start.0 =
+ (selection.start.0 as isize).saturating_add(start_delta) as usize;
+ selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize;
+ snapshot.clip_offset_utf16(selection.start, Bias::Left)
+ ..snapshot.clip_offset_utf16(selection.end, Bias::Right)
+ })
+ .collect()
+ }
+
+ fn report_copilot_event(
+ &self,
+ suggestion_id: Option<String>,
+ suggestion_accepted: bool,
+ cx: &AppContext,
+ ) {
+ let Some(project) = &self.project else { return };
+
+ // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension
+ let file_extension = self
+ .buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|b| b.read(cx).file())
+ .and_then(|file| Path::new(file.file_name(cx)).extension())
+ .and_then(|e| e.to_str())
+ .map(|a| a.to_string());
+
+ let telemetry = project.read(cx).client().telemetry().clone();
+ let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+
+ let event = ClickhouseEvent::Copilot {
+ suggestion_id,
+ suggestion_accepted,
+ file_extension,
+ };
+ telemetry.report_clickhouse_event(event, telemetry_settings);
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ fn report_editor_event(
+ &self,
+ _operation: &'static str,
+ _file_extension: Option<String>,
+ _cx: &AppContext,
+ ) {
+ }
+
+ #[cfg(not(any(test, feature = "test-support")))]
+ fn report_editor_event(
+ &self,
+ operation: &'static str,
+ file_extension: Option<String>,
+ cx: &AppContext,
+ ) {
+ let Some(project) = &self.project else { return };
+
+ // If None, we are in a file without an extension
+ let file = self
+ .buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|b| b.read(cx).file());
+ let file_extension = file_extension.or(file
+ .as_ref()
+ .and_then(|file| Path::new(file.file_name(cx)).extension())
+ .and_then(|e| e.to_str())
+ .map(|a| a.to_string()));
+
+ let vim_mode = cx
+ .global::<SettingsStore>()
+ .raw_user_settings()
+ .get("vim_mode")
+ == Some(&serde_json::Value::Bool(true));
+ let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+ let copilot_enabled = all_language_settings(file, cx).copilot_enabled(None, None);
+ let copilot_enabled_for_language = self
+ .buffer
+ .read(cx)
+ .settings_at(0, cx)
+ .show_copilot_suggestions;
+
+ let telemetry = project.read(cx).client().telemetry().clone();
+ let event = ClickhouseEvent::Editor {
+ file_extension,
+ vim_mode,
+ operation,
+ copilot_enabled,
+ copilot_enabled_for_language,
+ };
+ telemetry.report_clickhouse_event(event, telemetry_settings)
+ }
+
+ /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines,
+ /// with each line being an array of {text, highlight} objects.
+ fn copy_highlight_json(&mut self, _: &CopyHighlightJson, cx: &mut ViewContext<Self>) {
+ let Some(buffer) = self.buffer.read(cx).as_singleton() else {
+ return;
+ };
+
+ #[derive(Serialize)]
+ struct Chunk<'a> {
+ text: String,
+ highlight: Option<&'a str>,
+ }
+
+ let snapshot = buffer.read(cx).snapshot();
+ let range = self
+ .selected_text_range(cx)
+ .and_then(|selected_range| {
+ if selected_range.is_empty() {
+ None
+ } else {
+ Some(selected_range)
+ }
+ })
+ .unwrap_or_else(|| 0..snapshot.len());
+
+ let chunks = snapshot.chunks(range, true);
+ let mut lines = Vec::new();
+ let mut line: VecDeque<Chunk> = VecDeque::new();
+
+ let theme = &theme::current(cx).editor.syntax;
+
+ for chunk in chunks {
+ let highlight = chunk.syntax_highlight_id.and_then(|id| id.name(theme));
+ let mut chunk_lines = chunk.text.split("\n").peekable();
+ while let Some(text) = chunk_lines.next() {
+ let mut merged_with_last_token = false;
+ if let Some(last_token) = line.back_mut() {
+ if last_token.highlight == highlight {
+ last_token.text.push_str(text);
+ merged_with_last_token = true;
+ }
+ }
+
+ if !merged_with_last_token {
+ line.push_back(Chunk {
+ text: text.into(),
+ highlight,
+ });
+ }
+
+ if chunk_lines.peek().is_some() {
+ if line.len() > 1 && line.front().unwrap().text.is_empty() {
+ line.pop_front();
+ }
+ if line.len() > 1 && line.back().unwrap().text.is_empty() {
+ line.pop_back();
+ }
+
+ lines.push(mem::take(&mut line));
+ }
+ }
+ }
+
+ let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else {
+ return;
+ };
+ cx.write_to_clipboard(ClipboardItem::new(lines));
+ }
+
+ pub fn inlay_hint_cache(&self) -> &InlayHintCache {
+ &self.inlay_hint_cache
+ }
+
+ pub fn replay_insert_event(
+ &mut self,
+ text: &str,
+ relative_utf16_range: Option<Range<isize>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if !self.input_enabled {
+ cx.emit(Event::InputIgnored { text: text.into() });
+ return;
+ }
+ if let Some(relative_utf16_range) = relative_utf16_range {
+ let selections = self.selections.all::<OffsetUtf16>(cx);
+ self.change_selections(None, cx, |s| {
+ let new_ranges = selections.into_iter().map(|range| {
+ let start = OffsetUtf16(
+ range
+ .head()
+ .0
+ .saturating_add_signed(relative_utf16_range.start),
+ );
+ let end = OffsetUtf16(
+ range
+ .head()
+ .0
+ .saturating_add_signed(relative_utf16_range.end),
+ );
+ start..end
+ });
+ s.select_ranges(new_ranges);
+ });
+ }
+
+ self.handle_input(text, cx);
+ }
+
+ pub fn supports_inlay_hints(&self, cx: &AppContext) -> bool {
+ let Some(project) = self.project.as_ref() else {
+ return false;
+ };
+ let project = project.read(cx);
+
+ let mut supports = false;
+ self.buffer().read(cx).for_each_buffer(|buffer| {
+ if !supports {
+ supports = project
+ .language_servers_for_buffer(buffer.read(cx), cx)
+ .any(
+ |(_, server)| match server.capabilities().inlay_hint_provider {
+ Some(lsp::OneOf::Left(enabled)) => enabled,
+ Some(lsp::OneOf::Right(_)) => true,
+ None => false,
+ },
+ )
+ }
+ });
+ supports
+ }
+}
+
+pub trait CollaborationHub {
+ fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator>;
+ fn user_participant_indices<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> &'a HashMap<u64, ParticipantIndex>;
+}
+
+impl CollaborationHub for ModelHandle<Project> {
+ fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap<PeerId, Collaborator> {
+ self.read(cx).collaborators()
+ }
+
+ fn user_participant_indices<'a>(
+ &self,
+ cx: &'a AppContext,
+ ) -> &'a HashMap<u64, ParticipantIndex> {
+ self.read(cx).user_store().read(cx).participant_indices()
+ }
+}
+
+fn inlay_hint_settings(
+ location: Anchor,
+ snapshot: &MultiBufferSnapshot,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) -> InlayHintSettings {
+ let file = snapshot.file_at(location);
+ let language = snapshot.language_at(location);
+ let settings = all_language_settings(file, cx);
+ settings
+ .language(language.map(|l| l.name()).as_deref())
+ .inlay_hints
+}
+
+fn consume_contiguous_rows(
+ contiguous_row_selections: &mut Vec<Selection<Point>>,
+ selection: &Selection<Point>,
+ display_map: &DisplaySnapshot,
+ selections: &mut std::iter::Peekable<std::slice::Iter<Selection<Point>>>,
+) -> (u32, u32) {
+ contiguous_row_selections.push(selection.clone());
+ let start_row = selection.start.row;
+ let mut end_row = ending_row(selection, display_map);
+
+ while let Some(next_selection) = selections.peek() {
+ if next_selection.start.row <= end_row {
+ end_row = ending_row(next_selection, display_map);
+ contiguous_row_selections.push(selections.next().unwrap().clone());
+ } else {
+ break;
+ }
+ }
+ (start_row, end_row)
+}
+
+fn ending_row(next_selection: &Selection<Point>, display_map: &DisplaySnapshot) -> u32 {
+ if next_selection.end.column > 0 || next_selection.is_empty() {
+ display_map.next_line_boundary(next_selection.end).0.row + 1
+ } else {
+ next_selection.end.row
+ }
+}
+
+impl EditorSnapshot {
+ pub fn remote_selections_in_range<'a>(
+ &'a self,
+ range: &'a Range<Anchor>,
+ collaboration_hub: &dyn CollaborationHub,
+ cx: &'a AppContext,
+ ) -> impl 'a + Iterator<Item = RemoteSelection> {
+ let participant_indices = collaboration_hub.user_participant_indices(cx);
+ let collaborators_by_peer_id = collaboration_hub.collaborators(cx);
+ let collaborators_by_replica_id = collaborators_by_peer_id
+ .iter()
+ .map(|(_, collaborator)| (collaborator.replica_id, collaborator))
+ .collect::<HashMap<_, _>>();
+ self.buffer_snapshot
+ .remote_selections_in_range(range)
+ .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| {
+ let collaborator = collaborators_by_replica_id.get(&replica_id)?;
+ let participant_index = participant_indices.get(&collaborator.user_id).copied();
+ Some(RemoteSelection {
+ replica_id,
+ selection,
+ cursor_shape,
+ line_mode,
+ participant_index,
+ peer_id: collaborator.peer_id,
+ })
+ })
+ }
+
+ pub fn language_at<T: ToOffset>(&self, position: T) -> Option<&Arc<Language>> {
+ self.display_snapshot.buffer_snapshot.language_at(position)
+ }
+
+ pub fn is_focused(&self) -> bool {
+ self.is_focused
+ }
+
+ pub fn placeholder_text(&self) -> Option<&Arc<str>> {
+ self.placeholder_text.as_ref()
+ }
+
+ pub fn scroll_position(&self) -> Vector2F {
+ self.scroll_anchor.scroll_position(&self.display_snapshot)
+ }
+}
+
+impl Deref for EditorSnapshot {
+ type Target = DisplaySnapshot;
+
+ fn deref(&self) -> &Self::Target {
+ &self.display_snapshot
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum Event {
+ InputIgnored {
+ text: Arc<str>,
+ },
+ InputHandled {
+ utf16_range_to_replace: Option<Range<isize>>,
+ text: Arc<str>,
+ },
+ ExcerptsAdded {
+ buffer: ModelHandle<Buffer>,
+ predecessor: ExcerptId,
+ excerpts: Vec<(ExcerptId, ExcerptRange<language::Anchor>)>,
+ },
+ ExcerptsRemoved {
+ ids: Vec<ExcerptId>,
+ },
+ BufferEdited,
+ Edited,
+ Reparsed,
+ Focused,
+ Blurred,
+ DirtyChanged,
+ Saved,
+ TitleChanged,
+ DiffBaseChanged,
+ SelectionsChanged {
+ local: bool,
+ },
+ ScrollPositionChanged {
+ local: bool,
+ autoscroll: bool,
+ },
+ Closed,
+}
+
+pub struct EditorFocused(pub ViewHandle<Editor>);
+pub struct EditorBlurred(pub ViewHandle<Editor>);
+pub struct EditorReleased(pub WeakViewHandle<Editor>);
+
+impl Entity for Editor {
+ type Event = Event;
+
+ fn release(&mut self, cx: &mut AppContext) {
+ cx.emit_global(EditorReleased(self.handle.clone()));
+ }
+}
+
+impl View for Editor {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ let style = self.style(cx);
+ let font_changed = self.display_map.update(cx, |map, cx| {
+ map.set_fold_ellipses_color(style.folds.ellipses.text_color);
+ map.set_font(style.text.font_id, style.text.font_size, cx)
+ });
+
+ if font_changed {
+ cx.defer(move |editor, cx: &mut ViewContext<Editor>| {
+ hide_hover(editor, cx);
+ hide_link_definition(editor, cx);
+ });
+ }
+
+ Stack::new()
+ .with_child(EditorElement::new(style.clone()))
+ .with_child(ChildView::new(&self.mouse_context_menu, cx))
+ .into_any()
+ }
+
+ fn ui_name() -> &'static str {
+ "Editor"
+ }
+
+ fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ if cx.is_self_focused() {
+ let focused_event = EditorFocused(cx.handle());
+ cx.emit(Event::Focused);
+ cx.emit_global(focused_event);
+ }
+ if let Some(rename) = self.pending_rename.as_ref() {
+ cx.focus(&rename.editor);
+ } else if cx.is_self_focused() || !focused.is::<Editor>() {
+ if !self.focused {
+ self.blink_manager.update(cx, BlinkManager::enable);
+ }
+ self.focused = true;
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.finalize_last_transaction(cx);
+ if self.leader_peer_id.is_none() {
+ buffer.set_active_selections(
+ &self.selections.disjoint_anchors(),
+ self.selections.line_mode,
+ self.cursor_shape,
+ cx,
+ );
+ }
+ });
+ }
+ }
+
+ fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
+ let blurred_event = EditorBlurred(cx.handle());
+ cx.emit_global(blurred_event);
+ self.focused = false;
+ self.blink_manager.update(cx, BlinkManager::disable);
+ self.buffer
+ .update(cx, |buffer, cx| buffer.remove_active_selections(cx));
+ self.hide_context_menu(cx);
+ hide_hover(self, cx);
+ cx.emit(Event::Blurred);
+ cx.notify();
+ }
+
+ fn modifiers_changed(
+ &mut self,
+ event: &gpui::platform::ModifiersChangedEvent,
+ cx: &mut ViewContext<Self>,
+ ) -> bool {
+ let pending_selection = self.has_pending_selection();
+
+ if let Some(point) = &self.link_go_to_definition_state.last_trigger_point {
+ if event.cmd && !pending_selection {
+ let point = point.clone();
+ let snapshot = self.snapshot(cx);
+ let kind = point.definition_kind(event.shift);
+
+ show_link_definition(kind, self, point, snapshot, cx);
+ return false;
+ }
+ }
+
+ {
+ if self.link_go_to_definition_state.symbol_range.is_some()
+ || !self.link_go_to_definition_state.definitions.is_empty()
+ {
+ self.link_go_to_definition_state.symbol_range.take();
+ self.link_go_to_definition_state.definitions.clear();
+ cx.notify();
+ }
+
+ self.link_go_to_definition_state.task = None;
+
+ self.clear_highlights::<LinkGoToDefinitionState>(cx);
+ }
+
+ false
+ }
+
+ fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) {
+ Self::reset_to_default_keymap_context(keymap);
+ let mode = match self.mode {
+ EditorMode::SingleLine => "single_line",
+ EditorMode::AutoHeight { .. } => "auto_height",
+ EditorMode::Full => "full",
+ };
+ keymap.add_key("mode", mode);
+ if self.pending_rename.is_some() {
+ keymap.add_identifier("renaming");
+ }
+ if self.context_menu_visible() {
+ match self.context_menu.read().as_ref() {
+ Some(ContextMenu::Completions(_)) => {
+ keymap.add_identifier("menu");
+ keymap.add_identifier("showing_completions")
+ }
+ Some(ContextMenu::CodeActions(_)) => {
+ keymap.add_identifier("menu");
+ keymap.add_identifier("showing_code_actions")
+ }
+ None => {}
+ }
+ }
+
+ for layer in self.keymap_context_layers.values() {
+ keymap.extend(layer);
+ }
+
+ if let Some(extension) = self
+ .buffer
+ .read(cx)
+ .as_singleton()
+ .and_then(|buffer| buffer.read(cx).file()?.path().extension()?.to_str())
+ {
+ keymap.add_key("extension", extension.to_string());
+ }
+ }
+
+ fn text_for_range(&self, range_utf16: Range<usize>, cx: &AppContext) -> Option<String> {
+ Some(
+ self.buffer
+ .read(cx)
+ .read(cx)
+ .text_for_range(OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end))
+ .collect(),
+ )
+ }
+
+ fn selected_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+ // Prevent the IME menu from appearing when holding down an alphabetic key
+ // while input is disabled.
+ if !self.input_enabled {
+ return None;
+ }
+
+ let range = self.selections.newest::<OffsetUtf16>(cx).range();
+ Some(range.start.0..range.end.0)
+ }
+
+ fn marked_text_range(&self, cx: &AppContext) -> Option<Range<usize>> {
+ let snapshot = self.buffer.read(cx).read(cx);
+ let range = self.text_highlights::<InputComposition>(cx)?.1.get(0)?;
+ Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0)
+ }
+
+ fn unmark_text(&mut self, cx: &mut ViewContext<Self>) {
+ self.clear_highlights::<InputComposition>(cx);
+ self.ime_transaction.take();
+ }
+
+ fn replace_text_in_range(
+ &mut self,
+ range_utf16: Option<Range<usize>>,
+ text: &str,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if !self.input_enabled {
+ cx.emit(Event::InputIgnored { text: text.into() });
+ return;
+ }
+
+ self.transact(cx, |this, cx| {
+ let new_selected_ranges = if let Some(range_utf16) = range_utf16 {
+ let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+ Some(this.selection_replacement_ranges(range_utf16, cx))
+ } else {
+ this.marked_text_ranges(cx)
+ };
+
+ let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| {
+ let newest_selection_id = this.selections.newest_anchor().id;
+ this.selections
+ .all::<OffsetUtf16>(cx)
+ .iter()
+ .zip(ranges_to_replace.iter())
+ .find_map(|(selection, range)| {
+ if selection.id == newest_selection_id {
+ Some(
+ (range.start.0 as isize - selection.head().0 as isize)
+ ..(range.end.0 as isize - selection.head().0 as isize),
+ )
+ } else {
+ None
+ }
+ })
+ });
+
+ cx.emit(Event::InputHandled {
+ utf16_range_to_replace: range_to_replace,
+ text: text.into(),
+ });
+
+ if let Some(new_selected_ranges) = new_selected_ranges {
+ this.change_selections(None, cx, |selections| {
+ selections.select_ranges(new_selected_ranges)
+ });
+ }
+
+ this.handle_input(text, cx);
+ });
+
+ if let Some(transaction) = self.ime_transaction {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.group_until_transaction(transaction, cx);
+ });
+ }
+
+ self.unmark_text(cx);
+ }
+
+ fn replace_and_mark_text_in_range(
+ &mut self,
+ range_utf16: Option<Range<usize>>,
+ text: &str,
+ new_selected_range_utf16: Option<Range<usize>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if !self.input_enabled {
+ cx.emit(Event::InputIgnored { text: text.into() });
+ return;
+ }
+
+ let transaction = self.transact(cx, |this, cx| {
+ let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) {
+ let snapshot = this.buffer.read(cx).read(cx);
+ if let Some(relative_range_utf16) = range_utf16.as_ref() {
+ for marked_range in &mut marked_ranges {
+ marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end;
+ marked_range.start.0 += relative_range_utf16.start;
+ marked_range.start =
+ snapshot.clip_offset_utf16(marked_range.start, Bias::Left);
+ marked_range.end =
+ snapshot.clip_offset_utf16(marked_range.end, Bias::Right);
+ }
+ }
+ Some(marked_ranges)
+ } else if let Some(range_utf16) = range_utf16 {
+ let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end);
+ Some(this.selection_replacement_ranges(range_utf16, cx))
+ } else {
+ None
+ };
+
+ let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| {
+ let newest_selection_id = this.selections.newest_anchor().id;
+ this.selections
+ .all::<OffsetUtf16>(cx)
+ .iter()
+ .zip(ranges_to_replace.iter())
+ .find_map(|(selection, range)| {
+ if selection.id == newest_selection_id {
+ Some(
+ (range.start.0 as isize - selection.head().0 as isize)
+ ..(range.end.0 as isize - selection.head().0 as isize),
+ )
+ } else {
+ None
+ }
+ })
+ });
+
+ cx.emit(Event::InputHandled {
+ utf16_range_to_replace: range_to_replace,
+ text: text.into(),
+ });
+
+ if let Some(ranges) = ranges_to_replace {
+ this.change_selections(None, cx, |s| s.select_ranges(ranges));
+ }
+
+ let marked_ranges = {
+ let snapshot = this.buffer.read(cx).read(cx);
+ this.selections
+ .disjoint_anchors()
+ .iter()
+ .map(|selection| {
+ selection.start.bias_left(&*snapshot)..selection.end.bias_right(&*snapshot)
+ })
+ .collect::<Vec<_>>()
+ };
+
+ if text.is_empty() {
+ this.unmark_text(cx);
+ } else {
+ this.highlight_text::<InputComposition>(
+ marked_ranges.clone(),
+ this.style(cx).composition_mark,
+ cx,
+ );
+ }
+
+ this.handle_input(text, cx);
+
+ if let Some(new_selected_range) = new_selected_range_utf16 {
+ let snapshot = this.buffer.read(cx).read(cx);
+ let new_selected_ranges = marked_ranges
+ .into_iter()
+ .map(|marked_range| {
+ let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0;
+ let new_start = OffsetUtf16(new_selected_range.start + insertion_start);
+ let new_end = OffsetUtf16(new_selected_range.end + insertion_start);
+ snapshot.clip_offset_utf16(new_start, Bias::Left)
+ ..snapshot.clip_offset_utf16(new_end, Bias::Right)
+ })
+ .collect::<Vec<_>>();
+
+ drop(snapshot);
+ this.change_selections(None, cx, |selections| {
+ selections.select_ranges(new_selected_ranges)
+ });
+ }
+ });
+
+ self.ime_transaction = self.ime_transaction.or(transaction);
+ if let Some(transaction) = self.ime_transaction {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.group_until_transaction(transaction, cx);
+ });
+ }
+
+ if self.text_highlights::<InputComposition>(cx).is_none() {
+ self.ime_transaction.take();
+ }
+ }
+}
+
+fn build_style(
+ settings: &ThemeSettings,
+ get_field_editor_theme: Option<&GetFieldEditorTheme>,
+ override_text_style: Option<&OverrideTextStyle>,
+ cx: &AppContext,
+) -> EditorStyle {
+ let font_cache = cx.font_cache();
+ let line_height_scalar = settings.line_height();
+ let theme_id = settings.theme.meta.id;
+ let mut theme = settings.theme.editor.clone();
+ let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme {
+ let field_editor_theme = get_field_editor_theme(&settings.theme);
+ theme.text_color = field_editor_theme.text.color;
+ theme.selection = field_editor_theme.selection;
+ theme.background = field_editor_theme
+ .container
+ .background_color
+ .unwrap_or_default();
+ EditorStyle {
+ text: field_editor_theme.text,
+ placeholder_text: field_editor_theme.placeholder_text,
+ line_height_scalar,
+ theme,
+ theme_id,
+ }
+ } else {
+ let font_family_id = settings.buffer_font_family;
+ let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
+ let font_properties = Default::default();
+ let font_id = font_cache
+ .select_font(font_family_id, &font_properties)
+ .unwrap();
+ let font_size = settings.buffer_font_size(cx);
+ EditorStyle {
+ text: TextStyle {
+ color: settings.theme.editor.text_color,
+ font_family_name,
+ font_family_id,
+ font_id,
+ font_size,
+ font_properties,
+ underline: Default::default(),
+ soft_wrap: false,
+ },
+ placeholder_text: None,
+ line_height_scalar,
+ theme,
+ theme_id,
+ }
+ };
+
+ if let Some(highlight_style) = override_text_style.and_then(|build_style| build_style(&style)) {
+ if let Some(highlighted) = style
+ .text
+ .clone()
+ .highlight(highlight_style, font_cache)
+ .log_err()
+ {
+ style.text = highlighted;
+ }
+ }
+
+ style
+}
+
+trait SelectionExt {
+ fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize>;
+ fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point>;
+ fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint>;
+ fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot)
+ -> Range<u32>;
+}
+
+impl<T: ToPoint + ToOffset> SelectionExt for Selection<T> {
+ fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range<Point> {
+ let start = self.start.to_point(buffer);
+ let end = self.end.to_point(buffer);
+ if self.reversed {
+ end..start
+ } else {
+ start..end
+ }
+ }
+
+ fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range<usize> {
+ let start = self.start.to_offset(buffer);
+ let end = self.end.to_offset(buffer);
+ if self.reversed {
+ end..start
+ } else {
+ start..end
+ }
+ }
+
+ fn display_range(&self, map: &DisplaySnapshot) -> Range<DisplayPoint> {
+ let start = self
+ .start
+ .to_point(&map.buffer_snapshot)
+ .to_display_point(map);
+ let end = self
+ .end
+ .to_point(&map.buffer_snapshot)
+ .to_display_point(map);
+ if self.reversed {
+ end..start
+ } else {
+ start..end
+ }
+ }
+
+ fn spanned_rows(
+ &self,
+ include_end_if_at_line_start: bool,
+ map: &DisplaySnapshot,
+ ) -> Range<u32> {
+ let start = self.start.to_point(&map.buffer_snapshot);
+ let mut end = self.end.to_point(&map.buffer_snapshot);
+ if !include_end_if_at_line_start && start.row != end.row && end.column == 0 {
+ end.row -= 1;
+ }
+
+ let buffer_start = map.prev_line_boundary(start).0;
+ let buffer_end = map.next_line_boundary(end).0;
+ buffer_start.row..buffer_end.row + 1
+ }
+}
+
+impl<T: InvalidationRegion> InvalidationStack<T> {
+ fn invalidate<S>(&mut self, selections: &[Selection<S>], buffer: &MultiBufferSnapshot)
+ where
+ S: Clone + ToOffset,
+ {
+ while let Some(region) = self.last() {
+ let all_selections_inside_invalidation_ranges =
+ if selections.len() == region.ranges().len() {
+ selections
+ .iter()
+ .zip(region.ranges().iter().map(|r| r.to_offset(buffer)))
+ .all(|(selection, invalidation_range)| {
+ let head = selection.head().to_offset(buffer);
+ invalidation_range.start <= head && invalidation_range.end >= head
+ })
+ } else {
+ false
+ };
+
+ if all_selections_inside_invalidation_ranges {
+ break;
+ } else {
+ self.pop();
+ }
+ }
+ }
+}
+
+impl<T> Default for InvalidationStack<T> {
+ fn default() -> Self {
+ Self(Default::default())
+ }
+}
+
+impl<T> Deref for InvalidationStack<T> {
+ type Target = Vec<T>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl<T> DerefMut for InvalidationStack<T> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.0
+ }
+}
+
+impl InvalidationRegion for SnippetState {
+ fn ranges(&self) -> &[Range<Anchor>] {
+ &self.ranges[self.active_index]
+ }
+}
+
+impl Deref for EditorStyle {
+ type Target = theme::Editor;
+
+ fn deref(&self) -> &Self::Target {
+ &self.theme
+ }
+}
+
+pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> RenderBlock {
+ let mut highlighted_lines = Vec::new();
+
+ for (index, line) in diagnostic.message.lines().enumerate() {
+ let line = match &diagnostic.source {
+ Some(source) if index == 0 => {
+ let source_highlight = Vec::from_iter(0..source.len());
+ highlight_diagnostic_message(source_highlight, &format!("{source}: {line}"))
+ }
+
+ _ => highlight_diagnostic_message(Vec::new(), line),
+ };
+ highlighted_lines.push(line);
+ }
+ let message = diagnostic.message;
+ Arc::new(move |cx: &mut BlockContext| {
+ let message = message.clone();
+ let settings = settings::get::<ThemeSettings>(cx);
+ let tooltip_style = settings.theme.tooltip.clone();
+ let theme = &settings.theme.editor;
+ let style = diagnostic_style(diagnostic.severity, is_valid, theme);
+ let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round();
+ let anchor_x = cx.anchor_x;
+ enum BlockContextToolip {}
+ MouseEventHandler::new::<BlockContext, _>(cx.block_id, cx, |_, _| {
+ Flex::column()
+ .with_children(highlighted_lines.iter().map(|(line, highlights)| {
+ Label::new(
+ line.clone(),
+ style.message.clone().with_font_size(font_size),
+ )
+ .with_highlights(highlights.clone())
+ .contained()
+ .with_margin_left(anchor_x)
+ }))
+ .aligned()
+ .left()
+ .into_any()
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, _, cx| {
+ cx.write_to_clipboard(ClipboardItem::new(message.clone()));
+ })
+ // We really need to rethink this ID system...
+ .with_tooltip::<BlockContextToolip>(
+ cx.block_id,
+ "Copy diagnostic message",
+ None,
+ tooltip_style,
+ cx,
+ )
+ .into_any()
+ })
+}
+
+pub fn highlight_diagnostic_message(
+ initial_highlights: Vec<usize>,
+ message: &str,
+) -> (String, Vec<usize>) {
+ let mut message_without_backticks = String::new();
+ let mut prev_offset = 0;
+ let mut inside_block = false;
+ let mut highlights = initial_highlights;
+ for (match_ix, (offset, _)) in message
+ .match_indices('`')
+ .chain([(message.len(), "")])
+ .enumerate()
+ {
+ message_without_backticks.push_str(&message[prev_offset..offset]);
+ if inside_block {
+ highlights.extend(prev_offset - match_ix..offset - match_ix);
+ }
+
+ inside_block = !inside_block;
+ prev_offset = offset + 1;
+ }
+
+ (message_without_backticks, highlights)
+}
+
+pub fn diagnostic_style(
+ severity: DiagnosticSeverity,
+ valid: bool,
+ theme: &theme::Editor,
+) -> DiagnosticStyle {
+ match (severity, valid) {
+ (DiagnosticSeverity::ERROR, true) => theme.error_diagnostic.clone(),
+ (DiagnosticSeverity::ERROR, false) => theme.invalid_error_diagnostic.clone(),
+ (DiagnosticSeverity::WARNING, true) => theme.warning_diagnostic.clone(),
+ (DiagnosticSeverity::WARNING, false) => theme.invalid_warning_diagnostic.clone(),
+ (DiagnosticSeverity::INFORMATION, true) => theme.information_diagnostic.clone(),
+ (DiagnosticSeverity::INFORMATION, false) => theme.invalid_information_diagnostic.clone(),
+ (DiagnosticSeverity::HINT, true) => theme.hint_diagnostic.clone(),
+ (DiagnosticSeverity::HINT, false) => theme.invalid_hint_diagnostic.clone(),
+ _ => theme.invalid_hint_diagnostic.clone(),
+ }
+}
+
+pub fn combine_syntax_and_fuzzy_match_highlights(
+ text: &str,
+ default_style: HighlightStyle,
+ syntax_ranges: impl Iterator<Item = (Range<usize>, HighlightStyle)>,
+ match_indices: &[usize],
+) -> Vec<(Range<usize>, HighlightStyle)> {
+ let mut result = Vec::new();
+ let mut match_indices = match_indices.iter().copied().peekable();
+
+ for (range, mut syntax_highlight) in syntax_ranges.chain([(usize::MAX..0, Default::default())])
+ {
+ syntax_highlight.weight = None;
+
+ // Add highlights for any fuzzy match characters before the next
+ // syntax highlight range.
+ while let Some(&match_index) = match_indices.peek() {
+ if match_index >= range.start {
+ break;
+ }
+ match_indices.next();
+ let end_index = char_ix_after(match_index, text);
+ let mut match_style = default_style;
+ match_style.weight = Some(fonts::Weight::BOLD);
+ result.push((match_index..end_index, match_style));
+ }
+
+ if range.start == usize::MAX {
+ break;
+ }
+
+ // Add highlights for any fuzzy match characters within the
+ // syntax highlight range.
+ let mut offset = range.start;
+ while let Some(&match_index) = match_indices.peek() {
+ if match_index >= range.end {
+ break;
+ }
+
+ match_indices.next();
+ if match_index > offset {
+ result.push((offset..match_index, syntax_highlight));
+ }
+
+ let mut end_index = char_ix_after(match_index, text);
+ while let Some(&next_match_index) = match_indices.peek() {
+ if next_match_index == end_index && next_match_index < range.end {
+ end_index = char_ix_after(next_match_index, text);
+ match_indices.next();
+ } else {
+ break;
+ }
+ }
+
+ let mut match_style = syntax_highlight;
+ match_style.weight = Some(fonts::Weight::BOLD);
+ result.push((match_index..end_index, match_style));
+ offset = end_index;
+ }
+
+ if offset < range.end {
+ result.push((offset..range.end, syntax_highlight));
+ }
+ }
+
+ fn char_ix_after(ix: usize, text: &str) -> usize {
+ ix + text[ix..].chars().next().unwrap().len_utf8()
+ }
+
+ result
+}
+
+pub fn styled_runs_for_code_label<'a>(
+ label: &'a CodeLabel,
+ syntax_theme: &'a theme::SyntaxTheme,
+) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> {
+ let fade_out = HighlightStyle {
+ fade_out: Some(0.35),
+ ..Default::default()
+ };
+
+ let mut prev_end = label.filter_range.end;
+ label
+ .runs
+ .iter()
+ .enumerate()
+ .flat_map(move |(ix, (range, highlight_id))| {
+ let style = if let Some(style) = highlight_id.style(syntax_theme) {
+ style
+ } else {
+ return Default::default();
+ };
+ let mut muted_style = style;
+ muted_style.highlight(fade_out);
+
+ let mut runs = SmallVec::<[(Range<usize>, HighlightStyle); 3]>::new();
+ if range.start >= label.filter_range.end {
+ if range.start > prev_end {
+ runs.push((prev_end..range.start, fade_out));
+ }
+ runs.push((range.clone(), muted_style));
+ } else if range.end <= label.filter_range.end {
+ runs.push((range.clone(), style));
+ } else {
+ runs.push((range.start..label.filter_range.end, style));
+ runs.push((label.filter_range.end..range.end, muted_style));
+ }
+ prev_end = cmp::max(prev_end, range.end);
+
+ if ix + 1 == label.runs.len() && label.text.len() > prev_end {
+ runs.push((prev_end..label.text.len(), fade_out));
+ }
+
+ runs
+ })
+}
+
+pub fn split_words<'a>(text: &'a str) -> impl std::iter::Iterator<Item = &'a str> + 'a {
+ let mut index = 0;
+ let mut codepoints = text.char_indices().peekable();
+
+ std::iter::from_fn(move || {
+ let start_index = index;
+ while let Some((new_index, codepoint)) = codepoints.next() {
+ index = new_index + codepoint.len_utf8();
+ let current_upper = codepoint.is_uppercase();
+ let next_upper = codepoints
+ .peek()
+ .map(|(_, c)| c.is_uppercase())
+ .unwrap_or(false);
+
+ if !current_upper && next_upper {
+ return Some(&text[start_index..index]);
+ }
+ }
+
+ index = text.len();
+ if start_index < text.len() {
+ return Some(&text[start_index..]);
+ }
+ None
+ })
+ .flat_map(|word| word.split_inclusive('_'))
+ .flat_map(|word| word.split_inclusive('-'))
+}
+
+trait RangeToAnchorExt {
+ fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor>;
+}
+
+impl<T: ToOffset> RangeToAnchorExt for Range<T> {
+ fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range<Anchor> {
+ snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end)
+ }
+}
@@ -0,0 +1,62 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[derive(Deserialize)]
+pub struct EditorSettings {
+ pub cursor_blink: bool,
+ pub hover_popover_enabled: bool,
+ pub show_completions_on_input: bool,
+ pub show_completion_documentation: bool,
+ pub use_on_type_format: bool,
+ pub scrollbar: Scrollbar,
+ pub relative_line_numbers: bool,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct Scrollbar {
+ pub show: ShowScrollbar,
+ pub git_diff: bool,
+ pub selections: bool,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ShowScrollbar {
+ Auto,
+ System,
+ Always,
+ Never,
+}
+
+#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
+pub struct EditorSettingsContent {
+ pub cursor_blink: Option<bool>,
+ pub hover_popover_enabled: Option<bool>,
+ pub show_completions_on_input: Option<bool>,
+ pub show_completion_documentation: Option<bool>,
+ pub use_on_type_format: Option<bool>,
+ pub scrollbar: Option<ScrollbarContent>,
+ pub relative_line_numbers: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+pub struct ScrollbarContent {
+ pub show: Option<ShowScrollbar>,
+ pub git_diff: Option<bool>,
+ pub selections: Option<bool>,
+}
+
+impl Setting for EditorSettings {
+ const KEY: Option<&'static str> = None;
+
+ type FileContent = EditorSettingsContent;
+
+ fn load(
+ default_value: &Self::FileContent,
+ user_values: &[&Self::FileContent],
+ _: &gpui::AppContext,
+ ) -> anyhow::Result<Self> {
+ Self::load_via_json_merge(default_value, user_values)
+ }
+}
@@ -0,0 +1,8195 @@
+use super::*;
+use crate::{
+ scroll::scroll_amount::ScrollAmount,
+ test::{
+ assert_text_with_selections, build_editor, editor_lsp_test_context::EditorLspTestContext,
+ editor_test_context::EditorTestContext, select_ranges,
+ },
+ JoinLines,
+};
+use drag_and_drop::DragAndDrop;
+use futures::StreamExt;
+use gpui::{
+ executor::Deterministic,
+ geometry::{rect::RectF, vector::vec2f},
+ platform::{WindowBounds, WindowOptions},
+ serde_json::{self, json},
+ TestAppContext,
+};
+use indoc::indoc;
+use language::{
+ language_settings::{AllLanguageSettings, AllLanguageSettingsContent, LanguageSettingsContent},
+ BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageRegistry,
+ Override, Point,
+};
+use parking_lot::Mutex;
+use project::project_settings::{LspSettings, ProjectSettings};
+use project::FakeFs;
+use std::sync::atomic;
+use std::sync::atomic::AtomicUsize;
+use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
+use unindent::Unindent;
+use util::{
+ assert_set_eq,
+ test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker},
+};
+use workspace::{
+ item::{FollowableItem, Item, ItemHandle},
+ NavigationEntry, ViewId,
+};
+
+#[gpui::test]
+fn test_edit_events(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = cx.add_model(|cx| {
+ let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456");
+ buffer.set_group_interval(Duration::from_secs(1));
+ buffer
+ });
+
+ let events = Rc::new(RefCell::new(Vec::new()));
+ let editor1 = cx
+ .add_window({
+ let events = events.clone();
+ |cx| {
+ cx.subscribe(&cx.handle(), move |_, _, event, _| {
+ if matches!(
+ event,
+ Event::Edited | Event::BufferEdited | Event::DirtyChanged
+ ) {
+ events.borrow_mut().push(("editor1", event.clone()));
+ }
+ })
+ .detach();
+ Editor::for_buffer(buffer.clone(), None, cx)
+ }
+ })
+ .root(cx);
+ let editor2 = cx
+ .add_window({
+ let events = events.clone();
+ |cx| {
+ cx.subscribe(&cx.handle(), move |_, _, event, _| {
+ if matches!(
+ event,
+ Event::Edited | Event::BufferEdited | Event::DirtyChanged
+ ) {
+ events.borrow_mut().push(("editor2", event.clone()));
+ }
+ })
+ .detach();
+ Editor::for_buffer(buffer.clone(), None, cx)
+ }
+ })
+ .root(cx);
+ assert_eq!(mem::take(&mut *events.borrow_mut()), []);
+
+ // Mutating editor 1 will emit an `Edited` event only for that editor.
+ editor1.update(cx, |editor, cx| editor.insert("X", cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor1", Event::Edited),
+ ("editor1", Event::BufferEdited),
+ ("editor2", Event::BufferEdited),
+ ("editor1", Event::DirtyChanged),
+ ("editor2", Event::DirtyChanged)
+ ]
+ );
+
+ // Mutating editor 2 will emit an `Edited` event only for that editor.
+ editor2.update(cx, |editor, cx| editor.delete(&Delete, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor2", Event::Edited),
+ ("editor1", Event::BufferEdited),
+ ("editor2", Event::BufferEdited),
+ ]
+ );
+
+ // Undoing on editor 1 will emit an `Edited` event only for that editor.
+ editor1.update(cx, |editor, cx| editor.undo(&Undo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor1", Event::Edited),
+ ("editor1", Event::BufferEdited),
+ ("editor2", Event::BufferEdited),
+ ("editor1", Event::DirtyChanged),
+ ("editor2", Event::DirtyChanged),
+ ]
+ );
+
+ // Redoing on editor 1 will emit an `Edited` event only for that editor.
+ editor1.update(cx, |editor, cx| editor.redo(&Redo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor1", Event::Edited),
+ ("editor1", Event::BufferEdited),
+ ("editor2", Event::BufferEdited),
+ ("editor1", Event::DirtyChanged),
+ ("editor2", Event::DirtyChanged),
+ ]
+ );
+
+ // Undoing on editor 2 will emit an `Edited` event only for that editor.
+ editor2.update(cx, |editor, cx| editor.undo(&Undo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor2", Event::Edited),
+ ("editor1", Event::BufferEdited),
+ ("editor2", Event::BufferEdited),
+ ("editor1", Event::DirtyChanged),
+ ("editor2", Event::DirtyChanged),
+ ]
+ );
+
+ // Redoing on editor 2 will emit an `Edited` event only for that editor.
+ editor2.update(cx, |editor, cx| editor.redo(&Redo, cx));
+ assert_eq!(
+ mem::take(&mut *events.borrow_mut()),
+ [
+ ("editor2", Event::Edited),
+ ("editor1", Event::BufferEdited),
+ ("editor2", Event::BufferEdited),
+ ("editor1", Event::DirtyChanged),
+ ("editor2", Event::DirtyChanged),
+ ]
+ );
+
+ // No event is emitted when the mutation is a no-op.
+ editor2.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
+
+ editor.backspace(&Backspace, cx);
+ });
+ assert_eq!(mem::take(&mut *events.borrow_mut()), []);
+}
+
+#[gpui::test]
+fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut now = Instant::now();
+ let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456"));
+ let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval());
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let editor = cx
+ .add_window(|cx| build_editor(buffer.clone(), cx))
+ .root(cx);
+
+ editor.update(cx, |editor, cx| {
+ editor.start_transaction_at(now, cx);
+ editor.change_selections(None, cx, |s| s.select_ranges([2..4]));
+
+ editor.insert("cd", cx);
+ editor.end_transaction_at(now, cx);
+ assert_eq!(editor.text(cx), "12cd56");
+ assert_eq!(editor.selections.ranges(cx), vec![4..4]);
+
+ editor.start_transaction_at(now, cx);
+ editor.change_selections(None, cx, |s| s.select_ranges([4..5]));
+ editor.insert("e", cx);
+ editor.end_transaction_at(now, cx);
+ assert_eq!(editor.text(cx), "12cde6");
+ assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+
+ now += group_interval + Duration::from_millis(1);
+ editor.change_selections(None, cx, |s| s.select_ranges([2..2]));
+
+ // Simulate an edit in another editor
+ buffer.update(cx, |buffer, cx| {
+ buffer.start_transaction_at(now, cx);
+ buffer.edit([(0..1, "a")], None, cx);
+ buffer.edit([(1..1, "b")], None, cx);
+ buffer.end_transaction_at(now, cx);
+ });
+
+ assert_eq!(editor.text(cx), "ab2cde6");
+ assert_eq!(editor.selections.ranges(cx), vec![3..3]);
+
+ // Last transaction happened past the group interval in a different editor.
+ // Undo it individually and don't restore selections.
+ editor.undo(&Undo, cx);
+ assert_eq!(editor.text(cx), "12cde6");
+ assert_eq!(editor.selections.ranges(cx), vec![2..2]);
+
+ // First two transactions happened within the group interval in this editor.
+ // Undo them together and restore selections.
+ editor.undo(&Undo, cx);
+ editor.undo(&Undo, cx); // Undo stack is empty here, so this is a no-op.
+ assert_eq!(editor.text(cx), "123456");
+ assert_eq!(editor.selections.ranges(cx), vec![0..0]);
+
+ // Redo the first two transactions together.
+ editor.redo(&Redo, cx);
+ assert_eq!(editor.text(cx), "12cde6");
+ assert_eq!(editor.selections.ranges(cx), vec![5..5]);
+
+ // Redo the last transaction on its own.
+ editor.redo(&Redo, cx);
+ assert_eq!(editor.text(cx), "ab2cde6");
+ assert_eq!(editor.selections.ranges(cx), vec![6..6]);
+
+ // Test empty transactions.
+ editor.start_transaction_at(now, cx);
+ editor.end_transaction_at(now, cx);
+ editor.undo(&Undo, cx);
+ assert_eq!(editor.text(cx), "12cde6");
+ });
+}
+
+#[gpui::test]
+fn test_ime_composition(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = cx.add_model(|cx| {
+ let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde");
+ // Ensure automatic grouping doesn't occur.
+ buffer.set_group_interval(Duration::ZERO);
+ buffer
+ });
+
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ cx.add_window(|cx| {
+ let mut editor = build_editor(buffer.clone(), cx);
+
+ // Start a new IME composition.
+ editor.replace_and_mark_text_in_range(Some(0..1), "ร ", None, cx);
+ editor.replace_and_mark_text_in_range(Some(0..1), "รก", None, cx);
+ editor.replace_and_mark_text_in_range(Some(0..1), "รค", None, cx);
+ assert_eq!(editor.text(cx), "รคbcde");
+ assert_eq!(
+ editor.marked_text_ranges(cx),
+ Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+ );
+
+ // Finalize IME composition.
+ editor.replace_text_in_range(None, "ฤ", cx);
+ assert_eq!(editor.text(cx), "ฤbcde");
+ assert_eq!(editor.marked_text_ranges(cx), None);
+
+ // IME composition edits are grouped and are undone/redone at once.
+ editor.undo(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "abcde");
+ assert_eq!(editor.marked_text_ranges(cx), None);
+ editor.redo(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "ฤbcde");
+ assert_eq!(editor.marked_text_ranges(cx), None);
+
+ // Start a new IME composition.
+ editor.replace_and_mark_text_in_range(Some(0..1), "ร ", None, cx);
+ assert_eq!(
+ editor.marked_text_ranges(cx),
+ Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
+ );
+
+ // Undoing during an IME composition cancels it.
+ editor.undo(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "ฤbcde");
+ assert_eq!(editor.marked_text_ranges(cx), None);
+
+ // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
+ editor.replace_and_mark_text_in_range(Some(4..999), "รจ", None, cx);
+ assert_eq!(editor.text(cx), "ฤbcdรจ");
+ assert_eq!(
+ editor.marked_text_ranges(cx),
+ Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
+ );
+
+ // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
+ editor.replace_text_in_range(Some(4..999), "ฤ", cx);
+ assert_eq!(editor.text(cx), "ฤbcdฤ");
+ assert_eq!(editor.marked_text_ranges(cx), None);
+
+ // Start a new IME composition with multiple cursors.
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([
+ OffsetUtf16(1)..OffsetUtf16(1),
+ OffsetUtf16(3)..OffsetUtf16(3),
+ OffsetUtf16(5)..OffsetUtf16(5),
+ ])
+ });
+ editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, cx);
+ assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
+ assert_eq!(
+ editor.marked_text_ranges(cx),
+ Some(vec![
+ OffsetUtf16(0)..OffsetUtf16(3),
+ OffsetUtf16(4)..OffsetUtf16(7),
+ OffsetUtf16(8)..OffsetUtf16(11)
+ ])
+ );
+
+ // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
+ editor.replace_and_mark_text_in_range(Some(1..2), "1", None, cx);
+ assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
+ assert_eq!(
+ editor.marked_text_ranges(cx),
+ Some(vec![
+ OffsetUtf16(1)..OffsetUtf16(2),
+ OffsetUtf16(5)..OffsetUtf16(6),
+ OffsetUtf16(9)..OffsetUtf16(10)
+ ])
+ );
+
+ // Finalize IME composition with multiple cursors.
+ editor.replace_text_in_range(Some(9..10), "2", cx);
+ assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
+ assert_eq!(editor.marked_text_ranges(cx), None);
+
+ editor
+ });
+}
+
+#[gpui::test]
+fn test_selection_with_mouse(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ editor.update(cx, |view, cx| {
+ view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
+ });
+ assert_eq!(
+ editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+ );
+
+ editor.update(cx, |view, cx| {
+ view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx);
+ });
+
+ assert_eq!(
+ editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+ );
+
+ editor.update(cx, |view, cx| {
+ view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx);
+ });
+
+ assert_eq!(
+ editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+ );
+
+ editor.update(cx, |view, cx| {
+ view.end_selection(cx);
+ view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx);
+ });
+
+ assert_eq!(
+ editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ [DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1)]
+ );
+
+ editor.update(cx, |view, cx| {
+ view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx);
+ view.update_selection(DisplayPoint::new(0, 0), 0, Vector2F::zero(), cx);
+ });
+
+ assert_eq!(
+ editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ [
+ DisplayPoint::new(2, 2)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)
+ ]
+ );
+
+ editor.update(cx, |view, cx| {
+ view.end_selection(cx);
+ });
+
+ assert_eq!(
+ editor.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ [DisplayPoint::new(3, 3)..DisplayPoint::new(0, 0)]
+ );
+}
+
+#[gpui::test]
+fn test_canceling_pending_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+
+ view.update(cx, |view, cx| {
+ view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.update_selection(DisplayPoint::new(3, 3), 0, Vector2F::zero(), cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.cancel(&Cancel, cx);
+ view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_clone(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let (text, selection_ranges) = marked_text_ranges(
+ indoc! {"
+ one
+ two
+ threeห
+ four
+ fiveห
+ "},
+ true,
+ );
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&text, cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone()));
+ editor.fold_ranges(
+ [
+ Point::new(1, 0)..Point::new(2, 0),
+ Point::new(3, 0)..Point::new(4, 0),
+ ],
+ true,
+ cx,
+ );
+ });
+
+ let cloned_editor = editor
+ .update(cx, |editor, cx| {
+ cx.add_window(Default::default(), |cx| editor.clone(cx))
+ })
+ .root(cx);
+
+ let snapshot = editor.update(cx, |e, cx| e.snapshot(cx));
+ let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx));
+
+ assert_eq!(
+ cloned_editor.update(cx, |e, cx| e.display_text(cx)),
+ editor.update(cx, |e, cx| e.display_text(cx))
+ );
+ assert_eq!(
+ cloned_snapshot
+ .folds_in_range(0..text.len())
+ .collect::<Vec<_>>(),
+ snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
+ );
+ assert_set_eq!(
+ cloned_editor.read_with(cx, |editor, cx| editor.selections.ranges::<Point>(cx)),
+ editor.read_with(cx, |editor, cx| editor.selections.ranges(cx))
+ );
+ assert_set_eq!(
+ cloned_editor.update(cx, |e, cx| e.selections.display_ranges(cx)),
+ editor.update(cx, |e, cx| e.selections.display_ranges(cx))
+ );
+}
+
+#[gpui::test]
+async fn test_navigation_history(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ cx.set_global(DragAndDrop::<Workspace>::default());
+ use workspace::item::Item;
+
+ let fs = FakeFs::new(cx.background());
+ let project = Project::test(fs, [], cx).await;
+ let window = cx.add_window(|cx| Workspace::test_new(project, cx));
+ let workspace = window.root(cx);
+ let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+ window.add_view(cx, |cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
+ let mut editor = build_editor(buffer.clone(), cx);
+ let handle = cx.handle();
+ editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
+
+ fn pop_history(editor: &mut Editor, cx: &mut WindowContext) -> Option<NavigationEntry> {
+ editor.nav_history.as_mut().unwrap().pop_backward(cx)
+ }
+
+ // Move the cursor a small distance.
+ // Nothing is added to the navigation history.
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)])
+ });
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)])
+ });
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a large distance.
+ // The history can jump back to the previous position.
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)])
+ });
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(nav_entry.item.id(), cx.view_id());
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a small distance via the mouse.
+ // Nothing is added to the navigation history.
+ editor.begin_selection(DisplayPoint::new(5, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Move the cursor a large distance via the mouse.
+ // The history can jump back to the previous position.
+ editor.begin_selection(DisplayPoint::new(15, 0), false, 1, cx);
+ editor.end_selection(cx);
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)]
+ );
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(nav_entry.item.id(), cx.view_id());
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)]
+ );
+ assert!(pop_history(&mut editor, cx).is_none());
+
+ // Set scroll position to check later
+ editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
+ let original_scroll_position = editor.scroll_manager.anchor();
+
+ // Jump to the end of the document and adjust scroll
+ editor.move_to_end(&MoveToEnd, cx);
+ editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx);
+ assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
+
+ let nav_entry = pop_history(&mut editor, cx).unwrap();
+ editor.navigate(nav_entry.data.unwrap(), cx);
+ assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
+
+ // Ensure we don't panic when navigation data contains invalid anchors *and* points.
+ let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
+ invalid_anchor.text_anchor.buffer_id = Some(999);
+ let invalid_point = Point::new(9999, 0);
+ editor.navigate(
+ Box::new(NavigationData {
+ cursor_anchor: invalid_anchor,
+ cursor_position: invalid_point,
+ scroll_anchor: ScrollAnchor {
+ anchor: invalid_anchor,
+ offset: Default::default(),
+ },
+ scroll_top_row: invalid_point.row,
+ }),
+ cx,
+ );
+ assert_eq!(
+ editor.selections.display_ranges(cx),
+ &[editor.max_point(cx)..editor.max_point(cx)]
+ );
+ assert_eq!(
+ editor.scroll_position(cx),
+ vec2f(0., editor.max_point(cx).row() as f32)
+ );
+
+ editor
+ });
+}
+
+#[gpui::test]
+fn test_cancel(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+
+ view.update(cx, |view, cx| {
+ view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx);
+ view.update_selection(DisplayPoint::new(1, 1), 0, Vector2F::zero(), cx);
+ view.end_selection(cx);
+
+ view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx);
+ view.update_selection(DisplayPoint::new(0, 3), 0, Vector2F::zero(), cx);
+ view.end_selection(cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.cancel(&Cancel, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [DisplayPoint::new(3, 4)..DisplayPoint::new(1, 1)]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.cancel(&Cancel, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1)]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_fold_action(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(
+ &"
+ impl Foo {
+ // Hello!
+
+ fn a() {
+ 1
+ }
+
+ fn b() {
+ 2
+ }
+
+ fn c() {
+ 3
+ }
+ }
+ "
+ .unindent(),
+ cx,
+ );
+ build_editor(buffer.clone(), cx)
+ })
+ .root(cx);
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(8, 0)..DisplayPoint::new(12, 0)]);
+ });
+ view.fold(&Fold, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "
+ impl Foo {
+ // Hello!
+
+ fn a() {
+ 1
+ }
+
+ fn b() {โฏ
+ }
+
+ fn c() {โฏ
+ }
+ }
+ "
+ .unindent(),
+ );
+
+ view.fold(&Fold, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "
+ impl Foo {โฏ
+ }
+ "
+ .unindent(),
+ );
+
+ view.unfold_lines(&UnfoldLines, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "
+ impl Foo {
+ // Hello!
+
+ fn a() {
+ 1
+ }
+
+ fn b() {โฏ
+ }
+
+ fn c() {โฏ
+ }
+ }
+ "
+ .unindent(),
+ );
+
+ view.unfold_lines(&UnfoldLines, cx);
+ assert_eq!(view.display_text(cx), view.buffer.read(cx).read(cx).text());
+ });
+}
+
+#[gpui::test]
+fn test_move_cursor(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
+ let view = cx
+ .add_window(|cx| build_editor(buffer.clone(), cx))
+ .root(cx);
+
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ vec![
+ (Point::new(1, 0)..Point::new(1, 0), "\t"),
+ (Point::new(1, 1)..Point::new(1, 1), "\t"),
+ ],
+ None,
+ cx,
+ );
+ });
+ view.update(cx, |view, cx| {
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+ );
+
+ view.move_right(&MoveRight, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4)]
+ );
+
+ view.move_left(&MoveLeft, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)]
+ );
+
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+ );
+
+ view.move_to_end(&MoveToEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(5, 6)..DisplayPoint::new(5, 6)]
+ );
+
+ view.move_to_beginning(&MoveToBeginning, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)]
+ );
+
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(0, 2)]);
+ });
+ view.select_to_beginning(&SelectToBeginning, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(0, 1)..DisplayPoint::new(0, 0)]
+ );
+
+ view.select_to_end(&SelectToEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(0, 1)..DisplayPoint::new(5, 6)]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("โโโโโ\nabcde\nฮฑฮฒฮณฮดฮต", cx);
+ build_editor(buffer.clone(), cx)
+ })
+ .root(cx);
+
+ assert_eq!('โ'.len_utf8(), 3);
+ assert_eq!('ฮฑ'.len_utf8(), 2);
+
+ view.update(cx, |view, cx| {
+ view.fold_ranges(
+ vec![
+ Point::new(0, 6)..Point::new(0, 12),
+ Point::new(1, 2)..Point::new(1, 4),
+ Point::new(2, 4)..Point::new(2, 8),
+ ],
+ true,
+ cx,
+ );
+ assert_eq!(view.display_text(cx), "โโโฏโ\nabโฏe\nฮฑฮฒโฏฮต");
+
+ view.move_right(&MoveRight, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(0, "โ".len())]
+ );
+ view.move_right(&MoveRight, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(0, "โโ".len())]
+ );
+ view.move_right(&MoveRight, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(0, "โโโฏ".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "abโฏe".len())]
+ );
+ view.move_left(&MoveLeft, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "abโฏ".len())]
+ );
+ view.move_left(&MoveLeft, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "ab".len())]
+ );
+ view.move_left(&MoveLeft, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "a".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "ฮฑ".len())]
+ );
+ view.move_right(&MoveRight, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "ฮฑฮฒ".len())]
+ );
+ view.move_right(&MoveRight, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "ฮฑฮฒโฏ".len())]
+ );
+ view.move_right(&MoveRight, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "ฮฑฮฒโฏฮต".len())]
+ );
+
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "abโฏe".len())]
+ );
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "ฮฑฮฒโฏฮต".len())]
+ );
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "abโฏe".len())]
+ );
+
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(0, "โโ".len())]
+ );
+ view.move_left(&MoveLeft, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(0, "โ".len())]
+ );
+ view.move_left(&MoveLeft, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(0, "".len())]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("โโโโโ\nabcd\nฮฑฮฒฮณ\nabcd\nโโโโโ\n", cx);
+ build_editor(buffer.clone(), cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([empty_range(0, "โโโโโ".len())]);
+ });
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(1, "abcd".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "ฮฑฮฒฮณ".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(3, "abcd".len())]
+ );
+
+ view.move_down(&MoveDown, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(4, "โโโโโ".len())]
+ );
+
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(3, "abcd".len())]
+ );
+
+ view.move_up(&MoveUp, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[empty_range(2, "ฮฑฮฒฮณ".len())]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_beginning_end_of_line(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("abc\n def", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
+ ]);
+ });
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_to_beginning_of_line(&MoveToBeginningOfLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_to_end_of_line(&MoveToEndOfLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+ ]
+ );
+ });
+
+ // Moving to the end of line again is a no-op.
+ view.update(cx, |view, cx| {
+ view.move_to_end_of_line(&MoveToEndOfLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_left(&MoveLeft, cx);
+ view.select_to_beginning_of_line(
+ &SelectToBeginningOfLine {
+ stop_at_soft_wraps: true,
+ },
+ cx,
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.select_to_beginning_of_line(
+ &SelectToBeginningOfLine {
+ stop_at_soft_wraps: true,
+ },
+ cx,
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 0),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.select_to_beginning_of_line(
+ &SelectToBeginningOfLine {
+ stop_at_soft_wraps: true,
+ },
+ cx,
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 2),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.select_to_end_of_line(
+ &SelectToEndOfLine {
+ stop_at_soft_wraps: true,
+ },
+ cx,
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 5),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.delete_to_end_of_line(&DeleteToEndOfLine, cx);
+ assert_eq!(view.display_text(cx), "ab\n de");
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 4),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
+ assert_eq!(view.display_text(cx), "\n");
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11),
+ DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4),
+ ])
+ });
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_selection_ranges("use std::หstr::{foo, bar}\n\n {หbaz.qux()}", view, cx);
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_selection_ranges("use stdห::str::{foo, bar}\n\n ห{baz.qux()}", view, cx);
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_selection_ranges("use หstd::str::{foo, bar}\n\nห {baz.qux()}", view, cx);
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_selection_ranges("หuse std::str::{foo, bar}\nห\n {baz.qux()}", view, cx);
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_selection_ranges("หuse std::str::{foo, barห}\n\n {baz.qux()}", view, cx);
+
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_selection_ranges("useห std::str::{foo, bar}ห\n\n {baz.qux()}", view, cx);
+
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_selection_ranges("use stdห::str::{foo, bar}\nห\n {baz.qux()}", view, cx);
+
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_selection_ranges("use std::หstr::{foo, bar}\n\n {หbaz.qux()}", view, cx);
+
+ view.move_right(&MoveRight, cx);
+ view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
+ assert_selection_ranges("use std::ยซหsยปtr::{foo, bar}\n\n {ยซหbยปaz.qux()}", view, cx);
+
+ view.select_to_previous_word_start(&SelectToPreviousWordStart, cx);
+ assert_selection_ranges("use stdยซห::sยปtr::{foo, bar}\n\n ยซห{bยปaz.qux()}", view, cx);
+
+ view.select_to_next_word_end(&SelectToNextWordEnd, cx);
+ assert_selection_ranges("use std::ยซหsยปtr::{foo, bar}\n\n {ยซหbยปaz.qux()}", view, cx);
+ });
+}
+
+#[gpui::test]
+fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer =
+ MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+
+ view.update(cx, |view, cx| {
+ view.set_wrap_width(Some(140.), cx);
+ assert_eq!(
+ view.display_text(cx),
+ "use one::{\n two::three::\n four::five\n};"
+ );
+
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)]);
+ });
+
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)]
+ );
+
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+ );
+
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+ );
+
+ view.move_to_next_word_end(&MoveToNextWordEnd, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)]
+ );
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)]
+ );
+
+ view.move_to_previous_word_start(&MoveToPreviousWordStart, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+ let window = cx.window;
+ window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
+
+ cx.set_state(
+ &r#"หone
+ two
+
+ three
+ fourห
+ five
+
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+ ห
+ three
+ four
+ five
+ ห
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+ ห
+ sixห"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+
+ sixห"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+
+ three
+ four
+ five
+ ห
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"one
+ two
+ ห
+ three
+ four
+ five
+
+ six"#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, cx));
+ cx.assert_editor_state(
+ &r#"หone
+ two
+
+ three
+ four
+ five
+
+ six"#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+ let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+ let window = cx.window;
+ window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx);
+
+ cx.set_state(
+ &r#"หone
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#,
+ );
+
+ cx.update_editor(|editor, cx| {
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.));
+ editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+ editor.scroll_screen(&ScrollAmount::Page(1.), cx);
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 6.));
+ editor.scroll_screen(&ScrollAmount::Page(-1.), cx);
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+
+ editor.scroll_screen(&ScrollAmount::Page(-0.5), cx);
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.));
+ editor.scroll_screen(&ScrollAmount::Page(0.5), cx);
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.));
+ });
+}
+
+#[gpui::test]
+async fn test_autoscroll(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let line_height = cx.update_editor(|editor, cx| {
+ editor.set_vertical_scroll_margin(2, cx);
+ editor.style(cx).text.line_height(cx.font_cache())
+ });
+
+ let window = cx.window;
+ window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx);
+
+ cx.set_state(
+ &r#"หone
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#,
+ );
+ cx.update_editor(|editor, cx| {
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0));
+ });
+
+ // Add a cursor below the visible area. Since both cursors cannot fit
+ // on screen, the editor autoscrolls to reveal the newest cursor, and
+ // allows the vertical scroll margin below that cursor.
+ cx.update_editor(|editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+ selections.select_ranges([
+ Point::new(0, 0)..Point::new(0, 0),
+ Point::new(6, 0)..Point::new(6, 0),
+ ]);
+ })
+ });
+ cx.update_editor(|editor, cx| {
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0));
+ });
+
+ // Move down. The editor cursor scrolls down to track the newest cursor.
+ cx.update_editor(|editor, cx| {
+ editor.move_down(&Default::default(), cx);
+ });
+ cx.update_editor(|editor, cx| {
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0));
+ });
+
+ // Add a cursor above the visible area. Since both cursors fit on screen,
+ // the editor scrolls to show both.
+ cx.update_editor(|editor, cx| {
+ editor.change_selections(Some(Autoscroll::fit()), cx, |selections| {
+ selections.select_ranges([
+ Point::new(1, 0)..Point::new(1, 0),
+ Point::new(6, 0)..Point::new(6, 0),
+ ]);
+ })
+ });
+ cx.update_editor(|editor, cx| {
+ assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0));
+ });
+}
+
+#[gpui::test]
+async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+ let window = cx.window;
+ window.simulate_resize(vec2f(100., 4. * line_height), &mut cx);
+
+ cx.set_state(
+ &r#"
+ หone
+ two
+ threeห
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ หfour
+ five
+ sixห
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_down(&MovePageDown::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ four
+ five
+ six
+ หseven
+ eight
+ nineห
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ หfour
+ five
+ sixห
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.move_page_up(&MovePageUp::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ หone
+ two
+ threeห
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ ten
+ "#
+ .unindent(),
+ );
+
+ // Test select collapsing
+ cx.update_editor(|editor, cx| {
+ editor.move_page_down(&MovePageDown::default(), cx);
+ editor.move_page_down(&MovePageDown::default(), cx);
+ editor.move_page_down(&MovePageDown::default(), cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ one
+ two
+ three
+ four
+ five
+ six
+ seven
+ eight
+ nine
+ หten
+ ห"#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state("one ยซtwo threeหยป four");
+ cx.update_editor(|editor, cx| {
+ editor.delete_to_beginning_of_line(&DeleteToBeginningOfLine, cx);
+ assert_eq!(editor.text(cx), " four");
+ });
+}
+
+#[gpui::test]
+fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("one two three four", cx);
+ build_editor(buffer.clone(), cx)
+ })
+ .root(cx);
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ // an empty selection - the preceding word fragment is deleted
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ // characters selected - they are deleted
+ DisplayPoint::new(0, 9)..DisplayPoint::new(0, 12),
+ ])
+ });
+ view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx);
+ assert_eq!(view.buffer.read(cx).read(cx).text(), "e two te four");
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ // an empty selection - the following word fragment is deleted
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+ // characters selected - they are deleted
+ DisplayPoint::new(0, 9)..DisplayPoint::new(0, 10),
+ ])
+ });
+ view.delete_to_next_word_end(&DeleteToNextWordEnd, cx);
+ assert_eq!(view.buffer.read(cx).read(cx).text(), "e t te our");
+ });
+}
+
+#[gpui::test]
+fn test_newline(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
+ build_editor(buffer.clone(), cx)
+ })
+ .root(cx);
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+ DisplayPoint::new(1, 6)..DisplayPoint::new(1, 6),
+ ])
+ });
+
+ view.newline(&Newline, cx);
+ assert_eq!(view.text(cx), "aa\naa\n \n bb\n bb\n");
+ });
+}
+
+#[gpui::test]
+fn test_newline_with_old_selections(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(
+ "
+ a
+ b(
+ X
+ )
+ c(
+ X
+ )
+ "
+ .unindent()
+ .as_str(),
+ cx,
+ );
+ let mut editor = build_editor(buffer.clone(), cx);
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([
+ Point::new(2, 4)..Point::new(2, 5),
+ Point::new(5, 4)..Point::new(5, 5),
+ ])
+ });
+ editor
+ })
+ .root(cx);
+
+ editor.update(cx, |editor, cx| {
+ // Edit the buffer directly, deleting ranges surrounding the editor's selections
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ [
+ (Point::new(1, 2)..Point::new(3, 0), ""),
+ (Point::new(4, 2)..Point::new(6, 0), ""),
+ ],
+ None,
+ cx,
+ );
+ assert_eq!(
+ buffer.read(cx).text(),
+ "
+ a
+ b()
+ c()
+ "
+ .unindent()
+ );
+ });
+ assert_eq!(
+ editor.selections.ranges(cx),
+ &[
+ Point::new(1, 2)..Point::new(1, 2),
+ Point::new(2, 2)..Point::new(2, 2),
+ ],
+ );
+
+ editor.newline(&Newline, cx);
+ assert_eq!(
+ editor.text(cx),
+ "
+ a
+ b(
+ )
+ c(
+ )
+ "
+ .unindent()
+ );
+
+ // The selections are moved after the inserted newlines
+ assert_eq!(
+ editor.selections.ranges(cx),
+ &[
+ Point::new(2, 0)..Point::new(2, 0),
+ Point::new(4, 0)..Point::new(4, 0),
+ ],
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_newline_above(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
+ });
+
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+ .unwrap(),
+ );
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ cx.set_state(indoc! {"
+ const a: หA = (
+ (ห
+ ยซconst_functionหยป(ห),
+ soยซmหยปetยซhหยปing_หelse,ห
+ )ห
+ ห);ห
+ "});
+
+ cx.update_editor(|e, cx| e.newline_above(&NewlineAbove, cx));
+ cx.assert_editor_state(indoc! {"
+ ห
+ const a: A = (
+ ห
+ (
+ ห
+ ห
+ const_function(),
+ ห
+ ห
+ ห
+ ห
+ something_else,
+ ห
+ )
+ ห
+ ห
+ );
+ "});
+}
+
+#[gpui::test]
+async fn test_newline_below(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
+ });
+
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+ .unwrap(),
+ );
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ cx.set_state(indoc! {"
+ const a: หA = (
+ (ห
+ ยซconst_functionหยป(ห),
+ soยซmหยปetยซhหยปing_หelse,ห
+ )ห
+ ห);ห
+ "});
+
+ cx.update_editor(|e, cx| e.newline_below(&NewlineBelow, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: A = (
+ ห
+ (
+ ห
+ const_function(),
+ ห
+ ห
+ something_else,
+ ห
+ ห
+ ห
+ ห
+ )
+ ห
+ );
+ ห
+ ห
+ "});
+}
+
+#[gpui::test]
+async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
+ });
+
+ let language = Arc::new(Language::new(
+ LanguageConfig {
+ line_comment: Some("//".into()),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+ {
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ cx.set_state(indoc! {"
+ // Fooห
+ "});
+
+ cx.update_editor(|e, cx| e.newline(&Newline, cx));
+ cx.assert_editor_state(indoc! {"
+ // Foo
+ //ห
+ "});
+ // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
+ cx.set_state(indoc! {"
+ ห// Foo
+ "});
+ cx.update_editor(|e, cx| e.newline(&Newline, cx));
+ cx.assert_editor_state(indoc! {"
+
+ ห// Foo
+ "});
+ }
+ // Ensure that comment continuations can be disabled.
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.extend_comment_on_newline = Some(false);
+ });
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state(indoc! {"
+ // Fooห
+ "});
+ cx.update_editor(|e, cx| e.newline(&Newline, cx));
+ cx.assert_editor_state(indoc! {"
+ // Foo
+ ห
+ "});
+}
+
+#[gpui::test]
+fn test_insert_with_old_selections(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
+ let mut editor = build_editor(buffer.clone(), cx);
+ editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20]));
+ editor
+ })
+ .root(cx);
+
+ editor.update(cx, |editor, cx| {
+ // Edit the buffer directly, deleting ranges surrounding the editor's selections
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
+ assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
+ });
+ assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
+
+ editor.insert("Z", cx);
+ assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
+
+ // The selections are moved after the inserted characters
+ assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
+ });
+}
+
+#[gpui::test]
+async fn test_tab(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(3)
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state(indoc! {"
+ หabหc
+ ห๐ห๐หefg
+ dห
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ หab หc
+ ห๐ ห๐ หefg
+ d ห
+ "});
+
+ cx.set_state(indoc! {"
+ a
+ ยซ๐หยป๐ยซ๐หยป๐ยซ๐หยป
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ a
+ ยซ๐หยป๐ยซ๐หยป๐ยซ๐หยป
+ "});
+}
+
+#[gpui::test]
+async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
+ .unwrap(),
+ );
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // cursors that are already at the suggested indent level insert
+ // a soft tab. cursors that are to the left of the suggested indent
+ // auto-indent their line.
+ cx.set_state(indoc! {"
+ ห
+ const a: B = (
+ c(
+ d(
+ ห
+ )
+ ห
+ ห )
+ );
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ ห
+ const a: B = (
+ c(
+ d(
+ ห
+ )
+ ห
+ ห)
+ );
+ "});
+
+ // handle auto-indent when there are multiple cursors on the same line
+ cx.set_state(indoc! {"
+ const a: B = (
+ c(
+ ห ห
+ ห )
+ );
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(
+ ห
+ ห)
+ );
+ "});
+}
+
+#[gpui::test]
+async fn test_tab_with_mixed_whitespace(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4)
+ });
+
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
+ .unwrap(),
+ );
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+ cx.set_state(indoc! {"
+ fn a() {
+ if b {
+ \t หc
+ }
+ }
+ "});
+
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ fn a() {
+ if b {
+ หc
+ }
+ }
+ "});
+}
+
+#[gpui::test]
+async fn test_indent_outdent(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.tab_size = NonZeroU32::new(4);
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ cx.set_state(indoc! {"
+ ยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+
+ // select across line ending
+ cx.set_state(indoc! {"
+ one two
+ tยซhree
+ หยป four
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ tยซhree
+ หยป four
+ "});
+
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ tยซhree
+ หยป four
+ "});
+
+ // Ensure that indenting/outdenting works when the cursor is at column 0.
+ cx.set_state(indoc! {"
+ one two
+ หthree
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ หthree
+ four
+ "});
+
+ cx.set_state(indoc! {"
+ one two
+ ห three
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ หthree
+ four
+ "});
+}
+
+#[gpui::test]
+async fn test_indent_outdent_with_hard_tabs(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.hard_tabs = Some(true);
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ // select two ranges on one line
+ cx.set_state(indoc! {"
+ ยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ \tยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ \t\tยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ \tยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซoneหยป ยซtwoหยป
+ three
+ four
+ "});
+
+ // select across a line ending
+ cx.set_state(indoc! {"
+ one two
+ tยซhree
+ หยปfour
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ \ttยซhree
+ หยปfour
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ \t\ttยซhree
+ หยปfour
+ "});
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ \ttยซhree
+ หยปfour
+ "});
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ tยซhree
+ หยปfour
+ "});
+
+ // Ensure that indenting/outdenting works when the cursor is at column 0.
+ cx.set_state(indoc! {"
+ one two
+ หthree
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ หthree
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab(&Tab, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ \tหthree
+ four
+ "});
+ cx.update_editor(|e, cx| e.tab_prev(&TabPrev, cx));
+ cx.assert_editor_state(indoc! {"
+ one two
+ หthree
+ four
+ "});
+}
+
+#[gpui::test]
+fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
+ init_test(cx, |settings| {
+ settings.languages.extend([
+ (
+ "TOML".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(2),
+ ..Default::default()
+ },
+ ),
+ (
+ "Rust".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(4),
+ ..Default::default()
+ },
+ ),
+ ]);
+ });
+
+ let toml_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "TOML".into(),
+ ..Default::default()
+ },
+ None,
+ ));
+ let rust_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ ..Default::default()
+ },
+ None,
+ ));
+
+ let toml_buffer = cx.add_model(|cx| {
+ Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx)
+ });
+ let rust_buffer = cx.add_model(|cx| {
+ Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n")
+ .with_language(rust_language, cx)
+ });
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ toml_buffer.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ rust_buffer.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer
+ });
+
+ cx.add_window(|cx| {
+ let mut editor = build_editor(multibuffer, cx);
+
+ assert_eq!(
+ editor.text(cx),
+ indoc! {"
+ a = 1
+ b = 2
+
+ const c: usize = 3;
+ "}
+ );
+
+ select_ranges(
+ &mut editor,
+ indoc! {"
+ ยซaหยป = 1
+ b = 2
+
+ ยซconst c:หยป usize = 3;
+ "},
+ cx,
+ );
+
+ editor.tab(&Tab, cx);
+ assert_text_with_selections(
+ &mut editor,
+ indoc! {"
+ ยซaหยป = 1
+ b = 2
+
+ ยซconst c:หยป usize = 3;
+ "},
+ cx,
+ );
+ editor.tab_prev(&TabPrev, cx);
+ assert_text_with_selections(
+ &mut editor,
+ indoc! {"
+ ยซaหยป = 1
+ b = 2
+
+ ยซconst c:หยป usize = 3;
+ "},
+ cx,
+ );
+
+ editor
+ });
+}
+
+#[gpui::test]
+async fn test_backspace(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ // Basic backspace
+ cx.set_state(indoc! {"
+ onหe two three
+ fouยซrหยป five six
+ seven ยซหeight nine
+ ยปten
+ "});
+ cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+ cx.assert_editor_state(indoc! {"
+ oหe two three
+ fouห five six
+ seven หten
+ "});
+
+ // Test backspace inside and around indents
+ cx.set_state(indoc! {"
+ zero
+ หone
+ หtwo
+ ห ห ห three
+ ห ห four
+ "});
+ cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+ cx.assert_editor_state(indoc! {"
+ zero
+ หone
+ หtwo
+ ห threeห four
+ "});
+
+ // Test backspace with line_mode set to true
+ cx.update_editor(|e, _| e.selections.line_mode = true);
+ cx.set_state(indoc! {"
+ The หquick หbrown
+ fox jumps over
+ the lazy dog
+ หThe quยซick bหยปrown"});
+ cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+ cx.assert_editor_state(indoc! {"
+ หfox jumps over
+ the lazy dogห"});
+}
+
+#[gpui::test]
+async fn test_delete(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state(indoc! {"
+ onหe two three
+ fouยซrหยป five six
+ seven ยซหeight nine
+ ยปten
+ "});
+ cx.update_editor(|e, cx| e.delete(&Delete, cx));
+ cx.assert_editor_state(indoc! {"
+ onห two three
+ fouห five six
+ seven หten
+ "});
+
+ // Test backspace with line_mode set to true
+ cx.update_editor(|e, _| e.selections.line_mode = true);
+ cx.set_state(indoc! {"
+ The หquick หbrown
+ fox ยซหjumยปps over
+ the lazy dog
+ หThe quยซick bหยปrown"});
+ cx.update_editor(|e, cx| e.backspace(&Backspace, cx));
+ cx.assert_editor_state("หthe lazy dogห");
+}
+
+#[gpui::test]
+fn test_delete_line(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+ ])
+ });
+ view.delete_line(&DeleteLine, cx);
+ assert_eq!(view.display_text(cx), "ghi");
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0),
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)
+ ]
+ );
+ });
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)])
+ });
+ view.delete_line(&DeleteLine, cx);
+ assert_eq!(view.display_text(cx), "ghi\n");
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1)]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ cx.add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+ let mut editor = build_editor(buffer.clone(), cx);
+ let buffer = buffer.read(cx).as_singleton().unwrap();
+
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ &[Point::new(0, 0)..Point::new(0, 0)]
+ );
+
+ // When on single line, replace newline at end by space
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ &[Point::new(0, 3)..Point::new(0, 3)]
+ );
+
+ // When multiple lines are selected, remove newlines that are spanned by the selection
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
+ });
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ &[Point::new(0, 11)..Point::new(0, 11)]
+ );
+
+ // Undo should be transactional
+ editor.undo(&Undo, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ &[Point::new(0, 5)..Point::new(2, 2)]
+ );
+
+ // When joining an empty line don't insert a space
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
+ });
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [Point::new(2, 3)..Point::new(2, 3)]
+ );
+
+ // We can remove trailing newlines
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [Point::new(2, 3)..Point::new(2, 3)]
+ );
+
+ // We don't blow up on the last line
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [Point::new(2, 3)..Point::new(2, 3)]
+ );
+
+ // reset to test indentation
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.edit(
+ [
+ (Point::new(1, 0)..Point::new(1, 2), " "),
+ (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
+ ],
+ None,
+ cx,
+ )
+ });
+
+ // We remove any leading spaces
+ assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
+ });
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
+
+ // We don't insert a space for a line containing only spaces
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
+
+ // We ignore any leading tabs
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
+
+ editor
+ });
+}
+
+#[gpui::test]
+fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ cx.add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
+ let mut editor = build_editor(buffer.clone(), cx);
+ let buffer = buffer.read(cx).as_singleton().unwrap();
+
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([
+ Point::new(0, 2)..Point::new(1, 1),
+ Point::new(1, 2)..Point::new(1, 2),
+ Point::new(3, 1)..Point::new(3, 2),
+ ])
+ });
+
+ editor.join_lines(&JoinLines, cx);
+ assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
+
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [
+ Point::new(0, 7)..Point::new(0, 7),
+ Point::new(1, 3)..Point::new(1, 3)
+ ]
+ );
+ editor
+ });
+}
+
+#[gpui::test]
+async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ // Test sort_lines_case_insensitive()
+ cx.set_state(indoc! {"
+ ยซz
+ y
+ x
+ Z
+ Y
+ Xหยป
+ "});
+ cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซx
+ X
+ y
+ Y
+ z
+ Zหยป
+ "});
+
+ // Test reverse_lines()
+ cx.set_state(indoc! {"
+ ยซ5
+ 4
+ 3
+ 2
+ 1หยป
+ "});
+ cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซ1
+ 2
+ 3
+ 4
+ 5หยป
+ "});
+
+ // Skip testing shuffle_line()
+
+ // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
+ // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
+
+ // Don't manipulate when cursor is on single line, but expand the selection
+ cx.set_state(indoc! {"
+ ddหdd
+ ccc
+ bb
+ a
+ "});
+ cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซddddหยป
+ ccc
+ bb
+ a
+ "});
+
+ // Basic manipulate case
+ // Start selection moves to column 0
+ // End of selection shrinks to fit shorter line
+ cx.set_state(indoc! {"
+ ddยซd
+ ccc
+ bb
+ aaaaaหยป
+ "});
+ cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซaaaaa
+ bb
+ ccc
+ dddหยป
+ "});
+
+ // Manipulate case with newlines
+ cx.set_state(indoc! {"
+ ddยซd
+ ccc
+
+ bb
+ aaaaa
+
+ หยป
+ "});
+ cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซ
+
+ aaaaa
+ bb
+ ccc
+ dddหยป
+
+ "});
+}
+
+#[gpui::test]
+async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ // Manipulate with multiple selections on a single line
+ cx.set_state(indoc! {"
+ ddยซdd
+ cหยปcยซc
+ bb
+ aaaหยปaa
+ "});
+ cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซaaaaa
+ bb
+ ccc
+ ddddหยป
+ "});
+
+ // Manipulate with multiple disjoin selections
+ cx.set_state(indoc! {"
+ 5ยซ
+ 4
+ 3
+ 2
+ 1หยป
+
+ ddยซdd
+ ccc
+ bb
+ aaaหยปaa
+ "});
+ cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซ1
+ 2
+ 3
+ 4
+ 5หยป
+
+ ยซaaaaa
+ bb
+ ccc
+ ddddหยป
+ "});
+}
+
+#[gpui::test]
+async fn test_manipulate_text(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ // Test convert_to_upper_case()
+ cx.set_state(indoc! {"
+ ยซhello worldหยป
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซHELLO WORLDหยป
+ "});
+
+ // Test convert_to_lower_case()
+ cx.set_state(indoc! {"
+ ยซHELLO WORLDหยป
+ "});
+ cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซhello worldหยป
+ "});
+
+ // Test multiple line, single selection case
+ // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+ cx.set_state(indoc! {"
+ ยซThe quick brown
+ fox jumps over
+ the lazy dogหยป
+ "});
+ cx.update_editor(|e, cx| e.convert_to_title_case(&ConvertToTitleCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซThe Quick Brown
+ Fox Jumps Over
+ The Lazy Dogหยป
+ "});
+
+ // Test multiple line, single selection case
+ // Test code hack that covers the fact that to_case crate doesn't support '\n' as a word boundary
+ cx.set_state(indoc! {"
+ ยซThe quick brown
+ fox jumps over
+ the lazy dogหยป
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซTheQuickBrown
+ FoxJumpsOver
+ TheLazyDogหยป
+ "});
+
+ // From here on out, test more complex cases of manipulate_text()
+
+ // Test no selection case - should affect words cursors are in
+ // Cursor at beginning, middle, and end of word
+ cx.set_state(indoc! {"
+ หhello big beauหtiful worldห
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซHELLOหยป big ยซBEAUTIFULหยป ยซWORLDหยป
+ "});
+
+ // Test multiple selections on a single line and across multiple lines
+ cx.set_state(indoc! {"
+ ยซTheหยป quick ยซbrown
+ foxหยป jumps ยซoverหยป
+ the ยซlazyหยป dog
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซTHEหยป quick ยซBROWN
+ FOXหยป jumps ยซOVERหยป
+ the ยซLAZYหยป dog
+ "});
+
+ // Test case where text length grows
+ cx.set_state(indoc! {"
+ ยซtschรผรหยป
+ "});
+ cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซTSCHรSSหยป
+ "});
+
+ // Test to make sure we don't crash when text shrinks
+ cx.set_state(indoc! {"
+ aaa_bbbห
+ "});
+ cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซaaaBbbหยป
+ "});
+
+ // Test to make sure we all aware of the fact that each word can grow and shrink
+ // Final selections should be aware of this fact
+ cx.set_state(indoc! {"
+ aaa_bหbb bbหb_ccc หccc_ddd
+ "});
+ cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx));
+ cx.assert_editor_state(indoc! {"
+ ยซaaaBbbหยป ยซbbbCccหยป ยซcccDddหยป
+ "});
+}
+
+#[gpui::test]
+fn test_duplicate_line(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+ DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+ ])
+ });
+ view.duplicate_line(&DuplicateLine, cx);
+ assert_eq!(view.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(1, 2)..DisplayPoint::new(1, 2),
+ DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0),
+ DisplayPoint::new(6, 0)..DisplayPoint::new(6, 0),
+ ]
+ );
+ });
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 1)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(1, 2)..DisplayPoint::new(2, 1),
+ ])
+ });
+ view.duplicate_line(&DuplicateLine, cx);
+ assert_eq!(view.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(3, 1)..DisplayPoint::new(4, 1),
+ DisplayPoint::new(4, 2)..DisplayPoint::new(5, 1),
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_move_line_up_down(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.fold_ranges(
+ vec![
+ Point::new(0, 2)..Point::new(1, 2),
+ Point::new(2, 3)..Point::new(4, 1),
+ Point::new(7, 0)..Point::new(8, 4),
+ ],
+ true,
+ cx,
+ );
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+ DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+ DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2),
+ ])
+ });
+ assert_eq!(
+ view.display_text(cx),
+ "aaโฏbbb\ncccโฏeeee\nfffff\nggggg\nโฏi\njjjjj"
+ );
+
+ view.move_line_up(&MoveLineUp, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "aaโฏbbb\ncccโฏeeee\nggggg\nโฏi\njjjjj\nfffff"
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+ DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
+ DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_line_down(&MoveLineDown, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "cccโฏeeee\naaโฏbbb\nfffff\nggggg\nโฏi\njjjjj"
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+ DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+ DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_line_down(&MoveLineDown, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "cccโฏeeee\nfffff\naaโฏbbb\nggggg\nโฏi\njjjjj"
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 1),
+ DisplayPoint::new(3, 2)..DisplayPoint::new(4, 3),
+ DisplayPoint::new(5, 0)..DisplayPoint::new(5, 2)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.move_line_up(&MoveLineUp, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "cccโฏeeee\naaโฏbbb\nggggg\nโฏi\njjjjj\nfffff"
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1),
+ DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3),
+ DisplayPoint::new(4, 0)..DisplayPoint::new(4, 2)
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ editor.update(cx, |editor, cx| {
+ let snapshot = editor.buffer.read(cx).snapshot(cx);
+ editor.insert_blocks(
+ [BlockProperties {
+ style: BlockStyle::Fixed,
+ position: snapshot.anchor_after(Point::new(2, 0)),
+ disposition: BlockDisposition::Below,
+ height: 1,
+ render: Arc::new(|_| Empty::new().into_any()),
+ }],
+ Some(Autoscroll::fit()),
+ cx,
+ );
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
+ });
+ editor.move_line_down(&MoveLineDown, cx);
+ });
+}
+
+#[gpui::test]
+fn test_transpose(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ _ = cx.add_window(|cx| {
+ let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx);
+
+ editor.change_selections(None, cx, |s| s.select_ranges([1..1]));
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bac");
+ assert_eq!(editor.selections.ranges(cx), [2..2]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bca");
+ assert_eq!(editor.selections.ranges(cx), [3..3]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bac");
+ assert_eq!(editor.selections.ranges(cx), [3..3]);
+
+ editor
+ });
+
+ _ = cx.add_window(|cx| {
+ let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+
+ editor.change_selections(None, cx, |s| s.select_ranges([3..3]));
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "acb\nde");
+ assert_eq!(editor.selections.ranges(cx), [3..3]);
+
+ editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "acbd\ne");
+ assert_eq!(editor.selections.ranges(cx), [5..5]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "acbde\n");
+ assert_eq!(editor.selections.ranges(cx), [6..6]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "acbd\ne");
+ assert_eq!(editor.selections.ranges(cx), [6..6]);
+
+ editor
+ });
+
+ _ = cx.add_window(|cx| {
+ let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx);
+
+ editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bacd\ne");
+ assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bcade\n");
+ assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bcda\ne");
+ assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bcade\n");
+ assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "bcaed\n");
+ assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
+
+ editor
+ });
+
+ _ = cx.add_window(|cx| {
+ let mut editor = build_editor(MultiBuffer::build_simple("๐๐โ", cx), cx);
+
+ editor.change_selections(None, cx, |s| s.select_ranges([4..4]));
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "๐๐โ");
+ assert_eq!(editor.selections.ranges(cx), [8..8]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "๐โ๐");
+ assert_eq!(editor.selections.ranges(cx), [11..11]);
+
+ editor.transpose(&Default::default(), cx);
+ assert_eq!(editor.text(cx), "๐๐โ");
+ assert_eq!(editor.selections.ranges(cx), [11..11]);
+
+ editor
+ });
+}
+
+#[gpui::test]
+async fn test_clipboard(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ cx.set_state("ยซoneโ
หยปtwo ยซthree หยปfour ยซfive หยปsix ");
+ cx.update_editor(|e, cx| e.cut(&Cut, cx));
+ cx.assert_editor_state("หtwo หfour หsix ");
+
+ // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
+ cx.set_state("two หfour หsix ห");
+ cx.update_editor(|e, cx| e.paste(&Paste, cx));
+ cx.assert_editor_state("two oneโ
หfour three หsix five ห");
+
+ // Paste again but with only two cursors. Since the number of cursors doesn't
+ // match the number of slices in the clipboard, the entire clipboard text
+ // is pasted at each cursor.
+ cx.set_state("หtwo oneโ
four three six five ห");
+ cx.update_editor(|e, cx| {
+ e.handle_input("( ", cx);
+ e.paste(&Paste, cx);
+ e.handle_input(") ", cx);
+ });
+ cx.assert_editor_state(
+ &([
+ "( oneโ
",
+ "three ",
+ "five ) หtwo oneโ
four three six five ( oneโ
",
+ "three ",
+ "five ) ห",
+ ]
+ .join("\n")),
+ );
+
+ // Cut with three selections, one of which is full-line.
+ cx.set_state(indoc! {"
+ 1ยซ2หยป3
+ 4ห567
+ ยซ8หยป9"});
+ cx.update_editor(|e, cx| e.cut(&Cut, cx));
+ cx.assert_editor_state(indoc! {"
+ 1ห3
+ ห9"});
+
+ // Paste with three selections, noticing how the copied selection that was full-line
+ // gets inserted before the second cursor.
+ cx.set_state(indoc! {"
+ 1ห3
+ 9ห
+ ยซoหยปne"});
+ cx.update_editor(|e, cx| e.paste(&Paste, cx));
+ cx.assert_editor_state(indoc! {"
+ 12ห3
+ 4567
+ 9ห
+ 8หne"});
+
+ // Copy with a single cursor only, which writes the whole line into the clipboard.
+ cx.set_state(indoc! {"
+ The quick brown
+ fox juหmps over
+ the lazy dog"});
+ cx.update_editor(|e, cx| e.copy(&Copy, cx));
+ cx.cx.assert_clipboard_content(Some("fox jumps over\n"));
+
+ // Paste with three selections, noticing how the copied full-line selection is inserted
+ // before the empty selections but replaces the selection that is non-empty.
+ cx.set_state(indoc! {"
+ Tหhe quick brown
+ ยซfoหยปx jumps over
+ tหhe lazy dog"});
+ cx.update_editor(|e, cx| e.paste(&Paste, cx));
+ cx.assert_editor_state(indoc! {"
+ fox jumps over
+ Tหhe quick brown
+ fox jumps over
+ หx jumps over
+ fox jumps over
+ tหhe lazy dog"});
+}
+
+#[gpui::test]
+async fn test_paste_multiline(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = Arc::new(Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ ));
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // Cut an indented block, without the leading whitespace.
+ cx.set_state(indoc! {"
+ const a: B = (
+ c(),
+ ยซd(
+ e,
+ f
+ )หยป
+ );
+ "});
+ cx.update_editor(|e, cx| e.cut(&Cut, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(),
+ ห
+ );
+ "});
+
+ // Paste it at the same position.
+ cx.update_editor(|e, cx| e.paste(&Paste, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(),
+ d(
+ e,
+ f
+ )ห
+ );
+ "});
+
+ // Paste it at a line with a lower indent level.
+ cx.set_state(indoc! {"
+ ห
+ const a: B = (
+ c(),
+ );
+ "});
+ cx.update_editor(|e, cx| e.paste(&Paste, cx));
+ cx.assert_editor_state(indoc! {"
+ d(
+ e,
+ f
+ )ห
+ const a: B = (
+ c(),
+ );
+ "});
+
+ // Cut an indented block, with the leading whitespace.
+ cx.set_state(indoc! {"
+ const a: B = (
+ c(),
+ ยซ d(
+ e,
+ f
+ )
+ หยป);
+ "});
+ cx.update_editor(|e, cx| e.cut(&Cut, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(),
+ ห);
+ "});
+
+ // Paste it at the same position.
+ cx.update_editor(|e, cx| e.paste(&Paste, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(),
+ d(
+ e,
+ f
+ )
+ ห);
+ "});
+
+ // Paste it at a line with a higher indent level.
+ cx.set_state(indoc! {"
+ const a: B = (
+ c(),
+ d(
+ e,
+ fห
+ )
+ );
+ "});
+ cx.update_editor(|e, cx| e.paste(&Paste, cx));
+ cx.assert_editor_state(indoc! {"
+ const a: B = (
+ c(),
+ d(
+ e,
+ f d(
+ e,
+ f
+ )
+ ห
+ )
+ );
+ "});
+}
+
+#[gpui::test]
+fn test_select_all(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.select_all(&SelectAll, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ &[DisplayPoint::new(0, 0)..DisplayPoint::new(2, 3)]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_select_line(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+ DisplayPoint::new(4, 2)..DisplayPoint::new(4, 2),
+ ])
+ });
+ view.select_line(&SelectLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 0)..DisplayPoint::new(2, 0),
+ DisplayPoint::new(4, 0)..DisplayPoint::new(5, 0),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.select_line(&SelectLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 0)..DisplayPoint::new(3, 0),
+ DisplayPoint::new(4, 0)..DisplayPoint::new(5, 5),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.select_line(&SelectLine, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![DisplayPoint::new(0, 0)..DisplayPoint::new(5, 5)]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_split_selection_into_lines(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+ view.update(cx, |view, cx| {
+ view.fold_ranges(
+ vec![
+ Point::new(0, 2)..Point::new(1, 2),
+ Point::new(2, 3)..Point::new(4, 1),
+ Point::new(7, 0)..Point::new(8, 4),
+ ],
+ true,
+ cx,
+ );
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0),
+ DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
+ ])
+ });
+ assert_eq!(view.display_text(cx), "aaโฏbbb\ncccโฏeeee\nfffff\nggggg\nโฏi");
+ });
+
+ view.update(cx, |view, cx| {
+ view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "aaaaa\nbbbbb\ncccโฏeeee\nfffff\nggggg\nโฏi"
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 2),
+ DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0),
+ DisplayPoint::new(5, 4)..DisplayPoint::new(5, 4)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(5, 0)..DisplayPoint::new(0, 1)])
+ });
+ view.split_selection_into_lines(&SplitSelectionIntoLines, cx);
+ assert_eq!(
+ view.display_text(cx),
+ "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 5)..DisplayPoint::new(0, 5),
+ DisplayPoint::new(1, 5)..DisplayPoint::new(1, 5),
+ DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+ DisplayPoint::new(3, 5)..DisplayPoint::new(3, 5),
+ DisplayPoint::new(4, 5)..DisplayPoint::new(4, 5),
+ DisplayPoint::new(5, 5)..DisplayPoint::new(5, 5),
+ DisplayPoint::new(6, 5)..DisplayPoint::new(6, 5),
+ DisplayPoint::new(7, 0)..DisplayPoint::new(7, 0)
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_add_selection_above_below(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let view = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx);
+ build_editor(buffer, cx)
+ })
+ .root(cx);
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)])
+ });
+ });
+ view.update(cx, |view, cx| {
+ view.add_selection_above(&AddSelectionAbove, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_above(&AddSelectionAbove, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
+ );
+
+ view.undo_selection(&UndoSelection, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)
+ ]
+ );
+
+ view.redo_selection(&RedoSelection, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
+ DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3),
+ DisplayPoint::new(4, 3)..DisplayPoint::new(4, 3)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)])
+ });
+ });
+ view.update(cx, |view, cx| {
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
+ DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3),
+ DisplayPoint::new(4, 4)..DisplayPoint::new(4, 3)
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_above(&AddSelectionAbove, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_above(&AddSelectionAbove, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![DisplayPoint::new(1, 4)..DisplayPoint::new(1, 3)]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(0, 1)..DisplayPoint::new(1, 4)])
+ });
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
+ DisplayPoint::new(4, 1)..DisplayPoint::new(4, 4),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_above(&AddSelectionAbove, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(1, 1)..DisplayPoint::new(1, 4),
+ DisplayPoint::new(3, 1)..DisplayPoint::new(3, 2),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([DisplayPoint::new(4, 3)..DisplayPoint::new(1, 1)])
+ });
+ });
+ view.update(cx, |view, cx| {
+ view.add_selection_above(&AddSelectionAbove, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
+ DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
+ ]
+ );
+ });
+
+ view.update(cx, |view, cx| {
+ view.add_selection_below(&AddSelectionBelow, cx);
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ vec![
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(3, 2)..DisplayPoint::new(3, 1),
+ DisplayPoint::new(4, 3)..DisplayPoint::new(4, 1),
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_select_next(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state("abc\nหabc abc\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("abc\nยซabcหยป abc\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("abc\nยซabcหยป ยซabcหยป\ndefabc\nabc");
+
+ cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+ cx.assert_editor_state("abc\nยซabcหยป abc\ndefabc\nabc");
+
+ cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+ cx.assert_editor_state("abc\nยซabcหยป ยซabcหยป\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("abc\nยซabcหยป ยซabcหยป\ndefabc\nยซabcหยป");
+
+ cx.update_editor(|e, cx| e.select_next(&SelectNext::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซabcหยป ยซabcหยป\ndefabc\nยซabcหยป");
+}
+
+#[gpui::test]
+async fn test_select_previous(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ {
+ // `Select previous` without a selection (selects wordwise)
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state("abc\nหabc abc\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("abc\nยซabcหยป abc\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซabcหยป abc\ndefabc\nabc");
+
+ cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+ cx.assert_editor_state("abc\nยซabcหยป abc\ndefabc\nabc");
+
+ cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+ cx.assert_editor_state("ยซabcหยป\nยซabcหยป abc\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซabcหยป abc\ndefabc\nยซabcหยป");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซabcหยป ยซabcหยป\ndefabc\nยซabcหยป");
+ }
+ {
+ // `Select previous` with a selection
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.set_state("abc\nยซหabcยป abc\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซหabcยป abc\ndefabc\nabc");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซหabcยป abc\ndefabc\nยซabcหยป");
+
+ cx.update_editor(|view, cx| view.undo_selection(&UndoSelection, cx));
+ cx.assert_editor_state("ยซabcหยป\nยซหabcยป abc\ndefabc\nabc");
+
+ cx.update_editor(|view, cx| view.redo_selection(&RedoSelection, cx));
+ cx.assert_editor_state("ยซabcหยป\nยซหabcยป abc\ndefabc\nยซabcหยป");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซหabcยป abc\ndefยซabcหยป\nยซabcหยป");
+
+ cx.update_editor(|e, cx| e.select_previous(&SelectPrevious::default(), cx))
+ .unwrap();
+ cx.assert_editor_state("ยซabcหยป\nยซหabcยป ยซabcหยป\ndefยซabcหยป\nยซabcหยป");
+ }
+}
+
+#[gpui::test]
+async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language = Arc::new(Language::new(
+ LanguageConfig::default(),
+ Some(tree_sitter_rust::language()),
+ ));
+
+ let text = r#"
+ use mod1::mod2::{mod3, mod4};
+
+ fn fn_1(param1: bool, param2: &str) {
+ let var1 = "text";
+ }
+ "#
+ .unindent();
+
+ let buffer =
+ cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ .await;
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+ DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+ DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+ ]);
+ });
+ view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| { view.selections.display_ranges(cx) }),
+ &[
+ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+ DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+ DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+ ]
+ );
+
+ view.update(cx, |view, cx| {
+ view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[
+ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+ DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+ ]
+ );
+
+ view.update(cx, |view, cx| {
+ view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
+ );
+
+ // Trying to expand the selected syntax node one more time has no effect.
+ view.update(cx, |view, cx| {
+ view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[DisplayPoint::new(5, 0)..DisplayPoint::new(0, 0)]
+ );
+
+ view.update(cx, |view, cx| {
+ view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[
+ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+ DisplayPoint::new(4, 1)..DisplayPoint::new(2, 0),
+ ]
+ );
+
+ view.update(cx, |view, cx| {
+ view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[
+ DisplayPoint::new(0, 23)..DisplayPoint::new(0, 27),
+ DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+ DisplayPoint::new(3, 15)..DisplayPoint::new(3, 21),
+ ]
+ );
+
+ view.update(cx, |view, cx| {
+ view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[
+ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+ DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+ DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+ ]
+ );
+
+ // Trying to shrink the selected syntax node one more time has no effect.
+ view.update(cx, |view, cx| {
+ view.select_smaller_syntax_node(&SelectSmallerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[
+ DisplayPoint::new(0, 25)..DisplayPoint::new(0, 25),
+ DisplayPoint::new(2, 24)..DisplayPoint::new(2, 12),
+ DisplayPoint::new(3, 18)..DisplayPoint::new(3, 18),
+ ]
+ );
+
+ // Ensure that we keep expanding the selection if the larger selection starts or ends within
+ // a fold.
+ view.update(cx, |view, cx| {
+ view.fold_ranges(
+ vec![
+ Point::new(0, 21)..Point::new(0, 24),
+ Point::new(3, 20)..Point::new(3, 22),
+ ],
+ true,
+ cx,
+ );
+ view.select_larger_syntax_node(&SelectLargerSyntaxNode, cx);
+ });
+ assert_eq!(
+ view.update(cx, |view, cx| view.selections.display_ranges(cx)),
+ &[
+ DisplayPoint::new(0, 16)..DisplayPoint::new(0, 28),
+ DisplayPoint::new(2, 35)..DisplayPoint::new(2, 7),
+ DisplayPoint::new(3, 4)..DisplayPoint::new(3, 23),
+ ]
+ );
+}
+
+#[gpui::test]
+async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: false,
+ newline: true,
+ },
+ BracketPair {
+ start: "(".to_string(),
+ end: ")".to_string(),
+ close: false,
+ newline: true,
+ },
+ ],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query(
+ r#"
+ (_ "(" ")" @end) @indent
+ (_ "{" "}" @end) @indent
+ "#,
+ )
+ .unwrap(),
+ );
+
+ let text = "fn a() {}";
+
+ let buffer =
+ cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ editor
+ .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
+ .await;
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
+ editor.newline(&Newline, cx);
+ assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
+ assert_eq!(
+ editor.selections.ranges(cx),
+ &[
+ Point::new(1, 4)..Point::new(1, 4),
+ Point::new(3, 4)..Point::new(3, 4),
+ Point::new(5, 0)..Point::new(5, 0)
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_autoclose_pairs(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let language = Arc::new(Language::new(
+ LanguageConfig {
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ },
+ BracketPair {
+ start: "(".to_string(),
+ end: ")".to_string(),
+ close: true,
+ newline: true,
+ },
+ BracketPair {
+ start: "/*".to_string(),
+ end: " */".to_string(),
+ close: true,
+ newline: true,
+ },
+ BracketPair {
+ start: "[".to_string(),
+ end: "]".to_string(),
+ close: false,
+ newline: true,
+ },
+ BracketPair {
+ start: "\"".to_string(),
+ end: "\"".to_string(),
+ close: true,
+ newline: false,
+ },
+ ],
+ ..Default::default()
+ },
+ autoclose_before: "})]".to_string(),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ ));
+
+ let registry = Arc::new(LanguageRegistry::test());
+ registry.add(language.clone());
+ cx.update_buffer(|buffer, cx| {
+ buffer.set_language_registry(registry);
+ buffer.set_language(Some(language), cx);
+ });
+
+ cx.set_state(
+ &r#"
+ ๐ห
+ ฮตห
+ โค๏ธห
+ "#
+ .unindent(),
+ );
+
+ // autoclose multiple nested brackets at multiple cursors
+ cx.update_editor(|view, cx| {
+ view.handle_input("{", cx);
+ view.handle_input("{", cx);
+ view.handle_input("{", cx);
+ });
+ cx.assert_editor_state(
+ &"
+ ๐{{{ห}}}
+ ฮต{{{ห}}}
+ โค๏ธ{{{ห}}}
+ "
+ .unindent(),
+ );
+
+ // insert a different closing bracket
+ cx.update_editor(|view, cx| {
+ view.handle_input(")", cx);
+ });
+ cx.assert_editor_state(
+ &"
+ ๐{{{)ห}}}
+ ฮต{{{)ห}}}
+ โค๏ธ{{{)ห}}}
+ "
+ .unindent(),
+ );
+
+ // skip over the auto-closed brackets when typing a closing bracket
+ cx.update_editor(|view, cx| {
+ view.move_right(&MoveRight, cx);
+ view.handle_input("}", cx);
+ view.handle_input("}", cx);
+ view.handle_input("}", cx);
+ });
+ cx.assert_editor_state(
+ &"
+ ๐{{{)}}}}ห
+ ฮต{{{)}}}}ห
+ โค๏ธ{{{)}}}}ห
+ "
+ .unindent(),
+ );
+
+ // autoclose multi-character pairs
+ cx.set_state(
+ &"
+ ห
+ ห
+ "
+ .unindent(),
+ );
+ cx.update_editor(|view, cx| {
+ view.handle_input("/", cx);
+ view.handle_input("*", cx);
+ });
+ cx.assert_editor_state(
+ &"
+ /*ห */
+ /*ห */
+ "
+ .unindent(),
+ );
+
+ // one cursor autocloses a multi-character pair, one cursor
+ // does not autoclose.
+ cx.set_state(
+ &"
+ /ห
+ ห
+ "
+ .unindent(),
+ );
+ cx.update_editor(|view, cx| view.handle_input("*", cx));
+ cx.assert_editor_state(
+ &"
+ /*ห */
+ *ห
+ "
+ .unindent(),
+ );
+
+ // Don't autoclose if the next character isn't whitespace and isn't
+ // listed in the language's "autoclose_before" section.
+ cx.set_state("หa b");
+ cx.update_editor(|view, cx| view.handle_input("{", cx));
+ cx.assert_editor_state("{หa b");
+
+ // Don't autoclose if `close` is false for the bracket pair
+ cx.set_state("ห");
+ cx.update_editor(|view, cx| view.handle_input("[", cx));
+ cx.assert_editor_state("[ห");
+
+ // Surround with brackets if text is selected
+ cx.set_state("ยซaหยป b");
+ cx.update_editor(|view, cx| view.handle_input("{", cx));
+ cx.assert_editor_state("{ยซaหยป} b");
+
+ // Autclose pair where the start and end characters are the same
+ cx.set_state("aห");
+ cx.update_editor(|view, cx| view.handle_input("\"", cx));
+ cx.assert_editor_state("a\"ห\"");
+ cx.update_editor(|view, cx| view.handle_input("\"", cx));
+ cx.assert_editor_state("a\"\"ห");
+}
+
+#[gpui::test]
+async fn test_autoclose_with_embedded_language(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let html_language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "HTML".into(),
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "<".into(),
+ end: ">".into(),
+ close: true,
+ ..Default::default()
+ },
+ BracketPair {
+ start: "{".into(),
+ end: "}".into(),
+ close: true,
+ ..Default::default()
+ },
+ BracketPair {
+ start: "(".into(),
+ end: ")".into(),
+ close: true,
+ ..Default::default()
+ },
+ ],
+ ..Default::default()
+ },
+ autoclose_before: "})]>".into(),
+ ..Default::default()
+ },
+ Some(tree_sitter_html::language()),
+ )
+ .with_injection_query(
+ r#"
+ (script_element
+ (raw_text) @content
+ (#set! "language" "javascript"))
+ "#,
+ )
+ .unwrap(),
+ );
+
+ let javascript_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "JavaScript".into(),
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "/*".into(),
+ end: " */".into(),
+ close: true,
+ ..Default::default()
+ },
+ BracketPair {
+ start: "{".into(),
+ end: "}".into(),
+ close: true,
+ ..Default::default()
+ },
+ BracketPair {
+ start: "(".into(),
+ end: ")".into(),
+ close: true,
+ ..Default::default()
+ },
+ ],
+ ..Default::default()
+ },
+ autoclose_before: "})]>".into(),
+ ..Default::default()
+ },
+ Some(tree_sitter_typescript::language_tsx()),
+ ));
+
+ let registry = Arc::new(LanguageRegistry::test());
+ registry.add(html_language.clone());
+ registry.add(javascript_language.clone());
+
+ cx.update_buffer(|buffer, cx| {
+ buffer.set_language_registry(registry);
+ buffer.set_language(Some(html_language), cx);
+ });
+
+ cx.set_state(
+ &r#"
+ <body>ห
+ <script>
+ var x = 1;ห
+ </script>
+ </body>ห
+ "#
+ .unindent(),
+ );
+
+ // Precondition: different languages are active at different locations.
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let cursors = editor.selections.ranges::<usize>(cx);
+ let languages = cursors
+ .iter()
+ .map(|c| snapshot.language_at(c.start).unwrap().name())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ languages,
+ &["HTML".into(), "JavaScript".into(), "HTML".into()]
+ );
+ });
+
+ // Angle brackets autoclose in HTML, but not JavaScript.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("<", cx);
+ editor.handle_input("a", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><aห>
+ <script>
+ var x = 1;<aห
+ </script>
+ </body><aห>
+ "#
+ .unindent(),
+ );
+
+ // Curly braces and parens autoclose in both HTML and JavaScript.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input(" b=", cx);
+ editor.handle_input("{", cx);
+ editor.handle_input("c", cx);
+ editor.handle_input("(", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><a b={c(ห)}>
+ <script>
+ var x = 1;<a b={c(ห)}
+ </script>
+ </body><a b={c(ห)}>
+ "#
+ .unindent(),
+ );
+
+ // Brackets that were already autoclosed are skipped.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input(")", cx);
+ editor.handle_input("d", cx);
+ editor.handle_input("}", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><a b={c()d}ห>
+ <script>
+ var x = 1;<a b={c()d}ห
+ </script>
+ </body><a b={c()d}ห>
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| {
+ editor.handle_input(">", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><a b={c()d}>ห
+ <script>
+ var x = 1;<a b={c()d}>ห
+ </script>
+ </body><a b={c()d}>ห
+ "#
+ .unindent(),
+ );
+
+ // Reset
+ cx.set_state(
+ &r#"
+ <body>ห
+ <script>
+ var x = 1;ห
+ </script>
+ </body>ห
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("<", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body><ห>
+ <script>
+ var x = 1;<ห
+ </script>
+ </body><ห>
+ "#
+ .unindent(),
+ );
+
+ // When backspacing, the closing angle brackets are removed.
+ cx.update_editor(|editor, cx| {
+ editor.backspace(&Backspace, cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body>ห
+ <script>
+ var x = 1;ห
+ </script>
+ </body>ห
+ "#
+ .unindent(),
+ );
+
+ // Block comments autoclose in JavaScript, but not HTML.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("/", cx);
+ editor.handle_input("*", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ <body>/*ห
+ <script>
+ var x = 1;/*ห */
+ </script>
+ </body>/*ห
+ "#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+async fn test_autoclose_with_overrides(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let rust_language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ brackets: serde_json::from_value(json!([
+ { "start": "{", "end": "}", "close": true, "newline": true },
+ { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
+ ]))
+ .unwrap(),
+ autoclose_before: "})]>".into(),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_override_query("(string_literal) @string")
+ .unwrap(),
+ );
+
+ let registry = Arc::new(LanguageRegistry::test());
+ registry.add(rust_language.clone());
+
+ cx.update_buffer(|buffer, cx| {
+ buffer.set_language_registry(registry);
+ buffer.set_language(Some(rust_language), cx);
+ });
+
+ cx.set_state(
+ &r#"
+ let x = ห
+ "#
+ .unindent(),
+ );
+
+ // Inserting a quotation mark. A closing quotation mark is automatically inserted.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("\"", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ let x = "ห"
+ "#
+ .unindent(),
+ );
+
+ // Inserting another quotation mark. The cursor moves across the existing
+ // automatically-inserted quotation mark.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("\"", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ let x = ""ห
+ "#
+ .unindent(),
+ );
+
+ // Reset
+ cx.set_state(
+ &r#"
+ let x = ห
+ "#
+ .unindent(),
+ );
+
+ // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
+ cx.update_editor(|editor, cx| {
+ editor.handle_input("\"", cx);
+ editor.handle_input(" ", cx);
+ editor.move_left(&Default::default(), cx);
+ editor.handle_input("\\", cx);
+ editor.handle_input("\"", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ let x = "\"ห "
+ "#
+ .unindent(),
+ );
+
+ // Inserting a closing quotation mark at the position of an automatically-inserted quotation
+ // mark. Nothing is inserted.
+ cx.update_editor(|editor, cx| {
+ editor.move_right(&Default::default(), cx);
+ editor.handle_input("\"", cx);
+ });
+ cx.assert_editor_state(
+ &r#"
+ let x = "\" "ห
+ "#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language = Arc::new(Language::new(
+ LanguageConfig {
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ },
+ BracketPair {
+ start: "/* ".to_string(),
+ end: "*/".to_string(),
+ close: true,
+ ..Default::default()
+ },
+ ],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ ));
+
+ let text = r#"
+ a
+ b
+ c
+ "#
+ .unindent();
+
+ let buffer =
+ cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ .await;
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1),
+ ])
+ });
+
+ view.handle_input("{", cx);
+ view.handle_input("{", cx);
+ view.handle_input("{", cx);
+ assert_eq!(
+ view.text(cx),
+ "
+ {{{a}}}
+ {{{b}}}
+ {{{c}}}
+ "
+ .unindent()
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 3)..DisplayPoint::new(0, 4),
+ DisplayPoint::new(1, 3)..DisplayPoint::new(1, 4),
+ DisplayPoint::new(2, 3)..DisplayPoint::new(2, 4)
+ ]
+ );
+
+ view.undo(&Undo, cx);
+ view.undo(&Undo, cx);
+ view.undo(&Undo, cx);
+ assert_eq!(
+ view.text(cx),
+ "
+ a
+ b
+ c
+ "
+ .unindent()
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
+ ]
+ );
+
+ // Ensure inserting the first character of a multi-byte bracket pair
+ // doesn't surround the selections with the bracket.
+ view.handle_input("/", cx);
+ assert_eq!(
+ view.text(cx),
+ "
+ /
+ /
+ /
+ "
+ .unindent()
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
+ ]
+ );
+
+ view.undo(&Undo, cx);
+ assert_eq!(
+ view.text(cx),
+ "
+ a
+ b
+ c
+ "
+ .unindent()
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 0)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 0)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(2, 0)..DisplayPoint::new(2, 1)
+ ]
+ );
+
+ // Ensure inserting the last character of a multi-byte bracket pair
+ // doesn't surround the selections with the bracket.
+ view.handle_input("*", cx);
+ assert_eq!(
+ view.text(cx),
+ "
+ *
+ *
+ *
+ "
+ .unindent()
+ );
+ assert_eq!(
+ view.selections.display_ranges(cx),
+ [
+ DisplayPoint::new(0, 1)..DisplayPoint::new(0, 1),
+ DisplayPoint::new(1, 1)..DisplayPoint::new(1, 1),
+ DisplayPoint::new(2, 1)..DisplayPoint::new(2, 1)
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language = Arc::new(Language::new(
+ LanguageConfig {
+ brackets: BracketPairConfig {
+ pairs: vec![BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ }],
+ ..Default::default()
+ },
+ autoclose_before: "}".to_string(),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ ));
+
+ let text = r#"
+ a
+ b
+ c
+ "#
+ .unindent();
+
+ let buffer =
+ cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ editor
+ .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ .await;
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([
+ Point::new(0, 1)..Point::new(0, 1),
+ Point::new(1, 1)..Point::new(1, 1),
+ Point::new(2, 1)..Point::new(2, 1),
+ ])
+ });
+
+ editor.handle_input("{", cx);
+ editor.handle_input("{", cx);
+ editor.handle_input("_", cx);
+ assert_eq!(
+ editor.text(cx),
+ "
+ a{{_}}
+ b{{_}}
+ c{{_}}
+ "
+ .unindent()
+ );
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [
+ Point::new(0, 4)..Point::new(0, 4),
+ Point::new(1, 4)..Point::new(1, 4),
+ Point::new(2, 4)..Point::new(2, 4)
+ ]
+ );
+
+ editor.backspace(&Default::default(), cx);
+ editor.backspace(&Default::default(), cx);
+ assert_eq!(
+ editor.text(cx),
+ "
+ a{}
+ b{}
+ c{}
+ "
+ .unindent()
+ );
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [
+ Point::new(0, 2)..Point::new(0, 2),
+ Point::new(1, 2)..Point::new(1, 2),
+ Point::new(2, 2)..Point::new(2, 2)
+ ]
+ );
+
+ editor.delete_to_previous_word_start(&Default::default(), cx);
+ assert_eq!(
+ editor.text(cx),
+ "
+ a
+ b
+ c
+ "
+ .unindent()
+ );
+ assert_eq!(
+ editor.selections.ranges::<Point>(cx),
+ [
+ Point::new(0, 1)..Point::new(0, 1),
+ Point::new(1, 1)..Point::new(1, 1),
+ Point::new(2, 1)..Point::new(2, 1)
+ ]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_snippets(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let (text, insertion_ranges) = marked_text_ranges(
+ indoc! {"
+ a.ห b
+ a.ห b
+ a.ห b
+ "},
+ false,
+ );
+
+ let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
+ let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+
+ editor.update(cx, |editor, cx| {
+ let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
+
+ editor
+ .insert_snippet(&insertion_ranges, snippet, cx)
+ .unwrap();
+
+ fn assert(editor: &mut Editor, cx: &mut ViewContext<Editor>, marked_text: &str) {
+ let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
+ assert_eq!(editor.text(cx), expected_text);
+ assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
+ }
+
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ "},
+ );
+
+ // Can't move earlier than the first tab stop
+ assert!(!editor.move_to_prev_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ "},
+ );
+
+ assert!(editor.move_to_next_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, ยซtwoยป, three) b
+ a.f(one, ยซtwoยป, three) b
+ a.f(one, ยซtwoยป, three) b
+ "},
+ );
+
+ editor.move_to_prev_snippet_tabstop(cx);
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ a.f(ยซoneยป, two, ยซthreeยป) b
+ "},
+ );
+
+ assert!(editor.move_to_next_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, ยซtwoยป, three) b
+ a.f(one, ยซtwoยป, three) b
+ a.f(one, ยซtwoยป, three) b
+ "},
+ );
+ assert!(editor.move_to_next_snippet_tabstop(cx));
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, two, three)ห b
+ a.f(one, two, three)ห b
+ a.f(one, two, three)ห b
+ "},
+ );
+
+ // As soon as the last tab stop is reached, snippet state is gone
+ editor.move_to_prev_snippet_tabstop(cx);
+ assert(
+ editor,
+ cx,
+ indoc! {"
+ a.f(one, two, three)ห b
+ a.f(one, two, three)ห b
+ a.f(one, two, three)ห b
+ "},
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ document_formatting_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_file("/file.rs", Default::default()).await;
+
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let buffer = project
+ .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+ .await
+ .unwrap();
+
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+ let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+ fake_server
+ .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 4);
+ Ok(Some(vec![lsp::TextEdit::new(
+ lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+ ", ".to_string(),
+ )]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one, two\nthree\n"
+ );
+ assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+ // Ensure we can still save even if formatting hangs.
+ fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ futures::future::pending::<()>().await;
+ unreachable!()
+ });
+ let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+ cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one\ntwo\nthree\n"
+ );
+ assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+ // Set rust language override and assert overridden tabsize is sent to language server
+ update_test_language_settings(cx, |settings| {
+ settings.languages.insert(
+ "Rust".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(8),
+ ..Default::default()
+ },
+ );
+ });
+
+ let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+ fake_server
+ .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 8);
+ Ok(Some(vec![]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_file("/file.rs", Default::default()).await;
+
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let buffer = project
+ .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+ .await
+ .unwrap();
+
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+ let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+ fake_server
+ .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 4);
+ Ok(Some(vec![lsp::TextEdit::new(
+ lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+ ", ".to_string(),
+ )]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one, two\nthree\n"
+ );
+ assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ assert!(cx.read(|cx| editor.is_dirty(cx)));
+
+ // Ensure we can still save even if formatting hangs.
+ fake_server.handle_request::<lsp::request::RangeFormatting, _, _>(
+ move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ futures::future::pending::<()>().await;
+ unreachable!()
+ },
+ );
+ let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+ cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one\ntwo\nthree\n"
+ );
+ assert!(!cx.read(|cx| editor.is_dirty(cx)));
+
+ // Set rust language override and assert overridden tabsize is sent to language server
+ update_test_language_settings(cx, |settings| {
+ settings.languages.insert(
+ "Rust".into(),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(8),
+ ..Default::default()
+ },
+ );
+ });
+
+ let save = editor.update(cx, |editor, cx| editor.save(project.clone(), cx));
+ fake_server
+ .handle_request::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 8);
+ Ok(Some(vec![]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ save.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.formatter = Some(language_settings::Formatter::LanguageServer)
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ // Enable Prettier formatting for the same buffer, and ensure
+ // LSP is called instead of Prettier.
+ prettier_parser_name: Some("test_parser".to_string()),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ document_formatting_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_file("/file.rs", Default::default()).await;
+
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::new(language));
+ });
+ let buffer = project
+ .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+ .await
+ .unwrap();
+
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+
+ let format = editor.update(cx, |editor, cx| {
+ editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+ });
+ fake_server
+ .handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ assert_eq!(params.options.tab_size, 4);
+ Ok(Some(vec![lsp::TextEdit::new(
+ lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
+ ", ".to_string(),
+ )]))
+ })
+ .next()
+ .await;
+ cx.foreground().start_waiting();
+ format.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one, two\nthree\n"
+ );
+
+ editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx));
+ // Ensure we don't lock if formatting hangs.
+ fake_server.handle_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/file.rs").unwrap()
+ );
+ futures::future::pending::<()>().await;
+ unreachable!()
+ });
+ let format = editor.update(cx, |editor, cx| {
+ editor.perform_format(project, FormatTrigger::Manual, cx)
+ });
+ cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
+ cx.foreground().start_waiting();
+ format.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ "one\ntwo\nthree\n"
+ );
+}
+
+#[gpui::test]
+async fn test_concurrent_format_requests(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ document_formatting_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ one.twoห
+ "});
+
+ // The format request takes a long time. When it completes, it inserts
+ // a newline and an indent before the `.`
+ cx.lsp
+ .handle_request::<lsp::request::Formatting, _, _>(move |_, cx| {
+ let executor = cx.background();
+ async move {
+ executor.timer(Duration::from_millis(100)).await;
+ Ok(Some(vec![lsp::TextEdit {
+ range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
+ new_text: "\n ".into(),
+ }]))
+ }
+ });
+
+ // Submit a format request.
+ let format_1 = cx
+ .update_editor(|editor, cx| editor.format(&Format, cx))
+ .unwrap();
+ cx.foreground().run_until_parked();
+
+ // Submit a second format request.
+ let format_2 = cx
+ .update_editor(|editor, cx| editor.format(&Format, cx))
+ .unwrap();
+ cx.foreground().run_until_parked();
+
+ // Wait for both format requests to complete
+ cx.foreground().advance_clock(Duration::from_millis(200));
+ cx.foreground().start_waiting();
+ format_1.await.unwrap();
+ cx.foreground().start_waiting();
+ format_2.await.unwrap();
+
+ // The formatting edits only happens once.
+ cx.assert_editor_state(indoc! {"
+ one
+ .twoห
+ "});
+}
+
+#[gpui::test]
+async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.formatter = Some(language_settings::Formatter::Auto)
+ });
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ document_formatting_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Set up a buffer white some trailing whitespace and no trailing newline.
+ cx.set_state(
+ &[
+ "one ", //
+ "twoห", //
+ "three ", //
+ "four", //
+ ]
+ .join("\n"),
+ );
+
+ // Submit a format request.
+ let format = cx
+ .update_editor(|editor, cx| editor.format(&Format, cx))
+ .unwrap();
+
+ // Record which buffer changes have been sent to the language server
+ let buffer_changes = Arc::new(Mutex::new(Vec::new()));
+ cx.lsp
+ .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
+ let buffer_changes = buffer_changes.clone();
+ move |params, _| {
+ buffer_changes.lock().extend(
+ params
+ .content_changes
+ .into_iter()
+ .map(|e| (e.range.unwrap(), e.text)),
+ );
+ }
+ });
+
+ // Handle formatting requests to the language server.
+ cx.lsp.handle_request::<lsp::request::Formatting, _, _>({
+ let buffer_changes = buffer_changes.clone();
+ move |_, _| {
+ // When formatting is requested, trailing whitespace has already been stripped,
+ // and the trailing newline has already been added.
+ assert_eq!(
+ &buffer_changes.lock()[1..],
+ &[
+ (
+ lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
+ "".into()
+ ),
+ (
+ lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
+ "".into()
+ ),
+ (
+ lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
+ "\n".into()
+ ),
+ ]
+ );
+
+ // Insert blank lines between each line of the buffer.
+ async move {
+ Ok(Some(vec![
+ lsp::TextEdit {
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
+ new_text: "\n".into(),
+ },
+ lsp::TextEdit {
+ range: lsp::Range::new(lsp::Position::new(2, 0), lsp::Position::new(2, 0)),
+ new_text: "\n".into(),
+ },
+ ]))
+ }
+ }
+ });
+
+ // After formatting the buffer, the trailing whitespace is stripped,
+ // a newline is appended, and the edits provided by the language server
+ // have been applied.
+ format.await.unwrap();
+ cx.assert_editor_state(
+ &[
+ "one", //
+ "", //
+ "twoห", //
+ "", //
+ "three", //
+ "four", //
+ "", //
+ ]
+ .join("\n"),
+ );
+
+ // Undoing the formatting undoes the trailing whitespace removal, the
+ // trailing newline, and the LSP edits.
+ cx.update_buffer(|buffer, cx| buffer.undo(cx));
+ cx.assert_editor_state(
+ &[
+ "one ", //
+ "twoห", //
+ "three ", //
+ "four", //
+ ]
+ .join("\n"),
+ );
+}
+
+#[gpui::test]
+async fn test_completion(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+ resolve_provider: Some(true),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ oneห
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec!["first_completion", "second_completion"],
+ )
+ .await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor.context_menu_next(&Default::default(), cx);
+ editor
+ .confirm_completion(&ConfirmCompletion::default(), cx)
+ .unwrap()
+ });
+ cx.assert_editor_state(indoc! {"
+ one.second_completionห
+ two
+ three
+ "});
+
+ handle_resolve_completion_request(
+ &mut cx,
+ Some(vec![
+ (
+ //This overlaps with the primary completion edit which is
+ //misbehavior from the LSP spec, test that we filter it out
+ indoc! {"
+ one.second_หcompletion
+ two
+ threeห
+ "},
+ "overlapping additional edit",
+ ),
+ (
+ indoc! {"
+ one.second_completion
+ two
+ threeห
+ "},
+ "\nadditional edit",
+ ),
+ ]),
+ )
+ .await;
+ apply_additional_edits.await.unwrap();
+ cx.assert_editor_state(indoc! {"
+ one.second_completionห
+ two
+ three
+ additional edit
+ "});
+
+ cx.set_state(indoc! {"
+ one.second_completion
+ twoห
+ threeห
+ additional edit
+ "});
+ cx.simulate_keystroke(" ");
+ assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+ cx.simulate_keystroke("s");
+ assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+
+ cx.assert_editor_state(indoc! {"
+ one.second_completion
+ two sห
+ three sห
+ additional edit
+ "});
+ handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.second_completion
+ two s
+ three <s|>
+ additional edit
+ "},
+ vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+ )
+ .await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+
+ cx.simulate_keystroke("i");
+
+ handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.second_completion
+ two si
+ three <si|>
+ additional edit
+ "},
+ vec!["fourth_completion", "fifth_completion", "sixth_completion"],
+ )
+ .await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor
+ .confirm_completion(&ConfirmCompletion::default(), cx)
+ .unwrap()
+ });
+ cx.assert_editor_state(indoc! {"
+ one.second_completion
+ two sixth_completionห
+ three sixth_completionห
+ additional edit
+ "});
+
+ handle_resolve_completion_request(&mut cx, None).await;
+ apply_additional_edits.await.unwrap();
+
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|settings, cx| {
+ settings.update_user_settings::<EditorSettings>(cx, |settings| {
+ settings.show_completions_on_input = Some(false);
+ });
+ })
+ });
+ cx.set_state("editorห");
+ cx.simulate_keystroke(".");
+ assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+ cx.simulate_keystroke("c");
+ cx.simulate_keystroke("l");
+ cx.simulate_keystroke("o");
+ cx.assert_editor_state("editor.cloห");
+ assert!(cx.editor(|e, _| e.context_menu.read().is_none()));
+ cx.update_editor(|editor, cx| {
+ editor.show_completions(&ShowCompletions, cx);
+ });
+ handle_completion_request(&mut cx, "editor.<clo|>", vec!["close", "clobber"]).await;
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor
+ .confirm_completion(&ConfirmCompletion::default(), cx)
+ .unwrap()
+ });
+ cx.assert_editor_state("editor.closeห");
+ handle_resolve_completion_request(&mut cx, None).await;
+ apply_additional_edits.await.unwrap();
+}
+
+#[gpui::test]
+async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+ let mut cx = EditorTestContext::new(cx).await;
+ let language = Arc::new(Language::new(
+ LanguageConfig {
+ line_comment: Some("// ".into()),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ ));
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
+
+ // If multiple selections intersect a line, the line is only toggled once.
+ cx.set_state(indoc! {"
+ fn a() {
+ ยซ//b();
+ หยป// ยซc();
+ //หยป d();
+ }
+ "});
+
+ cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+ cx.assert_editor_state(indoc! {"
+ fn a() {
+ ยซb();
+ c();
+ หยป d();
+ }
+ "});
+
+ // The comment prefix is inserted at the same column for every line in a
+ // selection.
+ cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+ cx.assert_editor_state(indoc! {"
+ fn a() {
+ // ยซb();
+ // c();
+ หยป// d();
+ }
+ "});
+
+ // If a selection ends at the beginning of a line, that line is not toggled.
+ cx.set_selections_state(indoc! {"
+ fn a() {
+ // b();
+ ยซ// c();
+ หยป // d();
+ }
+ "});
+
+ cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+ cx.assert_editor_state(indoc! {"
+ fn a() {
+ // b();
+ ยซc();
+ หยป // d();
+ }
+ "});
+
+ // If a selection span a single line and is empty, the line is toggled.
+ cx.set_state(indoc! {"
+ fn a() {
+ a();
+ b();
+ ห
+ }
+ "});
+
+ cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+ cx.assert_editor_state(indoc! {"
+ fn a() {
+ a();
+ b();
+ //โขห
+ }
+ "});
+
+ // If a selection span multiple lines, empty lines are not toggled.
+ cx.set_state(indoc! {"
+ fn a() {
+ ยซa();
+
+ c();หยป
+ }
+ "});
+
+ cx.update_editor(|e, cx| e.toggle_comments(&ToggleComments::default(), cx));
+
+ cx.assert_editor_state(indoc! {"
+ fn a() {
+ // ยซa();
+
+ // c();หยป
+ }
+ "});
+}
+
+#[gpui::test]
+async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language = Arc::new(Language::new(
+ LanguageConfig {
+ line_comment: Some("// ".into()),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ ));
+
+ let registry = Arc::new(LanguageRegistry::test());
+ registry.add(language.clone());
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| {
+ buffer.set_language_registry(registry);
+ buffer.set_language(Some(language), cx);
+ });
+
+ let toggle_comments = &ToggleComments {
+ advance_downwards: true,
+ };
+
+ // Single cursor on one line -> advance
+ // Cursor moves horizontally 3 characters as well on non-blank line
+ cx.set_state(indoc!(
+ "fn a() {
+ หdog();
+ cat();
+ }"
+ ));
+ cx.update_editor(|editor, cx| {
+ editor.toggle_comments(toggle_comments, cx);
+ });
+ cx.assert_editor_state(indoc!(
+ "fn a() {
+ // dog();
+ catห();
+ }"
+ ));
+
+ // Single selection on one line -> don't advance
+ cx.set_state(indoc!(
+ "fn a() {
+ ยซdog()หยป;
+ cat();
+ }"
+ ));
+ cx.update_editor(|editor, cx| {
+ editor.toggle_comments(toggle_comments, cx);
+ });
+ cx.assert_editor_state(indoc!(
+ "fn a() {
+ // ยซdog()หยป;
+ cat();
+ }"
+ ));
+
+ // Multiple cursors on one line -> advance
+ cx.set_state(indoc!(
+ "fn a() {
+ หdหog();
+ cat();
+ }"
+ ));
+ cx.update_editor(|editor, cx| {
+ editor.toggle_comments(toggle_comments, cx);
+ });
+ cx.assert_editor_state(indoc!(
+ "fn a() {
+ // dog();
+ catห(ห);
+ }"
+ ));
+
+ // Multiple cursors on one line, with selection -> don't advance
+ cx.set_state(indoc!(
+ "fn a() {
+ หdหogยซ()หยป;
+ cat();
+ }"
+ ));
+ cx.update_editor(|editor, cx| {
+ editor.toggle_comments(toggle_comments, cx);
+ });
+ cx.assert_editor_state(indoc!(
+ "fn a() {
+ // หdหogยซ()หยป;
+ cat();
+ }"
+ ));
+
+ // Single cursor on one line -> advance
+ // Cursor moves to column 0 on blank line
+ cx.set_state(indoc!(
+ "fn a() {
+ หdog();
+
+ cat();
+ }"
+ ));
+ cx.update_editor(|editor, cx| {
+ editor.toggle_comments(toggle_comments, cx);
+ });
+ cx.assert_editor_state(indoc!(
+ "fn a() {
+ // dog();
+ ห
+ cat();
+ }"
+ ));
+
+ // Single cursor on one line -> advance
+ // Cursor starts and ends at column 0
+ cx.set_state(indoc!(
+ "fn a() {
+ ห dog();
+ cat();
+ }"
+ ));
+ cx.update_editor(|editor, cx| {
+ editor.toggle_comments(toggle_comments, cx);
+ });
+ cx.assert_editor_state(indoc!(
+ "fn a() {
+ // dog();
+ ห cat();
+ }"
+ ));
+}
+
+#[gpui::test]
+async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let html_language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ name: "HTML".into(),
+ block_comment: Some(("<!-- ".into(), " -->".into())),
+ ..Default::default()
+ },
+ Some(tree_sitter_html::language()),
+ )
+ .with_injection_query(
+ r#"
+ (script_element
+ (raw_text) @content
+ (#set! "language" "javascript"))
+ "#,
+ )
+ .unwrap(),
+ );
+
+ let javascript_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "JavaScript".into(),
+ line_comment: Some("// ".into()),
+ ..Default::default()
+ },
+ Some(tree_sitter_typescript::language_tsx()),
+ ));
+
+ let registry = Arc::new(LanguageRegistry::test());
+ registry.add(html_language.clone());
+ registry.add(javascript_language.clone());
+
+ cx.update_buffer(|buffer, cx| {
+ buffer.set_language_registry(registry);
+ buffer.set_language(Some(html_language), cx);
+ });
+
+ // Toggle comments for empty selections
+ cx.set_state(
+ &r#"
+ <p>A</p>ห
+ <p>B</p>ห
+ <p>C</p>ห
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ <!-- <p>A</p>ห -->
+ <!-- <p>B</p>ห -->
+ <!-- <p>C</p>ห -->
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ <p>A</p>ห
+ <p>B</p>ห
+ <p>C</p>ห
+ "#
+ .unindent(),
+ );
+
+ // Toggle comments for mixture of empty and non-empty selections, where
+ // multiple selections occupy a given line.
+ cx.set_state(
+ &r#"
+ <p>Aยซ</p>
+ <p>หยปB</p>ห
+ <p>Cยซ</p>
+ <p>หยปD</p>ห
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ <!-- <p>Aยซ</p>
+ <p>หยปB</p>ห -->
+ <!-- <p>Cยซ</p>
+ <p>หยปD</p>ห -->
+ "#
+ .unindent(),
+ );
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ <p>Aยซ</p>
+ <p>หยปB</p>ห
+ <p>Cยซ</p>
+ <p>หยปD</p>ห
+ "#
+ .unindent(),
+ );
+
+ // Toggle comments when different languages are active for different
+ // selections.
+ cx.set_state(
+ &r#"
+ ห<script>
+ หvar x = new Y();
+ ห</script>
+ "#
+ .unindent(),
+ );
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| editor.toggle_comments(&ToggleComments::default(), cx));
+ cx.assert_editor_state(
+ &r#"
+ <!-- ห<script> -->
+ // หvar x = new Y();
+ <!-- ห</script> -->
+ "#
+ .unindent(),
+ );
+}
+
+#[gpui::test]
+fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(0, 4),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(1, 0)..Point::new(1, 4),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
+ multibuffer
+ });
+
+ let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+ view.update(cx, |view, cx| {
+ assert_eq!(view.text(cx), "aaaa\nbbbb");
+ view.change_selections(None, cx, |s| {
+ s.select_ranges([
+ Point::new(0, 0)..Point::new(0, 0),
+ Point::new(1, 0)..Point::new(1, 0),
+ ])
+ });
+
+ view.handle_input("X", cx);
+ assert_eq!(view.text(cx), "Xaaaa\nXbbbb");
+ assert_eq!(
+ view.selections.ranges(cx),
+ [
+ Point::new(0, 1)..Point::new(0, 1),
+ Point::new(1, 1)..Point::new(1, 1),
+ ]
+ );
+
+ // Ensure the cursor's head is respected when deleting across an excerpt boundary.
+ view.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
+ });
+ view.backspace(&Default::default(), cx);
+ assert_eq!(view.text(cx), "Xa\nbbb");
+ assert_eq!(
+ view.selections.ranges(cx),
+ [Point::new(1, 0)..Point::new(1, 0)]
+ );
+
+ view.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
+ });
+ view.backspace(&Default::default(), cx);
+ assert_eq!(view.text(cx), "X\nbb");
+ assert_eq!(
+ view.selections.ranges(cx),
+ [Point::new(0, 1)..Point::new(0, 1)]
+ );
+ });
+}
+
+#[gpui::test]
+fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let markers = vec![('[', ']').into(), ('(', ')').into()];
+ let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
+ indoc! {"
+ [aaaa
+ (bbbb]
+ cccc)",
+ },
+ markers.clone(),
+ );
+ let excerpt_ranges = markers.into_iter().map(|marker| {
+ let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
+ ExcerptRange {
+ context,
+ primary: None,
+ }
+ });
+ let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text));
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
+ multibuffer
+ });
+
+ let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+ view.update(cx, |view, cx| {
+ let (expected_text, selection_ranges) = marked_text_ranges(
+ indoc! {"
+ aaaa
+ bหbbb
+ bหbbหb
+ cccc"
+ },
+ true,
+ );
+ assert_eq!(view.text(cx), expected_text);
+ view.change_selections(None, cx, |s| s.select_ranges(selection_ranges));
+
+ view.handle_input("X", cx);
+
+ let (expected_text, expected_selections) = marked_text_ranges(
+ indoc! {"
+ aaaa
+ bXหbbXb
+ bXหbbXหb
+ cccc"
+ },
+ false,
+ );
+ assert_eq!(view.text(cx), expected_text);
+ assert_eq!(view.selections.ranges(cx), expected_selections);
+
+ view.newline(&Newline, cx);
+ let (expected_text, expected_selections) = marked_text_ranges(
+ indoc! {"
+ aaaa
+ bX
+ หbbX
+ b
+ bX
+ หbbX
+ หb
+ cccc"
+ },
+ false,
+ );
+ assert_eq!(view.text(cx), expected_text);
+ assert_eq!(view.selections.ranges(cx), expected_selections);
+ });
+}
+
+#[gpui::test]
+fn test_refresh_selections(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
+ let mut excerpt1_id = None;
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ excerpt1_id = multibuffer
+ .push_excerpts(
+ buffer.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 4),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(1, 0)..Point::new(2, 4),
+ primary: None,
+ },
+ ],
+ cx,
+ )
+ .into_iter()
+ .next();
+ assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
+ multibuffer
+ });
+
+ let editor = cx
+ .add_window(|cx| {
+ let mut editor = build_editor(multibuffer.clone(), cx);
+ let snapshot = editor.snapshot(cx);
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
+ });
+ editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx);
+ assert_eq!(
+ editor.selections.ranges(cx),
+ [
+ Point::new(1, 3)..Point::new(1, 3),
+ Point::new(2, 1)..Point::new(2, 1),
+ ]
+ );
+ editor
+ })
+ .root(cx);
+
+ // Refreshing selections is a no-op when excerpts haven't changed.
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.refresh());
+ assert_eq!(
+ editor.selections.ranges(cx),
+ [
+ Point::new(1, 3)..Point::new(1, 3),
+ Point::new(2, 1)..Point::new(2, 1),
+ ]
+ );
+ });
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
+ });
+ editor.update(cx, |editor, cx| {
+ // Removing an excerpt causes the first selection to become degenerate.
+ assert_eq!(
+ editor.selections.ranges(cx),
+ [
+ Point::new(0, 0)..Point::new(0, 0),
+ Point::new(0, 1)..Point::new(0, 1)
+ ]
+ );
+
+ // Refreshing selections will relocate the first selection to the original buffer
+ // location.
+ editor.change_selections(None, cx, |s| s.refresh());
+ assert_eq!(
+ editor.selections.ranges(cx),
+ [
+ Point::new(0, 1)..Point::new(0, 1),
+ Point::new(0, 3)..Point::new(0, 3)
+ ]
+ );
+ assert!(editor.selections.pending_anchor().is_some());
+ });
+}
+
+#[gpui::test]
+fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a')));
+ let mut excerpt1_id = None;
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ excerpt1_id = multibuffer
+ .push_excerpts(
+ buffer.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 4),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(1, 0)..Point::new(2, 4),
+ primary: None,
+ },
+ ],
+ cx,
+ )
+ .into_iter()
+ .next();
+ assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
+ multibuffer
+ });
+
+ let editor = cx
+ .add_window(|cx| {
+ let mut editor = build_editor(multibuffer.clone(), cx);
+ let snapshot = editor.snapshot(cx);
+ editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx);
+ assert_eq!(
+ editor.selections.ranges(cx),
+ [Point::new(1, 3)..Point::new(1, 3)]
+ );
+ editor
+ })
+ .root(cx);
+
+ multibuffer.update(cx, |multibuffer, cx| {
+ multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
+ });
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ editor.selections.ranges(cx),
+ [Point::new(0, 0)..Point::new(0, 0)]
+ );
+
+ // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
+ editor.change_selections(None, cx, |s| s.refresh());
+ assert_eq!(
+ editor.selections.ranges(cx),
+ [Point::new(0, 3)..Point::new(0, 3)]
+ );
+ assert!(editor.selections.pending_anchor().is_some());
+ });
+}
+
+#[gpui::test]
+async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language = Arc::new(
+ Language::new(
+ LanguageConfig {
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ },
+ BracketPair {
+ start: "/* ".to_string(),
+ end: " */".to_string(),
+ close: true,
+ newline: true,
+ },
+ ],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_indents_query("")
+ .unwrap(),
+ );
+
+ let text = concat!(
+ "{ }\n", //
+ " x\n", //
+ " /* */\n", //
+ "x\n", //
+ "{{} }\n", //
+ );
+
+ let buffer =
+ cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx));
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
+ .await;
+
+ view.update(cx, |view, cx| {
+ view.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(0, 2)..DisplayPoint::new(0, 3),
+ DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5),
+ DisplayPoint::new(4, 4)..DisplayPoint::new(4, 4),
+ ])
+ });
+ view.newline(&Newline, cx);
+
+ assert_eq!(
+ view.buffer().read(cx).read(cx).text(),
+ concat!(
+ "{ \n", // Suppress rustfmt
+ "\n", //
+ "}\n", //
+ " x\n", //
+ " /* \n", //
+ " \n", //
+ " */\n", //
+ "x\n", //
+ "{{} \n", //
+ "}\n", //
+ )
+ );
+ });
+}
+
+#[gpui::test]
+fn test_highlighted_ranges(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
+ build_editor(buffer.clone(), cx)
+ })
+ .root(cx);
+
+ editor.update(cx, |editor, cx| {
+ struct Type1;
+ struct Type2;
+
+ let buffer = editor.buffer.read(cx).snapshot(cx);
+
+ let anchor_range =
+ |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
+
+ editor.highlight_background::<Type1>(
+ vec![
+ anchor_range(Point::new(2, 1)..Point::new(2, 3)),
+ anchor_range(Point::new(4, 2)..Point::new(4, 4)),
+ anchor_range(Point::new(6, 3)..Point::new(6, 5)),
+ anchor_range(Point::new(8, 4)..Point::new(8, 6)),
+ ],
+ |_| Color::red(),
+ cx,
+ );
+ editor.highlight_background::<Type2>(
+ vec![
+ anchor_range(Point::new(3, 2)..Point::new(3, 5)),
+ anchor_range(Point::new(5, 3)..Point::new(5, 6)),
+ anchor_range(Point::new(7, 4)..Point::new(7, 7)),
+ anchor_range(Point::new(9, 5)..Point::new(9, 8)),
+ ],
+ |_| Color::green(),
+ cx,
+ );
+
+ let snapshot = editor.snapshot(cx);
+ let mut highlighted_ranges = editor.background_highlights_in_range(
+ anchor_range(Point::new(3, 4)..Point::new(7, 4)),
+ &snapshot,
+ theme::current(cx).as_ref(),
+ );
+ // Enforce a consistent ordering based on color without relying on the ordering of the
+ // highlight's `TypeId` which is non-deterministic.
+ highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
+ assert_eq!(
+ highlighted_ranges,
+ &[
+ (
+ DisplayPoint::new(3, 2)..DisplayPoint::new(3, 5),
+ Color::green(),
+ ),
+ (
+ DisplayPoint::new(5, 3)..DisplayPoint::new(5, 6),
+ Color::green(),
+ ),
+ (
+ DisplayPoint::new(4, 2)..DisplayPoint::new(4, 4),
+ Color::red(),
+ ),
+ (
+ DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+ Color::red(),
+ ),
+ ]
+ );
+ assert_eq!(
+ editor.background_highlights_in_range(
+ anchor_range(Point::new(5, 6)..Point::new(6, 4)),
+ &snapshot,
+ theme::current(cx).as_ref(),
+ ),
+ &[(
+ DisplayPoint::new(6, 3)..DisplayPoint::new(6, 5),
+ Color::red(),
+ )]
+ );
+ });
+}
+
+#[gpui::test]
+async fn test_following(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let fs = FakeFs::new(cx.background());
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+
+ let buffer = project.update(cx, |project, cx| {
+ let buffer = project
+ .create_buffer(&sample_text(16, 8, 'a'), None, cx)
+ .unwrap();
+ cx.add_model(|cx| MultiBuffer::singleton(buffer, cx))
+ });
+ let leader = cx
+ .add_window(|cx| build_editor(buffer.clone(), cx))
+ .root(cx);
+ let follower = cx
+ .update(|cx| {
+ cx.add_window(
+ WindowOptions {
+ bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))),
+ ..Default::default()
+ },
+ |cx| build_editor(buffer.clone(), cx),
+ )
+ })
+ .root(cx);
+
+ let is_still_following = Rc::new(RefCell::new(true));
+ let follower_edit_event_count = Rc::new(RefCell::new(0));
+ let pending_update = Rc::new(RefCell::new(None));
+ follower.update(cx, {
+ let update = pending_update.clone();
+ let is_still_following = is_still_following.clone();
+ let follower_edit_event_count = follower_edit_event_count.clone();
+ |_, cx| {
+ cx.subscribe(&leader, move |_, leader, event, cx| {
+ leader
+ .read(cx)
+ .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+ })
+ .detach();
+
+ cx.subscribe(&follower, move |_, _, event, cx| {
+ if Editor::should_unfollow_on_event(event, cx) {
+ *is_still_following.borrow_mut() = false;
+ }
+ if let Event::BufferEdited = event {
+ *follower_edit_event_count.borrow_mut() += 1;
+ }
+ })
+ .detach();
+ }
+ });
+
+ // Update the selections only
+ leader.update(cx, |leader, cx| {
+ leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
+ });
+ follower
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ follower.read_with(cx, |follower, cx| {
+ assert_eq!(follower.selections.ranges(cx), vec![1..1]);
+ });
+ assert_eq!(*is_still_following.borrow(), true);
+ assert_eq!(*follower_edit_event_count.borrow(), 0);
+
+ // Update the scroll position only
+ leader.update(cx, |leader, cx| {
+ leader.set_scroll_position(vec2f(1.5, 3.5), cx);
+ });
+ follower
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ follower.update(cx, |follower, cx| follower.scroll_position(cx)),
+ vec2f(1.5, 3.5)
+ );
+ assert_eq!(*is_still_following.borrow(), true);
+ assert_eq!(*follower_edit_event_count.borrow(), 0);
+
+ // Update the selections and scroll position. The follower's scroll position is updated
+ // via autoscroll, not via the leader's exact scroll position.
+ leader.update(cx, |leader, cx| {
+ leader.change_selections(None, cx, |s| s.select_ranges([0..0]));
+ leader.request_autoscroll(Autoscroll::newest(), cx);
+ leader.set_scroll_position(vec2f(1.5, 3.5), cx);
+ });
+ follower
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ follower.update(cx, |follower, cx| {
+ assert_eq!(follower.scroll_position(cx), vec2f(1.5, 0.0));
+ assert_eq!(follower.selections.ranges(cx), vec![0..0]);
+ });
+ assert_eq!(*is_still_following.borrow(), true);
+
+ // Creating a pending selection that precedes another selection
+ leader.update(cx, |leader, cx| {
+ leader.change_selections(None, cx, |s| s.select_ranges([1..1]));
+ leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx);
+ });
+ follower
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ follower.read_with(cx, |follower, cx| {
+ assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
+ });
+ assert_eq!(*is_still_following.borrow(), true);
+
+ // Extend the pending selection so that it surrounds another selection
+ leader.update(cx, |leader, cx| {
+ leader.extend_selection(DisplayPoint::new(0, 2), 1, cx);
+ });
+ follower
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, pending_update.borrow_mut().take().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ follower.read_with(cx, |follower, cx| {
+ assert_eq!(follower.selections.ranges(cx), vec![0..2]);
+ });
+
+ // Scrolling locally breaks the follow
+ follower.update(cx, |follower, cx| {
+ let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
+ follower.set_scroll_anchor(
+ ScrollAnchor {
+ anchor: top_anchor,
+ offset: vec2f(0.0, 0.5),
+ },
+ cx,
+ );
+ });
+ assert_eq!(*is_still_following.borrow(), false);
+}
+
+#[gpui::test]
+async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let fs = FakeFs::new(cx.background());
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
+
+ let leader = pane.update(cx, |_, cx| {
+ let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+ cx.add_view(|cx| build_editor(multibuffer.clone(), cx))
+ });
+
+ // Start following the editor when it has no excerpts.
+ let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+ let follower_1 = cx
+ .update(|cx| {
+ Editor::from_state_proto(
+ pane.clone(),
+ workspace.clone(),
+ ViewId {
+ creator: Default::default(),
+ id: 0,
+ },
+ &mut state_message,
+ cx,
+ )
+ })
+ .unwrap()
+ .await
+ .unwrap();
+
+ let update_message = Rc::new(RefCell::new(None));
+ follower_1.update(cx, {
+ let update = update_message.clone();
+ |_, cx| {
+ cx.subscribe(&leader, move |_, leader, event, cx| {
+ leader
+ .read(cx)
+ .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx);
+ })
+ .detach();
+ }
+ });
+
+ let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
+ (
+ project
+ .create_buffer("abc\ndef\nghi\njkl\n", None, cx)
+ .unwrap(),
+ project
+ .create_buffer("mno\npqr\nstu\nvwx\n", None, cx)
+ .unwrap(),
+ )
+ });
+
+ // Insert some excerpts.
+ leader.update(cx, |leader, cx| {
+ leader.buffer.update(cx, |multibuffer, cx| {
+ let excerpt_ids = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ ExcerptRange {
+ context: 1..6,
+ primary: None,
+ },
+ ExcerptRange {
+ context: 12..15,
+ primary: None,
+ },
+ ExcerptRange {
+ context: 0..3,
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer.insert_excerpts_after(
+ excerpt_ids[0],
+ buffer_2.clone(),
+ [
+ ExcerptRange {
+ context: 8..12,
+ primary: None,
+ },
+ ExcerptRange {
+ context: 0..6,
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ });
+ });
+
+ // Apply the update of adding the excerpts.
+ follower_1
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ assert_eq!(
+ follower_1.read_with(cx, |editor, cx| editor.text(cx)),
+ leader.read_with(cx, |editor, cx| editor.text(cx))
+ );
+ update_message.borrow_mut().take();
+
+ // Start following separately after it already has excerpts.
+ let mut state_message = leader.update(cx, |leader, cx| leader.to_state_proto(cx));
+ let follower_2 = cx
+ .update(|cx| {
+ Editor::from_state_proto(
+ pane.clone(),
+ workspace.clone(),
+ ViewId {
+ creator: Default::default(),
+ id: 0,
+ },
+ &mut state_message,
+ cx,
+ )
+ })
+ .unwrap()
+ .await
+ .unwrap();
+ assert_eq!(
+ follower_2.read_with(cx, |editor, cx| editor.text(cx)),
+ leader.read_with(cx, |editor, cx| editor.text(cx))
+ );
+
+ // Remove some excerpts.
+ leader.update(cx, |leader, cx| {
+ leader.buffer.update(cx, |multibuffer, cx| {
+ let excerpt_ids = multibuffer.excerpt_ids();
+ multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
+ multibuffer.remove_excerpts([excerpt_ids[0]], cx);
+ });
+ });
+
+ // Apply the update of removing the excerpts.
+ follower_1
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ follower_2
+ .update(cx, |follower, cx| {
+ follower.apply_update_proto(&project, update_message.borrow().clone().unwrap(), cx)
+ })
+ .await
+ .unwrap();
+ update_message.borrow_mut().take();
+ assert_eq!(
+ follower_1.read_with(cx, |editor, cx| editor.text(cx)),
+ leader.read_with(cx, |editor, cx| editor.text(cx))
+ );
+}
+
+#[test]
+fn test_combine_syntax_and_fuzzy_match_highlights() {
+ let string = "abcdefghijklmnop";
+ let syntax_ranges = [
+ (
+ 0..3,
+ HighlightStyle {
+ color: Some(Color::red()),
+ ..Default::default()
+ },
+ ),
+ (
+ 4..8,
+ HighlightStyle {
+ color: Some(Color::green()),
+ ..Default::default()
+ },
+ ),
+ ];
+ let match_indices = [4, 6, 7, 8];
+ assert_eq!(
+ combine_syntax_and_fuzzy_match_highlights(
+ string,
+ Default::default(),
+ syntax_ranges.into_iter(),
+ &match_indices,
+ ),
+ &[
+ (
+ 0..3,
+ HighlightStyle {
+ color: Some(Color::red()),
+ ..Default::default()
+ },
+ ),
+ (
+ 4..5,
+ HighlightStyle {
+ color: Some(Color::green()),
+ weight: Some(fonts::Weight::BOLD),
+ ..Default::default()
+ },
+ ),
+ (
+ 5..6,
+ HighlightStyle {
+ color: Some(Color::green()),
+ ..Default::default()
+ },
+ ),
+ (
+ 6..8,
+ HighlightStyle {
+ color: Some(Color::green()),
+ weight: Some(fonts::Weight::BOLD),
+ ..Default::default()
+ },
+ ),
+ (
+ 8..9,
+ HighlightStyle {
+ weight: Some(fonts::Weight::BOLD),
+ ..Default::default()
+ },
+ ),
+ ]
+ );
+}
+
+#[gpui::test]
+async fn go_to_prev_overlapping_diagnostic(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let project = cx.update_editor(|editor, _| editor.project.clone().unwrap());
+
+ cx.set_state(indoc! {"
+ หfn func(abc def: i32) -> u32 {
+ }
+ "});
+
+ cx.update(|cx| {
+ project.update(cx, |project, cx| {
+ project
+ .update_diagnostics(
+ LanguageServerId(0),
+ lsp::PublishDiagnosticsParams {
+ uri: lsp::Url::from_file_path("/root/file").unwrap(),
+ version: None,
+ diagnostics: vec![
+ lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 11),
+ lsp::Position::new(0, 12),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ ..Default::default()
+ },
+ lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 12),
+ lsp::Position::new(0, 15),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ ..Default::default()
+ },
+ lsp::Diagnostic {
+ range: lsp::Range::new(
+ lsp::Position::new(0, 25),
+ lsp::Position::new(0, 28),
+ ),
+ severity: Some(lsp::DiagnosticSeverity::ERROR),
+ ..Default::default()
+ },
+ ],
+ },
+ &[],
+ cx,
+ )
+ .unwrap()
+ });
+ });
+
+ deterministic.run_until_parked();
+
+ cx.update_editor(|editor, cx| {
+ editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+ });
+
+ cx.assert_editor_state(indoc! {"
+ fn func(abc def: i32) -> หu32 {
+ }
+ "});
+
+ cx.update_editor(|editor, cx| {
+ editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+ });
+
+ cx.assert_editor_state(indoc! {"
+ fn func(abc หdef: i32) -> u32 {
+ }
+ "});
+
+ cx.update_editor(|editor, cx| {
+ editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+ });
+
+ cx.assert_editor_state(indoc! {"
+ fn func(abcห def: i32) -> u32 {
+ }
+ "});
+
+ cx.update_editor(|editor, cx| {
+ editor.go_to_prev_diagnostic(&GoToPrevDiagnostic, cx);
+ });
+
+ cx.assert_editor_state(indoc! {"
+ fn func(abc def: i32) -> หu32 {
+ }
+ "});
+}
+
+#[gpui::test]
+async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorTestContext::new(cx).await;
+
+ let diff_base = r#"
+ use some::mod;
+
+ const A: u32 = 42;
+
+ fn main() {
+ println!("hello");
+
+ println!("world");
+ }
+ "#
+ .unindent();
+
+ // Edits are modified, removed, modified, added
+ cx.set_state(
+ &r#"
+ use some::modified;
+
+ ห
+ fn main() {
+ println!("hello there");
+
+ println!("around the");
+ println!("world");
+ }
+ "#
+ .unindent(),
+ );
+
+ cx.set_diff_base(Some(&diff_base));
+ deterministic.run_until_parked();
+
+ cx.update_editor(|editor, cx| {
+ //Wrap around the bottom of the buffer
+ for _ in 0..3 {
+ editor.go_to_hunk(&GoToHunk, cx);
+ }
+ });
+
+ cx.assert_editor_state(
+ &r#"
+ หuse some::modified;
+
+
+ fn main() {
+ println!("hello there");
+
+ println!("around the");
+ println!("world");
+ }
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| {
+ //Wrap around the top of the buffer
+ for _ in 0..2 {
+ editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+ }
+ });
+
+ cx.assert_editor_state(
+ &r#"
+ use some::modified;
+
+
+ fn main() {
+ ห println!("hello there");
+
+ println!("around the");
+ println!("world");
+ }
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| {
+ editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+ });
+
+ cx.assert_editor_state(
+ &r#"
+ use some::modified;
+
+ ห
+ fn main() {
+ println!("hello there");
+
+ println!("around the");
+ println!("world");
+ }
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| {
+ for _ in 0..3 {
+ editor.go_to_prev_hunk(&GoToPrevHunk, cx);
+ }
+ });
+
+ cx.assert_editor_state(
+ &r#"
+ use some::modified;
+
+
+ fn main() {
+ ห println!("hello there");
+
+ println!("around the");
+ println!("world");
+ }
+ "#
+ .unindent(),
+ );
+
+ cx.update_editor(|editor, cx| {
+ editor.fold(&Fold, cx);
+
+ //Make sure that the fold only gets one hunk
+ for _ in 0..4 {
+ editor.go_to_hunk(&GoToHunk, cx);
+ }
+ });
+
+ cx.assert_editor_state(
+ &r#"
+ หuse some::modified;
+
+
+ fn main() {
+ println!("hello there");
+
+ println!("around the");
+ println!("world");
+ }
+ "#
+ .unindent(),
+ );
+}
+
+#[test]
+fn test_split_words() {
+ fn split<'a>(text: &'a str) -> Vec<&'a str> {
+ split_words(text).collect()
+ }
+
+ assert_eq!(split("HelloWorld"), &["Hello", "World"]);
+ assert_eq!(split("hello_world"), &["hello_", "world"]);
+ assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
+ assert_eq!(split("Hello_World"), &["Hello_", "World"]);
+ assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
+ assert_eq!(split("helloworld"), &["helloworld"]);
+}
+
+#[gpui::test]
+async fn test_move_to_enclosing_bracket(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
+ let mut assert = |before, after| {
+ let _state_context = cx.set_state(before);
+ cx.update_editor(|editor, cx| {
+ editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, cx)
+ });
+ cx.assert_editor_state(after);
+ };
+
+ // Outside bracket jumps to outside of matching bracket
+ assert("console.logห(var);", "console.log(var)ห;");
+ assert("console.log(var)ห;", "console.logห(var);");
+
+ // Inside bracket jumps to inside of matching bracket
+ assert("console.log(หvar);", "console.log(varห);");
+ assert("console.log(varห);", "console.log(หvar);");
+
+ // When outside a bracket and inside, favor jumping to the inside bracket
+ assert(
+ "console.log('foo', [1, 2, 3]ห);",
+ "console.log(ห'foo', [1, 2, 3]);",
+ );
+ assert(
+ "console.log(ห'foo', [1, 2, 3]);",
+ "console.log('foo', [1, 2, 3]ห);",
+ );
+
+ // Bias forward if two options are equally likely
+ assert(
+ "let result = curried_fun()ห();",
+ "let result = curried_fun()()ห;",
+ );
+
+ // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
+ assert(
+ indoc! {"
+ function test() {
+ console.log('test')ห
+ }"},
+ indoc! {"
+ function test() {
+ console.logห('test')
+ }"},
+ );
+}
+
+#[gpui::test(iterations = 10)]
+async fn test_copilot(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ cx.update(|cx| cx.set_global(copilot));
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // When inserting, ensure autocompletion is favored over Copilot suggestions.
+ cx.set_state(indoc! {"
+ oneห
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec!["completion_a", "completion_b"],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot1".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.context_menu_visible());
+ assert!(!editor.has_active_copilot_suggestion(cx));
+
+ // Confirming a completion inserts it and hides the context menu, without showing
+ // the copilot suggestion afterwards.
+ editor
+ .confirm_completion(&Default::default(), cx)
+ .unwrap()
+ .detach();
+ assert!(!editor.context_menu_visible());
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.text(cx), "one.completion_a\ntwo\nthree\n");
+ assert_eq!(editor.display_text(cx), "one.completion_a\ntwo\nthree\n");
+ });
+
+ // Ensure Copilot suggestions are shown right away if no autocompletion is available.
+ cx.set_state(indoc! {"
+ oneห
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec![],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot1".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
+ });
+
+ // Reset editor, and ensure autocompletion is still favored over Copilot suggestions.
+ cx.set_state(indoc! {"
+ oneห
+ two
+ three
+ "});
+ cx.simulate_keystroke(".");
+ let _ = handle_completion_request(
+ &mut cx,
+ indoc! {"
+ one.|<>
+ two
+ three
+ "},
+ vec!["completion_a", "completion_b"],
+ );
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot1".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.context_menu_visible());
+ assert!(!editor.has_active_copilot_suggestion(cx));
+
+ // When hiding the context menu, the Copilot suggestion becomes visible.
+ editor.hide_context_menu(cx);
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.\ntwo\nthree\n");
+ });
+
+ // Ensure existing completion is interpolated when inserting again.
+ cx.simulate_keystroke("c");
+ deterministic.run_until_parked();
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot1\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+ });
+
+ // After debouncing, new Copilot completions should be requested.
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "one.copilot2".into(),
+ range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 5)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.context_menu_visible());
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+
+ // Canceling should remove the active Copilot suggestion.
+ editor.cancel(&Default::default(), cx);
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.c\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+
+ // After canceling, tabbing shouldn't insert the previously shown suggestion.
+ editor.tab(&Default::default(), cx);
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.c \ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c \ntwo\nthree\n");
+
+ // When undoing the previously active suggestion is shown again.
+ editor.undo(&Default::default(), cx);
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.c\ntwo\nthree\n");
+ });
+
+ // If an edit occurs outside of this editor, the suggestion is still correctly interpolated.
+ cx.update_buffer(|buffer, cx| buffer.edit([(5..5, "o")], None, cx));
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+
+ // Tabbing when there is an active suggestion inserts it.
+ editor.tab(&Default::default(), cx);
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.copilot2\ntwo\nthree\n");
+
+ // When undoing the previously active suggestion is shown again.
+ editor.undo(&Default::default(), cx);
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.copilot2\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+
+ // Hide suggestion.
+ editor.cancel(&Default::default(), cx);
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.co\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.co\ntwo\nthree\n");
+ });
+
+ // If an edit occurs outside of this editor but no suggestion is being shown,
+ // we won't make it visible.
+ cx.update_buffer(|buffer, cx| buffer.edit([(6..6, "p")], None, cx));
+ cx.update_editor(|editor, cx| {
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one.cop\ntwo\nthree\n");
+ assert_eq!(editor.text(cx), "one.cop\ntwo\nthree\n");
+ });
+
+ // Reset the editor to verify how suggestions behave when tabbing on leading indentation.
+ cx.update_editor(|editor, cx| {
+ editor.set_text("fn foo() {\n \n}", cx);
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
+ });
+ });
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: " let x = 4;".into(),
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+
+ cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
+ assert_eq!(editor.text(cx), "fn foo() {\n \n}");
+
+ // Tabbing inside of leading whitespace inserts indentation without accepting the suggestion.
+ editor.tab(&Default::default(), cx);
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.text(cx), "fn foo() {\n \n}");
+ assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
+
+ // Tabbing again accepts the suggestion.
+ editor.tab(&Default::default(), cx);
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.text(cx), "fn foo() {\n let x = 4;\n}");
+ assert_eq!(editor.display_text(cx), "fn foo() {\n let x = 4;\n}");
+ });
+}
+
+#[gpui::test]
+async fn test_copilot_completion_invalidation(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ cx.update(|cx| cx.set_global(copilot));
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ one
+ twห
+ three
+ "});
+
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "two.foo()".into(),
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 2)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ cx.update_editor(|editor, cx| editor.next_copilot_suggestion(&Default::default(), cx));
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ cx.update_editor(|editor, cx| {
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+ assert_eq!(editor.text(cx), "one\ntw\nthree\n");
+
+ editor.backspace(&Default::default(), cx);
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+ assert_eq!(editor.text(cx), "one\nt\nthree\n");
+
+ editor.backspace(&Default::default(), cx);
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+ assert_eq!(editor.text(cx), "one\n\nthree\n");
+
+ // Deleting across the original suggestion range invalidates it.
+ editor.backspace(&Default::default(), cx);
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one\nthree\n");
+ assert_eq!(editor.text(cx), "one\nthree\n");
+
+ // Undoing the deletion restores the suggestion.
+ editor.undo(&Default::default(), cx);
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(editor.display_text(cx), "one\ntwo.foo()\nthree\n");
+ assert_eq!(editor.text(cx), "one\n\nthree\n");
+ });
+}
+
+#[gpui::test]
+async fn test_copilot_multibuffer(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ cx.update(|cx| cx.set_global(copilot));
+
+ let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n"));
+ let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n"));
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer
+ });
+ let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "b = 2 + a".into(),
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 5)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ editor.update(cx, |editor, cx| {
+ // Ensure copilot suggestions are shown for the first excerpt.
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
+ });
+ editor.next_copilot_suggestion(&Default::default(), cx);
+ });
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ editor.update(cx, |editor, cx| {
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2 + a\n\n\n\nc = 3\nd = 4\n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+ });
+
+ handle_copilot_completion_request(
+ &copilot_lsp,
+ vec![copilot::request::Completion {
+ text: "d = 4 + c".into(),
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 6)),
+ ..Default::default()
+ }],
+ vec![],
+ );
+ editor.update(cx, |editor, cx| {
+ // Move to another excerpt, ensuring the suggestion gets cleared.
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
+ });
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4\n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4\n");
+
+ // Type a character, ensuring we don't even try to interpolate the previous suggestion.
+ editor.handle_input(" ", cx);
+ assert!(!editor.has_active_copilot_suggestion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 \n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+ });
+
+ // Ensure the new suggestion is displayed when the debounce timeout expires.
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ editor.update(cx, |editor, cx| {
+ assert!(editor.has_active_copilot_suggestion(cx));
+ assert_eq!(
+ editor.display_text(cx),
+ "\n\na = 1\nb = 2\n\n\n\nc = 3\nd = 4 + c\n"
+ );
+ assert_eq!(editor.text(cx), "a = 1\nb = 2\n\nc = 3\nd = 4 \n");
+ });
+}
+
+#[gpui::test]
+async fn test_copilot_disabled_globs(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |settings| {
+ settings
+ .copilot
+ .get_or_insert(Default::default())
+ .disabled_globs = Some(vec![".env*".to_string()]);
+ });
+
+ let (copilot, copilot_lsp) = Copilot::fake(cx);
+ cx.update(|cx| cx.set_global(copilot));
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/test",
+ json!({
+ ".env": "SECRET=something\n",
+ "README.md": "hello\n"
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/test".as_ref()], cx).await;
+
+ let private_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/test/.env", cx)
+ })
+ .await
+ .unwrap();
+ let public_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/test/README.md", cx)
+ })
+ .await
+ .unwrap();
+
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ private_buffer.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ public_buffer.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ multibuffer
+ });
+ let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx);
+
+ let mut copilot_requests = copilot_lsp
+ .handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| async move {
+ Ok(copilot::request::GetCompletionsResult {
+ completions: vec![copilot::request::Completion {
+ text: "next line".into(),
+ range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(1, 0)),
+ ..Default::default()
+ }],
+ })
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |selections| {
+ selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
+ });
+ editor.next_copilot_suggestion(&Default::default(), cx);
+ });
+
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ assert!(copilot_requests.try_next().is_err());
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
+ });
+ editor.next_copilot_suggestion(&Default::default(), cx);
+ });
+
+ deterministic.advance_clock(COPILOT_DEBOUNCE_TIMEOUT);
+ assert!(copilot_requests.try_next().is_ok());
+}
+
+#[gpui::test]
+async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ brackets: BracketPairConfig {
+ pairs: vec![BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ }],
+ disabled_scopes_by_bracket_ix: Vec::new(),
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
+ first_trigger_character: "{".to_string(),
+ more_trigger_character: None,
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { let a = 5; }",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor_handle = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ fake_server.handle_request::<lsp::request::OnTypeFormatting, _, _>(|params, _| async move {
+ assert_eq!(
+ params.text_document_position.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ assert_eq!(
+ params.text_document_position.position,
+ lsp::Position::new(0, 21),
+ );
+
+ Ok(Some(vec![lsp::TextEdit {
+ new_text: "]".to_string(),
+ range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
+ }]))
+ });
+
+ editor_handle.update(cx, |editor, cx| {
+ cx.focus(&editor_handle);
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
+ });
+ editor.handle_input("{", cx);
+ });
+
+ cx.foreground().run_until_parked();
+
+ buffer.read_with(cx, |buffer, _| {
+ assert_eq!(
+ buffer.text(),
+ "fn main() { let a = {5}; }",
+ "No extra braces from on type formatting should appear in the buffer"
+ )
+ });
+}
+
+#[gpui::test]
+async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let language_name: Arc<str> = "Rust".into();
+ let mut language = Language::new(
+ LanguageConfig {
+ name: Arc::clone(&language_name),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+
+ let server_restarts = Arc::new(AtomicUsize::new(0));
+ let closure_restarts = Arc::clone(&server_restarts);
+ let language_server_name = "test language server";
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ name: language_server_name,
+ initialization_options: Some(json!({
+ "testOptionValue": true
+ })),
+ initializer: Some(Box::new(move |fake_server| {
+ let task_restarts = Arc::clone(&closure_restarts);
+ fake_server.handle_request::<lsp::request::Shutdown, _, _>(move |_, _| {
+ task_restarts.fetch_add(1, atomic::Ordering::Release);
+ futures::future::ready(Ok(()))
+ });
+ })),
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { let a = 5; }",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ let _fake_server = fake_servers.next().await.unwrap();
+ update_test_language_settings(cx, |language_settings| {
+ language_settings.languages.insert(
+ Arc::clone(&language_name),
+ LanguageSettingsContent {
+ tab_size: NonZeroU32::new(8),
+ ..Default::default()
+ },
+ );
+ });
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ server_restarts.load(atomic::Ordering::Acquire),
+ 0,
+ "Should not restart LSP server on an unrelated change"
+ );
+
+ update_test_project_settings(cx, |project_settings| {
+ project_settings.lsp.insert(
+ "Some other server name".into(),
+ LspSettings {
+ initialization_options: Some(json!({
+ "some other init value": false
+ })),
+ },
+ );
+ });
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ server_restarts.load(atomic::Ordering::Acquire),
+ 0,
+ "Should not restart LSP server on an unrelated LSP settings change"
+ );
+
+ update_test_project_settings(cx, |project_settings| {
+ project_settings.lsp.insert(
+ language_server_name.into(),
+ LspSettings {
+ initialization_options: Some(json!({
+ "anotherInitValue": false
+ })),
+ },
+ );
+ });
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ server_restarts.load(atomic::Ordering::Acquire),
+ 1,
+ "Should restart LSP server on a related LSP settings change"
+ );
+
+ update_test_project_settings(cx, |project_settings| {
+ project_settings.lsp.insert(
+ language_server_name.into(),
+ LspSettings {
+ initialization_options: Some(json!({
+ "anotherInitValue": false
+ })),
+ },
+ );
+ });
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ server_restarts.load(atomic::Ordering::Acquire),
+ 1,
+ "Should not restart LSP server on a related LSP settings change that is the same"
+ );
+
+ update_test_project_settings(cx, |project_settings| {
+ project_settings.lsp.insert(
+ language_server_name.into(),
+ LspSettings {
+ initialization_options: None,
+ },
+ );
+ });
+ cx.foreground().run_until_parked();
+ assert_eq!(
+ server_restarts.load(atomic::Ordering::Acquire),
+ 2,
+ "Should restart LSP server on another related LSP settings change"
+ );
+}
+
+#[gpui::test]
+async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![".".to_string()]),
+ resolve_provider: Some(true),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"fn main() { let a = 2ห; }"});
+ cx.simulate_keystroke(".");
+ let completion_item = lsp::CompletionItem {
+ label: "some".into(),
+ kind: Some(lsp::CompletionItemKind::SNIPPET),
+ detail: Some("Wrap the expression in an `Option::Some`".to_string()),
+ documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: "```rust\nSome(2)\n```".to_string(),
+ })),
+ deprecated: Some(false),
+ sort_text: Some("fffffff2".to_string()),
+ filter_text: Some("some".to_string()),
+ insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ range: lsp::Range {
+ start: lsp::Position {
+ line: 0,
+ character: 22,
+ },
+ end: lsp::Position {
+ line: 0,
+ character: 22,
+ },
+ },
+ new_text: "Some(2)".to_string(),
+ })),
+ additional_text_edits: Some(vec![lsp::TextEdit {
+ range: lsp::Range {
+ start: lsp::Position {
+ line: 0,
+ character: 20,
+ },
+ end: lsp::Position {
+ line: 0,
+ character: 22,
+ },
+ },
+ new_text: "".to_string(),
+ }]),
+ ..Default::default()
+ };
+
+ let closure_completion_item = completion_item.clone();
+ let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |_, _, _| {
+ let task_completion_item = closure_completion_item.clone();
+ async move {
+ Ok(Some(lsp::CompletionResponse::Array(vec![
+ task_completion_item,
+ ])))
+ }
+ });
+
+ request.next().await;
+
+ cx.condition(|editor, _| editor.context_menu_visible())
+ .await;
+ let apply_additional_edits = cx.update_editor(|editor, cx| {
+ editor
+ .confirm_completion(&ConfirmCompletion::default(), cx)
+ .unwrap()
+ });
+ cx.assert_editor_state(indoc! {"fn main() { let a = 2.Some(2)ห; }"});
+
+ cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
+ let task_completion_item = completion_item.clone();
+ async move { Ok(task_completion_item) }
+ })
+ .next()
+ .await
+ .unwrap();
+ apply_additional_edits.await.unwrap();
+ cx.assert_editor_state(indoc! {"fn main() { let a = Some(2)ห; }"});
+}
+
+#[gpui::test]
+async fn test_completions_in_languages_with_extra_word_characters(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new(
+ Language::new(
+ LanguageConfig {
+ path_suffixes: vec!["jsx".into()],
+ overrides: [(
+ "element".into(),
+ LanguageConfigOverride {
+ word_characters: Override::Set(['-'].into_iter().collect()),
+ ..Default::default()
+ },
+ )]
+ .into_iter()
+ .collect(),
+ ..Default::default()
+ },
+ Some(tree_sitter_typescript::language_tsx()),
+ )
+ .with_override_query("(jsx_self_closing_element) @element")
+ .unwrap(),
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions {
+ trigger_characters: Some(vec![":".to_string()]),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.lsp
+ .handle_request::<lsp::request::Completion, _, _>(move |_, _| async move {
+ Ok(Some(lsp::CompletionResponse::Array(vec![
+ lsp::CompletionItem {
+ label: "bg-blue".into(),
+ ..Default::default()
+ },
+ lsp::CompletionItem {
+ label: "bg-red".into(),
+ ..Default::default()
+ },
+ lsp::CompletionItem {
+ label: "bg-yellow".into(),
+ ..Default::default()
+ },
+ ])))
+ });
+
+ cx.set_state(r#"<p class="bgห" />"#);
+
+ // Trigger completion when typing a dash, because the dash is an extra
+ // word character in the 'element' scope, which contains the cursor.
+ cx.simulate_keystroke("-");
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, _| {
+ if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+ assert_eq!(
+ menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+ &["bg-red", "bg-blue", "bg-yellow"]
+ );
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ });
+
+ cx.simulate_keystroke("l");
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, _| {
+ if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+ assert_eq!(
+ menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+ &["bg-blue", "bg-yellow"]
+ );
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ });
+
+ // When filtering completions, consider the character after the '-' to
+ // be the start of a subword.
+ cx.set_state(r#"<p class="yelห" />"#);
+ cx.simulate_keystroke("l");
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, _| {
+ if let Some(ContextMenu::Completions(menu)) = editor.context_menu.read().as_ref() {
+ assert_eq!(
+ menu.matches.iter().map(|m| &m.string).collect::<Vec<_>>(),
+ &["bg-yellow"]
+ );
+ } else {
+ panic!("expected completion menu to be open");
+ }
+ });
+}
+
+#[gpui::test]
+async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.formatter = Some(language_settings::Formatter::Prettier)
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ prettier_parser_name: Some("test_parser".to_string()),
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+
+ let test_plugin = "test_plugin";
+ let _ = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ prettier_plugins: vec![test_plugin],
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_file("/file.rs", Default::default()).await;
+
+ let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
+ let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::new(language));
+ });
+ let buffer = project
+ .update(cx, |project, cx| project.open_local_buffer("/file.rs", cx))
+ .await
+ .unwrap();
+
+ let buffer_text = "one\ntwo\nthree\n";
+ let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
+ let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx);
+ editor.update(cx, |editor, cx| editor.set_text(buffer_text, cx));
+
+ let format = editor.update(cx, |editor, cx| {
+ editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+ });
+ format.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ buffer_text.to_string() + prettier_format_suffix,
+ "Test prettier formatting was not applied to the original buffer text",
+ );
+
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.formatter = Some(language_settings::Formatter::Auto)
+ });
+ let format = editor.update(cx, |editor, cx| {
+ editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+ });
+ format.await.unwrap();
+ assert_eq!(
+ editor.read_with(cx, |editor, cx| editor.text(cx)),
+ buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
+ "Autoformatting (via test prettier) was not applied to the original buffer text",
+ );
+}
+
+fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
+ let point = DisplayPoint::new(row as u32, column as u32);
+ point..point
+}
+
+fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewContext<Editor>) {
+ let (text, ranges) = marked_text_ranges(marked_text, true);
+ assert_eq!(view.text(cx), text);
+ assert_eq!(
+ view.selections.ranges(cx),
+ ranges,
+ "Assert selections are {}",
+ marked_text
+ );
+}
+
+/// Handle completion request passing a marked string specifying where the completion
+/// should be triggered from using '|' character, what range should be replaced, and what completions
+/// should be returned using '<' and '>' to delimit the range
+pub fn handle_completion_request<'a>(
+ cx: &mut EditorLspTestContext<'a>,
+ marked_string: &str,
+ completions: Vec<&'static str>,
+) -> impl Future<Output = ()> {
+ let complete_from_marker: TextRangeMarker = '|'.into();
+ let replace_range_marker: TextRangeMarker = ('<', '>').into();
+ let (_, mut marked_ranges) = marked_text_ranges_by(
+ marked_string,
+ vec![complete_from_marker.clone(), replace_range_marker.clone()],
+ );
+
+ let complete_from_position =
+ cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
+ let replace_range =
+ cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
+
+ let mut request = cx.handle_request::<lsp::request::Completion, _, _>(move |url, params, _| {
+ let completions = completions.clone();
+ async move {
+ assert_eq!(params.text_document_position.text_document.uri, url.clone());
+ assert_eq!(
+ params.text_document_position.position,
+ complete_from_position
+ );
+ Ok(Some(lsp::CompletionResponse::Array(
+ completions
+ .iter()
+ .map(|completion_text| lsp::CompletionItem {
+ label: completion_text.to_string(),
+ text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
+ range: replace_range,
+ new_text: completion_text.to_string(),
+ })),
+ ..Default::default()
+ })
+ .collect(),
+ )))
+ }
+ });
+
+ async move {
+ request.next().await;
+ }
+}
+
+fn handle_resolve_completion_request<'a>(
+ cx: &mut EditorLspTestContext<'a>,
+ edits: Option<Vec<(&'static str, &'static str)>>,
+) -> impl Future<Output = ()> {
+ let edits = edits.map(|edits| {
+ edits
+ .iter()
+ .map(|(marked_string, new_text)| {
+ let (_, marked_ranges) = marked_text_ranges(marked_string, false);
+ let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
+ lsp::TextEdit::new(replace_range, new_text.to_string())
+ })
+ .collect::<Vec<_>>()
+ });
+
+ let mut request =
+ cx.handle_request::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
+ let edits = edits.clone();
+ async move {
+ Ok(lsp::CompletionItem {
+ additional_text_edits: edits,
+ ..Default::default()
+ })
+ }
+ });
+
+ async move {
+ request.next().await;
+ }
+}
+
+fn handle_copilot_completion_request(
+ lsp: &lsp::FakeLanguageServer,
+ completions: Vec<copilot::request::Completion>,
+ completions_cycling: Vec<copilot::request::Completion>,
+) {
+ lsp.handle_request::<copilot::request::GetCompletions, _, _>(move |_params, _cx| {
+ let completions = completions.clone();
+ async move {
+ Ok(copilot::request::GetCompletionsResult {
+ completions: completions.clone(),
+ })
+ }
+ });
+ lsp.handle_request::<copilot::request::GetCompletionsCycling, _, _>(move |_params, _cx| {
+ let completions_cycling = completions_cycling.clone();
+ async move {
+ Ok(copilot::request::GetCompletionsResult {
+ completions: completions_cycling.clone(),
+ })
+ }
+ });
+}
+
+pub(crate) fn update_test_language_settings(
+ cx: &mut TestAppContext,
+ f: impl Fn(&mut AllLanguageSettingsContent),
+) {
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<AllLanguageSettings>(cx, f);
+ });
+ });
+}
+
+pub(crate) fn update_test_project_settings(
+ cx: &mut TestAppContext,
+ f: impl Fn(&mut ProjectSettings),
+) {
+ cx.update(|cx| {
+ cx.update_global::<SettingsStore, _, _>(|store, cx| {
+ store.update_user_settings::<ProjectSettings>(cx, f);
+ });
+ });
+}
+
+pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
+ cx.foreground().forbid_parking();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ client::init_settings(cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ crate::init(cx);
+ });
+
+ update_test_language_settings(cx, f);
+}
@@ -0,0 +1,3478 @@
+use super::{
+ display_map::{BlockContext, ToDisplayPoint},
+ Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, SelectPhase, SoftWrap, ToPoint,
+ MAX_LINE_LEN,
+};
+use crate::{
+ display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, TransformBlock},
+ editor_settings::ShowScrollbar,
+ git::{diff_hunk_to_display, DisplayDiffHunk},
+ hover_popover::{
+ hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH,
+ MIN_POPOVER_LINE_HEIGHT,
+ },
+ link_go_to_definition::{
+ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link,
+ update_inlay_link_and_hover_points, GoToDefinitionTrigger,
+ },
+ mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt,
+};
+use collections::{BTreeMap, HashMap};
+use git::diff::DiffHunkStatus;
+use gpui::{
+ color::Color,
+ elements::*,
+ fonts::TextStyle,
+ geometry::{
+ rect::RectF,
+ vector::{vec2f, Vector2F},
+ PathBuilder,
+ },
+ json::{self, ToJson},
+ platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent},
+ text_layout::{self, Line, RunStyle, TextLayoutCache},
+ AnyElement, Axis, CursorRegion, Element, EventContext, FontCache, MouseRegion, Quad,
+ SizeConstraint, ViewContext, WindowContext,
+};
+use itertools::Itertools;
+use json::json;
+use language::{
+ language_settings::ShowWhitespaceSetting, Bias, CursorShape, OffsetUtf16, Selection,
+};
+use project::{
+ project_settings::{GitGutterSetting, ProjectSettings},
+ ProjectPath,
+};
+use smallvec::SmallVec;
+use std::{
+ borrow::Cow,
+ cmp::{self, Ordering},
+ fmt::Write,
+ iter,
+ ops::Range,
+ sync::Arc,
+};
+use text::Point;
+use theme::SelectionStyle;
+use workspace::item::Item;
+
+enum FoldMarkers {}
+
+struct SelectionLayout {
+ head: DisplayPoint,
+ cursor_shape: CursorShape,
+ is_newest: bool,
+ is_local: bool,
+ range: Range<DisplayPoint>,
+ active_rows: Range<u32>,
+}
+
+impl SelectionLayout {
+ fn new<T: ToPoint + ToDisplayPoint + Clone>(
+ selection: Selection<T>,
+ line_mode: bool,
+ cursor_shape: CursorShape,
+ map: &DisplaySnapshot,
+ is_newest: bool,
+ is_local: bool,
+ ) -> Self {
+ let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot));
+ let display_selection = point_selection.map(|p| p.to_display_point(map));
+ let mut range = display_selection.range();
+ let mut head = display_selection.head();
+ let mut active_rows = map.prev_line_boundary(point_selection.start).1.row()
+ ..map.next_line_boundary(point_selection.end).1.row();
+
+ // vim visual line mode
+ if line_mode {
+ let point_range = map.expand_to_line(point_selection.range());
+ range = point_range.start.to_display_point(map)..point_range.end.to_display_point(map);
+ }
+
+ // any vim visual mode (including line mode)
+ if cursor_shape == CursorShape::Block && !range.is_empty() && !selection.reversed {
+ if head.column() > 0 {
+ head = map.clip_point(DisplayPoint::new(head.row(), head.column() - 1), Bias::Left)
+ } else if head.row() > 0 && head != map.max_point() {
+ head = map.clip_point(
+ DisplayPoint::new(head.row() - 1, map.line_len(head.row() - 1)),
+ Bias::Left,
+ );
+ // updating range.end is a no-op unless you're cursor is
+ // on the newline containing a multi-buffer divider
+ // in which case the clip_point may have moved the head up
+ // an additional row.
+ range.end = DisplayPoint::new(head.row() + 1, 0);
+ active_rows.end = head.row();
+ }
+ }
+
+ Self {
+ head,
+ cursor_shape,
+ is_newest,
+ is_local,
+ range,
+ active_rows,
+ }
+ }
+}
+
+pub struct EditorElement {
+ style: Arc<EditorStyle>,
+}
+
+impl EditorElement {
+ pub fn new(style: EditorStyle) -> Self {
+ Self {
+ style: Arc::new(style),
+ }
+ }
+
+ fn attach_mouse_handlers(
+ position_map: &Arc<PositionMap>,
+ has_popovers: bool,
+ visible_bounds: RectF,
+ text_bounds: RectF,
+ gutter_bounds: RectF,
+ bounds: RectF,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ enum EditorElementMouseHandlers {}
+ let view_id = cx.view_id();
+ cx.scene().push_mouse_region(
+ MouseRegion::new::<EditorElementMouseHandlers>(view_id, view_id, visible_bounds)
+ .on_down(MouseButton::Left, {
+ let position_map = position_map.clone();
+ move |event, editor, cx| {
+ if !Self::mouse_down(
+ editor,
+ event.platform_event,
+ position_map.as_ref(),
+ text_bounds,
+ gutter_bounds,
+ cx,
+ ) {
+ cx.propagate_event();
+ }
+ }
+ })
+ .on_down(MouseButton::Right, {
+ let position_map = position_map.clone();
+ move |event, editor, cx| {
+ if !Self::mouse_right_down(
+ editor,
+ event.position,
+ position_map.as_ref(),
+ text_bounds,
+ cx,
+ ) {
+ cx.propagate_event();
+ }
+ }
+ })
+ .on_up(MouseButton::Left, {
+ let position_map = position_map.clone();
+ move |event, editor, cx| {
+ if !Self::mouse_up(
+ editor,
+ event.position,
+ event.cmd,
+ event.shift,
+ event.alt,
+ position_map.as_ref(),
+ text_bounds,
+ cx,
+ ) {
+ cx.propagate_event()
+ }
+ }
+ })
+ .on_drag(MouseButton::Left, {
+ let position_map = position_map.clone();
+ move |event, editor, cx| {
+ if event.end {
+ return;
+ }
+
+ if !Self::mouse_dragged(
+ editor,
+ event.platform_event,
+ position_map.as_ref(),
+ text_bounds,
+ cx,
+ ) {
+ cx.propagate_event()
+ }
+ }
+ })
+ .on_move({
+ let position_map = position_map.clone();
+ move |event, editor, cx| {
+ if !Self::mouse_moved(
+ editor,
+ event.platform_event,
+ &position_map,
+ text_bounds,
+ cx,
+ ) {
+ cx.propagate_event()
+ }
+ }
+ })
+ .on_move_out(move |_, editor: &mut Editor, cx| {
+ if has_popovers {
+ hide_hover(editor, cx);
+ }
+ })
+ .on_scroll({
+ let position_map = position_map.clone();
+ move |event, editor, cx| {
+ if !Self::scroll(
+ editor,
+ event.position,
+ *event.delta.raw(),
+ event.delta.precise(),
+ &position_map,
+ bounds,
+ cx,
+ ) {
+ cx.propagate_event()
+ }
+ }
+ }),
+ );
+
+ enum GutterHandlers {}
+ let view_id = cx.view_id();
+ let region_id = cx.view_id() + 1;
+ cx.scene().push_mouse_region(
+ MouseRegion::new::<GutterHandlers>(view_id, region_id, gutter_bounds).on_hover(
+ |hover, editor: &mut Editor, cx| {
+ editor.gutter_hover(
+ &GutterHover {
+ hovered: hover.started,
+ },
+ cx,
+ );
+ },
+ ),
+ )
+ }
+
+ fn mouse_down(
+ editor: &mut Editor,
+ MouseButtonEvent {
+ position,
+ modifiers:
+ Modifiers {
+ shift,
+ ctrl,
+ alt,
+ cmd,
+ ..
+ },
+ mut click_count,
+ ..
+ }: MouseButtonEvent,
+ position_map: &PositionMap,
+ text_bounds: RectF,
+ gutter_bounds: RectF,
+ cx: &mut EventContext<Editor>,
+ ) -> bool {
+ if gutter_bounds.contains_point(position) {
+ click_count = 3; // Simulate triple-click when clicking the gutter to select lines
+ } else if !text_bounds.contains_point(position) {
+ return false;
+ }
+
+ let point_for_position = position_map.point_for_position(text_bounds, position);
+ let position = point_for_position.previous_valid;
+ if shift && alt {
+ editor.select(
+ SelectPhase::BeginColumnar {
+ position,
+ goal_column: point_for_position.exact_unclipped.column(),
+ },
+ cx,
+ );
+ } else if shift && !ctrl && !alt && !cmd {
+ editor.select(
+ SelectPhase::Extend {
+ position,
+ click_count,
+ },
+ cx,
+ );
+ } else {
+ editor.select(
+ SelectPhase::Begin {
+ position,
+ add: alt,
+ click_count,
+ },
+ cx,
+ );
+ }
+
+ true
+ }
+
+ fn mouse_right_down(
+ editor: &mut Editor,
+ position: Vector2F,
+ position_map: &PositionMap,
+ text_bounds: RectF,
+ cx: &mut EventContext<Editor>,
+ ) -> bool {
+ if !text_bounds.contains_point(position) {
+ return false;
+ }
+ let point_for_position = position_map.point_for_position(text_bounds, position);
+ mouse_context_menu::deploy_context_menu(
+ editor,
+ position,
+ point_for_position.previous_valid,
+ cx,
+ );
+ true
+ }
+
+ fn mouse_up(
+ editor: &mut Editor,
+ position: Vector2F,
+ cmd: bool,
+ shift: bool,
+ alt: bool,
+ position_map: &PositionMap,
+ text_bounds: RectF,
+ cx: &mut EventContext<Editor>,
+ ) -> bool {
+ let end_selection = editor.has_pending_selection();
+ let pending_nonempty_selections = editor.has_pending_nonempty_selection();
+
+ if end_selection {
+ editor.select(SelectPhase::End, cx);
+ }
+
+ if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
+ let point = position_map.point_for_position(text_bounds, position);
+ let could_be_inlay = point.as_valid().is_none();
+ if shift || could_be_inlay {
+ go_to_fetched_type_definition(editor, point, alt, cx);
+ } else {
+ go_to_fetched_definition(editor, point, alt, cx);
+ }
+
+ return true;
+ }
+
+ end_selection
+ }
+
+ fn mouse_dragged(
+ editor: &mut Editor,
+ MouseMovedEvent {
+ modifiers: Modifiers { cmd, shift, .. },
+ position,
+ ..
+ }: MouseMovedEvent,
+ position_map: &PositionMap,
+ text_bounds: RectF,
+ cx: &mut EventContext<Editor>,
+ ) -> bool {
+ // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
+ // Don't trigger hover popover if mouse is hovering over context menu
+ let point = if text_bounds.contains_point(position) {
+ position_map
+ .point_for_position(text_bounds, position)
+ .as_valid()
+ } else {
+ None
+ };
+
+ update_go_to_definition_link(
+ editor,
+ point.map(GoToDefinitionTrigger::Text),
+ cmd,
+ shift,
+ cx,
+ );
+
+ if editor.has_pending_selection() {
+ let mut scroll_delta = Vector2F::zero();
+
+ let vertical_margin = position_map.line_height.min(text_bounds.height() / 3.0);
+ let top = text_bounds.origin_y() + vertical_margin;
+ let bottom = text_bounds.lower_left().y() - vertical_margin;
+ if position.y() < top {
+ scroll_delta.set_y(-scale_vertical_mouse_autoscroll_delta(top - position.y()))
+ }
+ if position.y() > bottom {
+ scroll_delta.set_y(scale_vertical_mouse_autoscroll_delta(position.y() - bottom))
+ }
+
+ let horizontal_margin = position_map.line_height.min(text_bounds.width() / 3.0);
+ let left = text_bounds.origin_x() + horizontal_margin;
+ let right = text_bounds.upper_right().x() - horizontal_margin;
+ if position.x() < left {
+ scroll_delta.set_x(-scale_horizontal_mouse_autoscroll_delta(
+ left - position.x(),
+ ))
+ }
+ if position.x() > right {
+ scroll_delta.set_x(scale_horizontal_mouse_autoscroll_delta(
+ position.x() - right,
+ ))
+ }
+
+ let point_for_position = position_map.point_for_position(text_bounds, position);
+
+ editor.select(
+ SelectPhase::Update {
+ position: point_for_position.previous_valid,
+ goal_column: point_for_position.exact_unclipped.column(),
+ scroll_position: (position_map.snapshot.scroll_position() + scroll_delta)
+ .clamp(Vector2F::zero(), position_map.scroll_max),
+ },
+ cx,
+ );
+ hover_at(editor, point, cx);
+ true
+ } else {
+ hover_at(editor, point, cx);
+ false
+ }
+ }
+
+ fn mouse_moved(
+ editor: &mut Editor,
+ MouseMovedEvent {
+ modifiers: Modifiers { shift, cmd, .. },
+ position,
+ ..
+ }: MouseMovedEvent,
+ position_map: &PositionMap,
+ text_bounds: RectF,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed
+ // Don't trigger hover popover if mouse is hovering over context menu
+ if text_bounds.contains_point(position) {
+ let point_for_position = position_map.point_for_position(text_bounds, position);
+ match point_for_position.as_valid() {
+ Some(point) => {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(point)),
+ cmd,
+ shift,
+ cx,
+ );
+ hover_at(editor, Some(point), cx);
+ }
+ None => {
+ update_inlay_link_and_hover_points(
+ &position_map.snapshot,
+ point_for_position,
+ editor,
+ cmd,
+ shift,
+ cx,
+ );
+ }
+ }
+ } else {
+ update_go_to_definition_link(editor, None, cmd, shift, cx);
+ hover_at(editor, None, cx);
+ }
+
+ true
+ }
+
+ fn scroll(
+ editor: &mut Editor,
+ position: Vector2F,
+ mut delta: Vector2F,
+ precise: bool,
+ position_map: &PositionMap,
+ bounds: RectF,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ if !bounds.contains_point(position) {
+ return false;
+ }
+
+ let line_height = position_map.line_height;
+ let max_glyph_width = position_map.em_width;
+
+ let axis = if precise {
+ //Trackpad
+ position_map.snapshot.ongoing_scroll.filter(&mut delta)
+ } else {
+ //Not trackpad
+ delta *= vec2f(max_glyph_width, line_height);
+ None //Resets ongoing scroll
+ };
+
+ let scroll_position = position_map.snapshot.scroll_position();
+ let x = (scroll_position.x() * max_glyph_width - delta.x()) / max_glyph_width;
+ let y = (scroll_position.y() * line_height - delta.y()) / line_height;
+ let scroll_position = vec2f(x, y).clamp(Vector2F::zero(), position_map.scroll_max);
+ editor.scroll(scroll_position, axis, cx);
+
+ true
+ }
+
+ fn paint_background(
+ &self,
+ gutter_bounds: RectF,
+ text_bounds: RectF,
+ layout: &LayoutState,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let bounds = gutter_bounds.union_rect(text_bounds);
+ let scroll_top =
+ layout.position_map.snapshot.scroll_position().y() * layout.position_map.line_height;
+ cx.scene().push_quad(Quad {
+ bounds: gutter_bounds,
+ background: Some(self.style.gutter_background),
+ border: Border::new(0., Color::transparent_black()).into(),
+ corner_radii: Default::default(),
+ });
+ cx.scene().push_quad(Quad {
+ bounds: text_bounds,
+ background: Some(self.style.background),
+ border: Border::new(0., Color::transparent_black()).into(),
+ corner_radii: Default::default(),
+ });
+
+ if let EditorMode::Full = layout.mode {
+ let mut active_rows = layout.active_rows.iter().peekable();
+ while let Some((start_row, contains_non_empty_selection)) = active_rows.next() {
+ let mut end_row = *start_row;
+ while active_rows.peek().map_or(false, |r| {
+ *r.0 == end_row + 1 && r.1 == contains_non_empty_selection
+ }) {
+ active_rows.next().unwrap();
+ end_row += 1;
+ }
+
+ if !contains_non_empty_selection {
+ let origin = vec2f(
+ bounds.origin_x(),
+ bounds.origin_y() + (layout.position_map.line_height * *start_row as f32)
+ - scroll_top,
+ );
+ let size = vec2f(
+ bounds.width(),
+ layout.position_map.line_height * (end_row - start_row + 1) as f32,
+ );
+ cx.scene().push_quad(Quad {
+ bounds: RectF::new(origin, size),
+ background: Some(self.style.active_line_background),
+ border: Border::default().into(),
+ corner_radii: Default::default(),
+ });
+ }
+ }
+
+ if let Some(highlighted_rows) = &layout.highlighted_rows {
+ let origin = vec2f(
+ bounds.origin_x(),
+ bounds.origin_y()
+ + (layout.position_map.line_height * highlighted_rows.start as f32)
+ - scroll_top,
+ );
+ let size = vec2f(
+ bounds.width(),
+ layout.position_map.line_height * highlighted_rows.len() as f32,
+ );
+ cx.scene().push_quad(Quad {
+ bounds: RectF::new(origin, size),
+ background: Some(self.style.highlighted_line_background),
+ border: Border::default().into(),
+ corner_radii: Default::default(),
+ });
+ }
+
+ let scroll_left =
+ layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width;
+
+ for (wrap_position, active) in layout.wrap_guides.iter() {
+ let x =
+ (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.)
+ - scroll_left;
+
+ if x < text_bounds.origin_x()
+ || (layout.show_scrollbars && x > self.scrollbar_left(&bounds))
+ {
+ continue;
+ }
+
+ let color = if *active {
+ self.style.active_wrap_guide
+ } else {
+ self.style.wrap_guide
+ };
+ cx.scene().push_quad(Quad {
+ bounds: RectF::new(
+ vec2f(x, text_bounds.origin_y()),
+ vec2f(1., text_bounds.height()),
+ ),
+ background: Some(color),
+ border: Border::new(0., Color::transparent_black()).into(),
+ corner_radii: Default::default(),
+ });
+ }
+ }
+ }
+
+ fn paint_gutter(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &mut LayoutState,
+ editor: &mut Editor,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let line_height = layout.position_map.line_height;
+
+ let scroll_position = layout.position_map.snapshot.scroll_position();
+ let scroll_top = scroll_position.y() * line_height;
+
+ let show_gutter = matches!(
+ settings::get::<ProjectSettings>(cx).git.git_gutter,
+ Some(GitGutterSetting::TrackedFiles)
+ );
+
+ if show_gutter {
+ Self::paint_diff_hunks(bounds, layout, cx);
+ }
+
+ for (ix, line) in layout.line_number_layouts.iter().enumerate() {
+ if let Some(line) = line {
+ let line_origin = bounds.origin()
+ + vec2f(
+ bounds.width() - line.width() - layout.gutter_padding,
+ ix as f32 * line_height - (scroll_top % line_height),
+ );
+
+ line.paint(line_origin, visible_bounds, line_height, cx);
+ }
+ }
+
+ for (ix, fold_indicator) in layout.fold_indicators.iter_mut().enumerate() {
+ if let Some(indicator) = fold_indicator.as_mut() {
+ let position = vec2f(
+ bounds.width() - layout.gutter_padding,
+ ix as f32 * line_height - (scroll_top % line_height),
+ );
+ let centering_offset = vec2f(
+ (layout.gutter_padding + layout.gutter_margin - indicator.size().x()) / 2.,
+ (line_height - indicator.size().y()) / 2.,
+ );
+
+ let indicator_origin = bounds.origin() + position + centering_offset;
+
+ indicator.paint(indicator_origin, visible_bounds, editor, cx);
+ }
+ }
+
+ if let Some((row, indicator)) = layout.code_actions_indicator.as_mut() {
+ let mut x = 0.;
+ let mut y = *row as f32 * line_height - scroll_top;
+ x += ((layout.gutter_padding + layout.gutter_margin) - indicator.size().x()) / 2.;
+ y += (line_height - indicator.size().y()) / 2.;
+ indicator.paint(bounds.origin() + vec2f(x, y), visible_bounds, editor, cx);
+ }
+ }
+
+ fn paint_diff_hunks(bounds: RectF, layout: &mut LayoutState, cx: &mut ViewContext<Editor>) {
+ let diff_style = &theme::current(cx).editor.diff.clone();
+ let line_height = layout.position_map.line_height;
+
+ let scroll_position = layout.position_map.snapshot.scroll_position();
+ let scroll_top = scroll_position.y() * line_height;
+
+ for hunk in &layout.display_hunks {
+ let (display_row_range, status) = match hunk {
+ //TODO: This rendering is entirely a horrible hack
+ &DisplayDiffHunk::Folded { display_row: row } => {
+ let start_y = row as f32 * line_height - scroll_top;
+ let end_y = start_y + line_height;
+
+ let width = diff_style.removed_width_em * line_height;
+ let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+ let highlight_size = vec2f(width * 2., end_y - start_y);
+ let highlight_bounds = RectF::new(highlight_origin, highlight_size);
+
+ cx.scene().push_quad(Quad {
+ bounds: highlight_bounds,
+ background: Some(diff_style.modified),
+ border: Border::new(0., Color::transparent_black()).into(),
+ corner_radii: (1. * line_height).into(),
+ });
+
+ continue;
+ }
+
+ DisplayDiffHunk::Unfolded {
+ display_row_range,
+ status,
+ } => (display_row_range, status),
+ };
+
+ let color = match status {
+ DiffHunkStatus::Added => diff_style.inserted,
+ DiffHunkStatus::Modified => diff_style.modified,
+
+ //TODO: This rendering is entirely a horrible hack
+ DiffHunkStatus::Removed => {
+ let row = display_row_range.start;
+
+ let offset = line_height / 2.;
+ let start_y = row as f32 * line_height - offset - scroll_top;
+ let end_y = start_y + line_height;
+
+ let width = diff_style.removed_width_em * line_height;
+ let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+ let highlight_size = vec2f(width * 2., end_y - start_y);
+ let highlight_bounds = RectF::new(highlight_origin, highlight_size);
+
+ cx.scene().push_quad(Quad {
+ bounds: highlight_bounds,
+ background: Some(diff_style.deleted),
+ border: Border::new(0., Color::transparent_black()).into(),
+ corner_radii: (1. * line_height).into(),
+ });
+
+ continue;
+ }
+ };
+
+ let start_row = display_row_range.start;
+ let end_row = display_row_range.end;
+
+ let start_y = start_row as f32 * line_height - scroll_top;
+ let end_y = end_row as f32 * line_height - scroll_top;
+
+ let width = diff_style.width_em * line_height;
+ let highlight_origin = bounds.origin() + vec2f(-width, start_y);
+ let highlight_size = vec2f(width * 2., end_y - start_y);
+ let highlight_bounds = RectF::new(highlight_origin, highlight_size);
+
+ cx.scene().push_quad(Quad {
+ bounds: highlight_bounds,
+ background: Some(color),
+ border: Border::new(0., Color::transparent_black()).into(),
+ corner_radii: (diff_style.corner_radius * line_height).into(),
+ });
+ }
+ }
+
+ fn paint_text(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &mut LayoutState,
+ editor: &mut Editor,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let style = &self.style;
+ let scroll_position = layout.position_map.snapshot.scroll_position();
+ let start_row = layout.visible_display_row_range.start;
+ let scroll_top = scroll_position.y() * layout.position_map.line_height;
+ let max_glyph_width = layout.position_map.em_width;
+ let scroll_left = scroll_position.x() * max_glyph_width;
+ let content_origin = bounds.origin() + vec2f(layout.gutter_margin, 0.);
+ let line_end_overshoot = 0.15 * layout.position_map.line_height;
+ let whitespace_setting = editor.buffer.read(cx).settings_at(0, cx).show_whitespaces;
+
+ cx.scene().push_layer(Some(bounds));
+
+ cx.scene().push_cursor_region(CursorRegion {
+ bounds,
+ style: if !editor.link_go_to_definition_state.definitions.is_empty() {
+ CursorStyle::PointingHand
+ } else {
+ CursorStyle::IBeam
+ },
+ });
+
+ let fold_corner_radius =
+ self.style.folds.ellipses.corner_radius_factor * layout.position_map.line_height;
+ for (id, range, color) in layout.fold_ranges.iter() {
+ self.paint_highlighted_range(
+ range.clone(),
+ *color,
+ fold_corner_radius,
+ fold_corner_radius * 2.,
+ layout,
+ content_origin,
+ scroll_top,
+ scroll_left,
+ bounds,
+ cx,
+ );
+
+ for bound in range_to_bounds(
+ &range,
+ content_origin,
+ scroll_left,
+ scroll_top,
+ &layout.visible_display_row_range,
+ line_end_overshoot,
+ &layout.position_map,
+ ) {
+ cx.scene().push_cursor_region(CursorRegion {
+ bounds: bound,
+ style: CursorStyle::PointingHand,
+ });
+
+ let display_row = range.start.row();
+
+ let buffer_row = DisplayPoint::new(display_row, 0)
+ .to_point(&layout.position_map.snapshot.display_snapshot)
+ .row;
+
+ let view_id = cx.view_id();
+ cx.scene().push_mouse_region(
+ MouseRegion::new::<FoldMarkers>(view_id, *id as usize, bound)
+ .on_click(MouseButton::Left, move |_, editor: &mut Editor, cx| {
+ editor.unfold_at(&UnfoldAt { buffer_row }, cx)
+ })
+ .with_notify_on_hover(true)
+ .with_notify_on_click(true),
+ )
+ }
+ }
+
+ for (range, color) in &layout.highlighted_ranges {
+ self.paint_highlighted_range(
+ range.clone(),
+ *color,
+ 0.,
+ line_end_overshoot,
+ layout,
+ content_origin,
+ scroll_top,
+ scroll_left,
+ bounds,
+ cx,
+ );
+ }
+
+ let mut cursors = SmallVec::<[Cursor; 32]>::new();
+ let corner_radius = 0.15 * layout.position_map.line_height;
+ let mut invisible_display_ranges = SmallVec::<[Range<DisplayPoint>; 32]>::new();
+
+ for (selection_style, selections) in &layout.selections {
+ for selection in selections {
+ self.paint_highlighted_range(
+ selection.range.clone(),
+ selection_style.selection,
+ corner_radius,
+ corner_radius * 2.,
+ layout,
+ content_origin,
+ scroll_top,
+ scroll_left,
+ bounds,
+ cx,
+ );
+
+ if selection.is_local && !selection.range.is_empty() {
+ invisible_display_ranges.push(selection.range.clone());
+ }
+ if !selection.is_local || editor.show_local_cursors(cx) {
+ let cursor_position = selection.head;
+ if layout
+ .visible_display_row_range
+ .contains(&cursor_position.row())
+ {
+ let cursor_row_layout = &layout.position_map.line_layouts
+ [(cursor_position.row() - start_row) as usize]
+ .line;
+ let cursor_column = cursor_position.column() as usize;
+
+ let cursor_character_x = cursor_row_layout.x_for_index(cursor_column);
+ let mut block_width =
+ cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x;
+ if block_width == 0.0 {
+ block_width = layout.position_map.em_width;
+ }
+ let block_text = if let CursorShape::Block = selection.cursor_shape {
+ layout
+ .position_map
+ .snapshot
+ .chars_at(cursor_position)
+ .next()
+ .and_then(|(character, _)| {
+ let font_id =
+ cursor_row_layout.font_for_index(cursor_column)?;
+ let text = character.to_string();
+
+ Some(cx.text_layout_cache().layout_str(
+ &text,
+ cursor_row_layout.font_size(),
+ &[(
+ text.chars().count(),
+ RunStyle {
+ font_id,
+ color: style.background,
+ underline: Default::default(),
+ },
+ )],
+ ))
+ })
+ } else {
+ None
+ };
+
+ let x = cursor_character_x - scroll_left;
+ let y = cursor_position.row() as f32 * layout.position_map.line_height
+ - scroll_top;
+ if selection.is_newest {
+ editor.pixel_position_of_newest_cursor = Some(vec2f(
+ bounds.origin_x() + x + block_width / 2.,
+ bounds.origin_y() + y + layout.position_map.line_height / 2.,
+ ));
+ }
+ cursors.push(Cursor {
+ color: selection_style.cursor,
+ block_width,
+ origin: vec2f(x, y),
+ line_height: layout.position_map.line_height,
+ shape: selection.cursor_shape,
+ block_text,
+ });
+ }
+ }
+ }
+ }
+
+ if let Some(visible_text_bounds) = bounds.intersection(visible_bounds) {
+ for (ix, line_with_invisibles) in layout.position_map.line_layouts.iter().enumerate() {
+ let row = start_row + ix as u32;
+ line_with_invisibles.draw(
+ layout,
+ row,
+ scroll_top,
+ content_origin,
+ scroll_left,
+ visible_text_bounds,
+ whitespace_setting,
+ &invisible_display_ranges,
+ visible_bounds,
+ cx,
+ )
+ }
+ }
+
+ cx.scene().push_layer(Some(bounds));
+ for cursor in cursors {
+ cursor.paint(content_origin, cx);
+ }
+ cx.scene().pop_layer();
+
+ if let Some((position, context_menu)) = layout.context_menu.as_mut() {
+ cx.scene().push_stacking_context(None, None);
+ let cursor_row_layout =
+ &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
+ let x = cursor_row_layout.x_for_index(position.column() as usize) - scroll_left;
+ let y = (position.row() + 1) as f32 * layout.position_map.line_height - scroll_top;
+ let mut list_origin = content_origin + vec2f(x, y);
+ let list_width = context_menu.size().x();
+ let list_height = context_menu.size().y();
+
+ // Snap the right edge of the list to the right edge of the window if
+ // its horizontal bounds overflow.
+ if list_origin.x() + list_width > cx.window_size().x() {
+ list_origin.set_x((cx.window_size().x() - list_width).max(0.));
+ }
+
+ if list_origin.y() + list_height > bounds.max_y() {
+ list_origin.set_y(list_origin.y() - layout.position_map.line_height - list_height);
+ }
+
+ context_menu.paint(
+ list_origin,
+ RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+ editor,
+ cx,
+ );
+
+ cx.scene().pop_stacking_context();
+ }
+
+ if let Some((position, hover_popovers)) = layout.hover_popovers.as_mut() {
+ cx.scene().push_stacking_context(None, None);
+
+ // This is safe because we check on layout whether the required row is available
+ let hovered_row_layout =
+ &layout.position_map.line_layouts[(position.row() - start_row) as usize].line;
+
+ // Minimum required size: Take the first popover, and add 1.5 times the minimum popover
+ // height. This is the size we will use to decide whether to render popovers above or below
+ // the hovered line.
+ let first_size = hover_popovers[0].size();
+ let height_to_reserve = first_size.y()
+ + 1.5 * MIN_POPOVER_LINE_HEIGHT as f32 * layout.position_map.line_height;
+
+ // Compute Hovered Point
+ let x = hovered_row_layout.x_for_index(position.column() as usize) - scroll_left;
+ let y = position.row() as f32 * layout.position_map.line_height - scroll_top;
+ let hovered_point = content_origin + vec2f(x, y);
+
+ if hovered_point.y() - height_to_reserve > 0.0 {
+ // There is enough space above. Render popovers above the hovered point
+ let mut current_y = hovered_point.y();
+ for hover_popover in hover_popovers {
+ let size = hover_popover.size();
+ let mut popover_origin = vec2f(hovered_point.x(), current_y - size.y());
+
+ let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
+ if x_out_of_bounds < 0.0 {
+ popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
+ }
+
+ hover_popover.paint(
+ popover_origin,
+ RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+ editor,
+ cx,
+ );
+
+ current_y = popover_origin.y() - HOVER_POPOVER_GAP;
+ }
+ } else {
+ // There is not enough space above. Render popovers below the hovered point
+ let mut current_y = hovered_point.y() + layout.position_map.line_height;
+ for hover_popover in hover_popovers {
+ let size = hover_popover.size();
+ let mut popover_origin = vec2f(hovered_point.x(), current_y);
+
+ let x_out_of_bounds = bounds.max_x() - (popover_origin.x() + size.x());
+ if x_out_of_bounds < 0.0 {
+ popover_origin.set_x(popover_origin.x() + x_out_of_bounds);
+ }
+
+ hover_popover.paint(
+ popover_origin,
+ RectF::from_points(Vector2F::zero(), vec2f(f32::MAX, f32::MAX)), // Let content bleed outside of editor
+ editor,
+ cx,
+ );
+
+ current_y = popover_origin.y() + size.y() + HOVER_POPOVER_GAP;
+ }
+ }
+
+ cx.scene().pop_stacking_context();
+ }
+
+ cx.scene().pop_layer();
+ }
+
+ fn scrollbar_left(&self, bounds: &RectF) -> f32 {
+ bounds.max_x() - self.style.theme.scrollbar.width
+ }
+
+ fn paint_scrollbar(
+ &mut self,
+ bounds: RectF,
+ layout: &mut LayoutState,
+ editor: &Editor,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ enum ScrollbarMouseHandlers {}
+ if layout.mode != EditorMode::Full {
+ return;
+ }
+
+ let style = &self.style.theme.scrollbar;
+
+ let top = bounds.min_y();
+ let bottom = bounds.max_y();
+ let right = bounds.max_x();
+ let left = self.scrollbar_left(&bounds);
+ let row_range = &layout.scrollbar_row_range;
+ let max_row = layout.max_row as f32 + (row_range.end - row_range.start);
+
+ let mut height = bounds.height();
+ let mut first_row_y_offset = 0.0;
+
+ // Impose a minimum height on the scrollbar thumb
+ let row_height = height / max_row;
+ let min_thumb_height =
+ style.min_height_factor * cx.font_cache.line_height(self.style.text.font_size);
+ let thumb_height = (row_range.end - row_range.start) * row_height;
+ if thumb_height < min_thumb_height {
+ first_row_y_offset = (min_thumb_height - thumb_height) / 2.0;
+ height -= min_thumb_height - thumb_height;
+ }
+
+ let y_for_row = |row: f32| -> f32 { top + first_row_y_offset + row * row_height };
+
+ let thumb_top = y_for_row(row_range.start) - first_row_y_offset;
+ let thumb_bottom = y_for_row(row_range.end) + first_row_y_offset;
+ let track_bounds = RectF::from_points(vec2f(left, top), vec2f(right, bottom));
+ let thumb_bounds = RectF::from_points(vec2f(left, thumb_top), vec2f(right, thumb_bottom));
+
+ if layout.show_scrollbars {
+ cx.scene().push_quad(Quad {
+ bounds: track_bounds,
+ border: style.track.border.into(),
+ background: style.track.background_color,
+ ..Default::default()
+ });
+ let scrollbar_settings = settings::get::<EditorSettings>(cx).scrollbar;
+ let theme = theme::current(cx);
+ let scrollbar_theme = &theme.editor.scrollbar;
+ if layout.is_singleton && scrollbar_settings.selections {
+ let start_anchor = Anchor::min();
+ let end_anchor = Anchor::max();
+ let color = scrollbar_theme.selections;
+ let border = Border {
+ width: 1.,
+ color: style.thumb.border.color,
+ overlay: false,
+ top: false,
+ right: true,
+ bottom: false,
+ left: true,
+ };
+ let mut push_region = |start: DisplayPoint, end: DisplayPoint| {
+ let start_y = y_for_row(start.row() as f32);
+ let mut end_y = y_for_row(end.row() as f32);
+ if end_y - start_y < 1. {
+ end_y = start_y + 1.;
+ }
+ let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+ cx.scene().push_quad(Quad {
+ bounds,
+ background: Some(color),
+ border: border.into(),
+ corner_radii: style.thumb.corner_radii.into(),
+ })
+ };
+ let background_ranges = editor
+ .background_highlight_row_ranges::<crate::items::BufferSearchHighlights>(
+ start_anchor..end_anchor,
+ &layout.position_map.snapshot,
+ 50000,
+ );
+ for row in background_ranges {
+ let start = row.start();
+ let end = row.end();
+ push_region(*start, *end);
+ }
+ }
+
+ if layout.is_singleton && scrollbar_settings.git_diff {
+ let diff_style = scrollbar_theme.git.clone();
+ for hunk in layout
+ .position_map
+ .snapshot
+ .buffer_snapshot
+ .git_diff_hunks_in_range(0..(max_row.floor() as u32))
+ {
+ let start_display = Point::new(hunk.buffer_range.start, 0)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
+ let end_display = Point::new(hunk.buffer_range.end, 0)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
+ let start_y = y_for_row(start_display.row() as f32);
+ let mut end_y = if hunk.buffer_range.start == hunk.buffer_range.end {
+ y_for_row((end_display.row() + 1) as f32)
+ } else {
+ y_for_row((end_display.row()) as f32)
+ };
+
+ if end_y - start_y < 1. {
+ end_y = start_y + 1.;
+ }
+ let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y));
+
+ let color = match hunk.status() {
+ DiffHunkStatus::Added => diff_style.inserted,
+ DiffHunkStatus::Modified => diff_style.modified,
+ DiffHunkStatus::Removed => diff_style.deleted,
+ };
+
+ let border = Border {
+ width: 1.,
+ color: style.thumb.border.color,
+ overlay: false,
+ top: false,
+ right: true,
+ bottom: false,
+ left: true,
+ };
+
+ cx.scene().push_quad(Quad {
+ bounds,
+ background: Some(color),
+ border: border.into(),
+ corner_radii: style.thumb.corner_radii.into(),
+ })
+ }
+ }
+
+ cx.scene().push_quad(Quad {
+ bounds: thumb_bounds,
+ border: style.thumb.border.into(),
+ background: style.thumb.background_color,
+ corner_radii: style.thumb.corner_radii.into(),
+ });
+ }
+
+ cx.scene().push_cursor_region(CursorRegion {
+ bounds: track_bounds,
+ style: CursorStyle::Arrow,
+ });
+ let region_id = cx.view_id();
+ cx.scene().push_mouse_region(
+ MouseRegion::new::<ScrollbarMouseHandlers>(region_id, region_id, track_bounds)
+ .on_move(move |event, editor: &mut Editor, cx| {
+ if event.pressed_button.is_none() {
+ editor.scroll_manager.show_scrollbar(cx);
+ }
+ })
+ .on_down(MouseButton::Left, {
+ let row_range = row_range.clone();
+ move |event, editor: &mut Editor, cx| {
+ let y = event.position.y();
+ if y < thumb_top || thumb_bottom < y {
+ let center_row = ((y - top) * max_row as f32 / height).round() as u32;
+ let top_row = center_row
+ .saturating_sub((row_range.end - row_range.start) as u32 / 2);
+ let mut position = editor.scroll_position(cx);
+ position.set_y(top_row as f32);
+ editor.set_scroll_position(position, cx);
+ } else {
+ editor.scroll_manager.show_scrollbar(cx);
+ }
+ }
+ })
+ .on_drag(MouseButton::Left, {
+ move |event, editor: &mut Editor, cx| {
+ if event.end {
+ return;
+ }
+
+ let y = event.prev_mouse_position.y();
+ let new_y = event.position.y();
+ if thumb_top < y && y < thumb_bottom {
+ let mut position = editor.scroll_position(cx);
+ position.set_y(position.y() + (new_y - y) * (max_row as f32) / height);
+ if position.y() < 0.0 {
+ position.set_y(0.);
+ }
+ editor.set_scroll_position(position, cx);
+ }
+ }
+ }),
+ );
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn paint_highlighted_range(
+ &self,
+ range: Range<DisplayPoint>,
+ color: Color,
+ corner_radius: f32,
+ line_end_overshoot: f32,
+ layout: &LayoutState,
+ content_origin: Vector2F,
+ scroll_top: f32,
+ scroll_left: f32,
+ bounds: RectF,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let start_row = layout.visible_display_row_range.start;
+ let end_row = layout.visible_display_row_range.end;
+ if range.start != range.end {
+ let row_range = if range.end.column() == 0 {
+ cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+ } else {
+ cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+ };
+
+ let highlighted_range = HighlightedRange {
+ color,
+ line_height: layout.position_map.line_height,
+ corner_radius,
+ start_y: content_origin.y()
+ + row_range.start as f32 * layout.position_map.line_height
+ - scroll_top,
+ lines: row_range
+ .into_iter()
+ .map(|row| {
+ let line_layout =
+ &layout.position_map.line_layouts[(row - start_row) as usize].line;
+ HighlightedRangeLine {
+ start_x: if row == range.start.row() {
+ content_origin.x()
+ + line_layout.x_for_index(range.start.column() as usize)
+ - scroll_left
+ } else {
+ content_origin.x() - scroll_left
+ },
+ end_x: if row == range.end.row() {
+ content_origin.x()
+ + line_layout.x_for_index(range.end.column() as usize)
+ - scroll_left
+ } else {
+ content_origin.x() + line_layout.width() + line_end_overshoot
+ - scroll_left
+ },
+ }
+ })
+ .collect(),
+ };
+
+ highlighted_range.paint(bounds, cx);
+ }
+ }
+
+ fn paint_blocks(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &mut LayoutState,
+ editor: &mut Editor,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let scroll_position = layout.position_map.snapshot.scroll_position();
+ let scroll_left = scroll_position.x() * layout.position_map.em_width;
+ let scroll_top = scroll_position.y() * layout.position_map.line_height;
+
+ for block in &mut layout.blocks {
+ let mut origin = bounds.origin()
+ + vec2f(
+ 0.,
+ block.row as f32 * layout.position_map.line_height - scroll_top,
+ );
+ if !matches!(block.style, BlockStyle::Sticky) {
+ origin += vec2f(-scroll_left, 0.);
+ }
+ block.element.paint(origin, visible_bounds, editor, cx);
+ }
+ }
+
+ fn column_pixels(&self, column: usize, cx: &ViewContext<Editor>) -> f32 {
+ let style = &self.style;
+
+ cx.text_layout_cache()
+ .layout_str(
+ " ".repeat(column).as_str(),
+ style.text.font_size,
+ &[(
+ column,
+ RunStyle {
+ font_id: style.text.font_id,
+ color: Color::black(),
+ underline: Default::default(),
+ },
+ )],
+ )
+ .width()
+ }
+
+ fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext<Editor>) -> f32 {
+ let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1;
+ self.column_pixels(digit_count, cx)
+ }
+
+ //Folds contained in a hunk are ignored apart from shrinking visual size
+ //If a fold contains any hunks then that fold line is marked as modified
+ fn layout_git_gutters(
+ &self,
+ display_rows: Range<u32>,
+ snapshot: &EditorSnapshot,
+ ) -> Vec<DisplayDiffHunk> {
+ let buffer_snapshot = &snapshot.buffer_snapshot;
+
+ let buffer_start_row = DisplayPoint::new(display_rows.start, 0)
+ .to_point(snapshot)
+ .row;
+ let buffer_end_row = DisplayPoint::new(display_rows.end, 0)
+ .to_point(snapshot)
+ .row;
+
+ buffer_snapshot
+ .git_diff_hunks_in_range(buffer_start_row..buffer_end_row)
+ .map(|hunk| diff_hunk_to_display(hunk, snapshot))
+ .dedup()
+ .collect()
+ }
+
+ fn calculate_relative_line_numbers(
+ &self,
+ snapshot: &EditorSnapshot,
+ rows: &Range<u32>,
+ relative_to: Option<u32>,
+ ) -> HashMap<u32, u32> {
+ let mut relative_rows: HashMap<u32, u32> = Default::default();
+ let Some(relative_to) = relative_to else {
+ return relative_rows;
+ };
+
+ let start = rows.start.min(relative_to);
+ let end = rows.end.max(relative_to);
+
+ let buffer_rows = snapshot
+ .buffer_rows(start)
+ .take(1 + (end - start) as usize)
+ .collect::<Vec<_>>();
+
+ let head_idx = relative_to - start;
+ let mut delta = 1;
+ let mut i = head_idx + 1;
+ while i < buffer_rows.len() as u32 {
+ if buffer_rows[i as usize].is_some() {
+ if rows.contains(&(i + start)) {
+ relative_rows.insert(i + start, delta);
+ }
+ delta += 1;
+ }
+ i += 1;
+ }
+ delta = 1;
+ i = head_idx.min(buffer_rows.len() as u32 - 1);
+ while i > 0 && buffer_rows[i as usize].is_none() {
+ i -= 1;
+ }
+
+ while i > 0 {
+ i -= 1;
+ if buffer_rows[i as usize].is_some() {
+ if rows.contains(&(i + start)) {
+ relative_rows.insert(i + start, delta);
+ }
+ delta += 1;
+ }
+ }
+
+ relative_rows
+ }
+
+ fn layout_line_numbers(
+ &self,
+ rows: Range<u32>,
+ active_rows: &BTreeMap<u32, bool>,
+ newest_selection_head: DisplayPoint,
+ is_singleton: bool,
+ snapshot: &EditorSnapshot,
+ cx: &ViewContext<Editor>,
+ ) -> (
+ Vec<Option<text_layout::Line>>,
+ Vec<Option<(FoldStatus, BufferRow, bool)>>,
+ ) {
+ let style = &self.style;
+ let include_line_numbers = snapshot.mode == EditorMode::Full;
+ let mut line_number_layouts = Vec::with_capacity(rows.len());
+ let mut fold_statuses = Vec::with_capacity(rows.len());
+ let mut line_number = String::new();
+ let is_relative = settings::get::<EditorSettings>(cx).relative_line_numbers;
+ let relative_to = if is_relative {
+ Some(newest_selection_head.row())
+ } else {
+ None
+ };
+
+ let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to);
+
+ for (ix, row) in snapshot
+ .buffer_rows(rows.start)
+ .take((rows.end - rows.start) as usize)
+ .enumerate()
+ {
+ let display_row = rows.start + ix as u32;
+ let (active, color) = if active_rows.contains_key(&display_row) {
+ (true, style.line_number_active)
+ } else {
+ (false, style.line_number)
+ };
+ if let Some(buffer_row) = row {
+ if include_line_numbers {
+ line_number.clear();
+ let default_number = buffer_row + 1;
+ let number = relative_rows
+ .get(&(ix as u32 + rows.start))
+ .unwrap_or(&default_number);
+ write!(&mut line_number, "{}", number).unwrap();
+ line_number_layouts.push(Some(cx.text_layout_cache().layout_str(
+ &line_number,
+ style.text.font_size,
+ &[(
+ line_number.len(),
+ RunStyle {
+ font_id: style.text.font_id,
+ color,
+ underline: Default::default(),
+ },
+ )],
+ )));
+ fold_statuses.push(
+ is_singleton
+ .then(|| {
+ snapshot
+ .fold_for_line(buffer_row)
+ .map(|fold_status| (fold_status, buffer_row, active))
+ })
+ .flatten(),
+ )
+ }
+ } else {
+ fold_statuses.push(None);
+ line_number_layouts.push(None);
+ }
+ }
+
+ (line_number_layouts, fold_statuses)
+ }
+
+ fn layout_lines(
+ &mut self,
+ rows: Range<u32>,
+ line_number_layouts: &[Option<Line>],
+ snapshot: &EditorSnapshot,
+ cx: &ViewContext<Editor>,
+ ) -> Vec<LineWithInvisibles> {
+ if rows.start >= rows.end {
+ return Vec::new();
+ }
+
+ // When the editor is empty and unfocused, then show the placeholder.
+ if snapshot.is_empty() {
+ let placeholder_style = self
+ .style
+ .placeholder_text
+ .as_ref()
+ .unwrap_or(&self.style.text);
+ let placeholder_text = snapshot.placeholder_text();
+ let placeholder_lines = placeholder_text
+ .as_ref()
+ .map_or("", AsRef::as_ref)
+ .split('\n')
+ .skip(rows.start as usize)
+ .chain(iter::repeat(""))
+ .take(rows.len());
+ placeholder_lines
+ .map(|line| {
+ cx.text_layout_cache().layout_str(
+ line,
+ placeholder_style.font_size,
+ &[(
+ line.len(),
+ RunStyle {
+ font_id: placeholder_style.font_id,
+ color: placeholder_style.color,
+ underline: Default::default(),
+ },
+ )],
+ )
+ })
+ .map(|line| LineWithInvisibles {
+ line,
+ invisibles: Vec::new(),
+ })
+ .collect()
+ } else {
+ let style = &self.style;
+ let chunks = snapshot.highlighted_chunks(rows.clone(), true, style);
+
+ LineWithInvisibles::from_chunks(
+ chunks,
+ &style.text,
+ cx.text_layout_cache(),
+ cx.font_cache(),
+ MAX_LINE_LEN,
+ rows.len() as usize,
+ line_number_layouts,
+ snapshot.mode,
+ )
+ }
+ }
+
+ #[allow(clippy::too_many_arguments)]
+ fn layout_blocks(
+ &mut self,
+ rows: Range<u32>,
+ snapshot: &EditorSnapshot,
+ editor_width: f32,
+ scroll_width: f32,
+ gutter_padding: f32,
+ gutter_width: f32,
+ em_width: f32,
+ text_x: f32,
+ line_height: f32,
+ style: &EditorStyle,
+ line_layouts: &[LineWithInvisibles],
+ editor: &mut Editor,
+ cx: &mut ViewContext<Editor>,
+ ) -> (f32, Vec<BlockLayout>) {
+ let mut block_id = 0;
+ let scroll_x = snapshot.scroll_anchor.offset.x();
+ let (fixed_blocks, non_fixed_blocks) = snapshot
+ .blocks_in_range(rows.clone())
+ .partition::<Vec<_>, _>(|(_, block)| match block {
+ TransformBlock::ExcerptHeader { .. } => false,
+ TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed,
+ });
+ let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| {
+ let mut element = match block {
+ TransformBlock::Custom(block) => {
+ let align_to = block
+ .position()
+ .to_point(&snapshot.buffer_snapshot)
+ .to_display_point(snapshot);
+ let anchor_x = text_x
+ + if rows.contains(&align_to.row()) {
+ line_layouts[(align_to.row() - rows.start) as usize]
+ .line
+ .x_for_index(align_to.column() as usize)
+ } else {
+ layout_line(align_to.row(), snapshot, style, cx.text_layout_cache())
+ .x_for_index(align_to.column() as usize)
+ };
+
+ block.render(&mut BlockContext {
+ view_context: cx,
+ anchor_x,
+ gutter_padding,
+ line_height,
+ scroll_x,
+ gutter_width,
+ em_width,
+ block_id,
+ })
+ }
+ TransformBlock::ExcerptHeader {
+ id,
+ buffer,
+ range,
+ starts_new_buffer,
+ ..
+ } => {
+ let tooltip_style = theme::current(cx).tooltip.clone();
+ let include_root = editor
+ .project
+ .as_ref()
+ .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+ .unwrap_or_default();
+ let jump_icon = project::File::from_dyn(buffer.file()).map(|file| {
+ let jump_path = ProjectPath {
+ worktree_id: file.worktree_id(cx),
+ path: file.path.clone(),
+ };
+ let jump_anchor = range
+ .primary
+ .as_ref()
+ .map_or(range.context.start, |primary| primary.start);
+ let jump_position = language::ToPoint::to_point(&jump_anchor, buffer);
+
+ enum JumpIcon {}
+ MouseEventHandler::new::<JumpIcon, _>((*id).into(), cx, |state, _| {
+ let style = style.jump_icon.style_for(state);
+ Svg::new("icons/arrow_up_right.svg")
+ .with_color(style.color)
+ .constrained()
+ .with_width(style.icon_width)
+ .aligned()
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_width(style.button_width)
+ .with_height(style.button_width)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_click(MouseButton::Left, move |_, editor, cx| {
+ if let Some(workspace) = editor
+ .workspace
+ .as_ref()
+ .and_then(|(workspace, _)| workspace.upgrade(cx))
+ {
+ workspace.update(cx, |workspace, cx| {
+ Editor::jump(
+ workspace,
+ jump_path.clone(),
+ jump_position,
+ jump_anchor,
+ cx,
+ );
+ });
+ }
+ })
+ .with_tooltip::<JumpIcon>(
+ (*id).into(),
+ "Jump to Buffer".to_string(),
+ Some(Box::new(crate::OpenExcerpts)),
+ tooltip_style.clone(),
+ cx,
+ )
+ .aligned()
+ .flex_float()
+ });
+
+ if *starts_new_buffer {
+ let editor_font_size = style.text.font_size;
+ let style = &style.diagnostic_path_header;
+ let font_size = (style.text_scale_factor * editor_font_size).round();
+
+ let path = buffer.resolve_file_path(cx, include_root);
+ let mut filename = None;
+ let mut parent_path = None;
+ // Can't use .and_then() because `.file_name()` and `.parent()` return references :(
+ if let Some(path) = path {
+ filename = path.file_name().map(|f| f.to_string_lossy().to_string());
+ parent_path =
+ path.parent().map(|p| p.to_string_lossy().to_string() + "/");
+ }
+
+ Flex::row()
+ .with_child(
+ Label::new(
+ filename.unwrap_or_else(|| "untitled".to_string()),
+ style.filename.text.clone().with_font_size(font_size),
+ )
+ .contained()
+ .with_style(style.filename.container)
+ .aligned(),
+ )
+ .with_children(parent_path.map(|path| {
+ Label::new(path, style.path.text.clone().with_font_size(font_size))
+ .contained()
+ .with_style(style.path.container)
+ .aligned()
+ }))
+ .with_children(jump_icon)
+ .contained()
+ .with_style(style.container)
+ .with_padding_left(gutter_padding)
+ .with_padding_right(gutter_padding)
+ .expanded()
+ .into_any_named("path header block")
+ } else {
+ let text_style = style.text.clone();
+ Flex::row()
+ .with_child(Label::new("โฏ", text_style))
+ .with_children(jump_icon)
+ .contained()
+ .with_padding_left(gutter_padding)
+ .with_padding_right(gutter_padding)
+ .expanded()
+ .into_any_named("collapsed context")
+ }
+ }
+ };
+
+ element.layout(
+ SizeConstraint {
+ min: Vector2F::zero(),
+ max: vec2f(width, block.height() as f32 * line_height),
+ },
+ editor,
+ cx,
+ );
+ element
+ };
+
+ let mut fixed_block_max_width = 0f32;
+ let mut blocks = Vec::new();
+ for (row, block) in fixed_blocks {
+ let element = render_block(block, f32::INFINITY, block_id);
+ block_id += 1;
+ fixed_block_max_width = fixed_block_max_width.max(element.size().x() + em_width);
+ blocks.push(BlockLayout {
+ row,
+ element,
+ style: BlockStyle::Fixed,
+ });
+ }
+ for (row, block) in non_fixed_blocks {
+ let style = match block {
+ TransformBlock::Custom(block) => block.style(),
+ TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky,
+ };
+ let width = match style {
+ BlockStyle::Sticky => editor_width,
+ BlockStyle::Flex => editor_width
+ .max(fixed_block_max_width)
+ .max(gutter_width + scroll_width),
+ BlockStyle::Fixed => unreachable!(),
+ };
+ let element = render_block(block, width, block_id);
+ block_id += 1;
+ blocks.push(BlockLayout {
+ row,
+ element,
+ style,
+ });
+ }
+ (
+ scroll_width.max(fixed_block_max_width - gutter_width),
+ blocks,
+ )
+ }
+}
+
+#[derive(Debug)]
+pub struct LineWithInvisibles {
+ pub line: Line,
+ invisibles: Vec<Invisible>,
+}
+
+impl LineWithInvisibles {
+ fn from_chunks<'a>(
+ chunks: impl Iterator<Item = HighlightedChunk<'a>>,
+ text_style: &TextStyle,
+ text_layout_cache: &TextLayoutCache,
+ font_cache: &Arc<FontCache>,
+ max_line_len: usize,
+ max_line_count: usize,
+ line_number_layouts: &[Option<Line>],
+ editor_mode: EditorMode,
+ ) -> Vec<Self> {
+ let mut layouts = Vec::with_capacity(max_line_count);
+ let mut line = String::new();
+ let mut invisibles = Vec::new();
+ let mut styles = Vec::new();
+ let mut non_whitespace_added = false;
+ let mut row = 0;
+ let mut line_exceeded_max_len = false;
+ for highlighted_chunk in chunks.chain([HighlightedChunk {
+ chunk: "\n",
+ style: None,
+ is_tab: false,
+ }]) {
+ for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
+ if ix > 0 {
+ layouts.push(Self {
+ line: text_layout_cache.layout_str(&line, text_style.font_size, &styles),
+ invisibles: invisibles.drain(..).collect(),
+ });
+
+ line.clear();
+ styles.clear();
+ row += 1;
+ line_exceeded_max_len = false;
+ non_whitespace_added = false;
+ if row == max_line_count {
+ return layouts;
+ }
+ }
+
+ if !line_chunk.is_empty() && !line_exceeded_max_len {
+ let text_style = if let Some(style) = highlighted_chunk.style {
+ text_style
+ .clone()
+ .highlight(style, font_cache)
+ .map(Cow::Owned)
+ .unwrap_or_else(|_| Cow::Borrowed(text_style))
+ } else {
+ Cow::Borrowed(text_style)
+ };
+
+ if line.len() + line_chunk.len() > max_line_len {
+ let mut chunk_len = max_line_len - line.len();
+ while !line_chunk.is_char_boundary(chunk_len) {
+ chunk_len -= 1;
+ }
+ line_chunk = &line_chunk[..chunk_len];
+ line_exceeded_max_len = true;
+ }
+
+ styles.push((
+ line_chunk.len(),
+ RunStyle {
+ font_id: text_style.font_id,
+ color: text_style.color,
+ underline: text_style.underline,
+ },
+ ));
+
+ if editor_mode == EditorMode::Full {
+ // Line wrap pads its contents with fake whitespaces,
+ // avoid printing them
+ let inside_wrapped_string = line_number_layouts
+ .get(row)
+ .and_then(|layout| layout.as_ref())
+ .is_none();
+ if highlighted_chunk.is_tab {
+ if non_whitespace_added || !inside_wrapped_string {
+ invisibles.push(Invisible::Tab {
+ line_start_offset: line.len(),
+ });
+ }
+ } else {
+ invisibles.extend(
+ line_chunk
+ .chars()
+ .enumerate()
+ .filter(|(_, line_char)| {
+ let is_whitespace = line_char.is_whitespace();
+ non_whitespace_added |= !is_whitespace;
+ is_whitespace
+ && (non_whitespace_added || !inside_wrapped_string)
+ })
+ .map(|(whitespace_index, _)| Invisible::Whitespace {
+ line_offset: line.len() + whitespace_index,
+ }),
+ )
+ }
+ }
+
+ line.push_str(line_chunk);
+ }
+ }
+ }
+
+ layouts
+ }
+
+ fn draw(
+ &self,
+ layout: &LayoutState,
+ row: u32,
+ scroll_top: f32,
+ content_origin: Vector2F,
+ scroll_left: f32,
+ visible_text_bounds: RectF,
+ whitespace_setting: ShowWhitespaceSetting,
+ selection_ranges: &[Range<DisplayPoint>],
+ visible_bounds: RectF,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let line_height = layout.position_map.line_height;
+ let line_y = row as f32 * line_height - scroll_top;
+
+ self.line.paint(
+ content_origin + vec2f(-scroll_left, line_y),
+ visible_text_bounds,
+ line_height,
+ cx,
+ );
+
+ self.draw_invisibles(
+ &selection_ranges,
+ layout,
+ content_origin,
+ scroll_left,
+ line_y,
+ row,
+ visible_bounds,
+ line_height,
+ whitespace_setting,
+ cx,
+ );
+ }
+
+ fn draw_invisibles(
+ &self,
+ selection_ranges: &[Range<DisplayPoint>],
+ layout: &LayoutState,
+ content_origin: Vector2F,
+ scroll_left: f32,
+ line_y: f32,
+ row: u32,
+ visible_bounds: RectF,
+ line_height: f32,
+ whitespace_setting: ShowWhitespaceSetting,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let allowed_invisibles_regions = match whitespace_setting {
+ ShowWhitespaceSetting::None => return,
+ ShowWhitespaceSetting::Selection => Some(selection_ranges),
+ ShowWhitespaceSetting::All => None,
+ };
+
+ for invisible in &self.invisibles {
+ let (&token_offset, invisible_symbol) = match invisible {
+ Invisible::Tab { line_start_offset } => (line_start_offset, &layout.tab_invisible),
+ Invisible::Whitespace { line_offset } => (line_offset, &layout.space_invisible),
+ };
+
+ let x_offset = self.line.x_for_index(token_offset);
+ let invisible_offset =
+ (layout.position_map.em_width - invisible_symbol.width()).max(0.0) / 2.0;
+ let origin = content_origin + vec2f(-scroll_left + x_offset + invisible_offset, line_y);
+
+ if let Some(allowed_regions) = allowed_invisibles_regions {
+ let invisible_point = DisplayPoint::new(row, token_offset as u32);
+ if !allowed_regions
+ .iter()
+ .any(|region| region.start <= invisible_point && invisible_point < region.end)
+ {
+ continue;
+ }
+ }
+ invisible_symbol.paint(origin, visible_bounds, line_height, cx);
+ }
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum Invisible {
+ Tab { line_start_offset: usize },
+ Whitespace { line_offset: usize },
+}
+
+impl Element<Editor> for EditorElement {
+ type LayoutState = LayoutState;
+ type PaintState = ();
+
+ fn layout(
+ &mut self,
+ constraint: SizeConstraint,
+ editor: &mut Editor,
+ cx: &mut ViewContext<Editor>,
+ ) -> (Vector2F, Self::LayoutState) {
+ let mut size = constraint.max;
+ if size.x().is_infinite() {
+ unimplemented!("we don't yet handle an infinite width constraint on buffer elements");
+ }
+
+ let snapshot = editor.snapshot(cx);
+ let style = self.style.clone();
+
+ let line_height = (style.text.font_size * style.line_height_scalar).round();
+
+ let gutter_padding;
+ let gutter_width;
+ let gutter_margin;
+ if snapshot.show_gutter {
+ let em_width = style.text.em_width(cx.font_cache());
+ gutter_padding = (em_width * style.gutter_padding_factor).round();
+ gutter_width = self.max_line_number_width(&snapshot, cx) + gutter_padding * 2.0;
+ gutter_margin = -style.text.descent(cx.font_cache());
+ } else {
+ gutter_padding = 0.0;
+ gutter_width = 0.0;
+ gutter_margin = 0.0;
+ };
+
+ let text_width = size.x() - gutter_width;
+ let em_width = style.text.em_width(cx.font_cache());
+ let em_advance = style.text.em_advance(cx.font_cache());
+ let overscroll = vec2f(em_width, 0.);
+ let snapshot = {
+ editor.set_visible_line_count(size.y() / line_height, cx);
+
+ let editor_width = text_width - gutter_margin - overscroll.x() - em_width;
+ let wrap_width = match editor.soft_wrap_mode(cx) {
+ SoftWrap::None => (MAX_LINE_LEN / 2) as f32 * em_advance,
+ SoftWrap::EditorWidth => editor_width,
+ SoftWrap::Column(column) => editor_width.min(column as f32 * em_advance),
+ };
+
+ if editor.set_wrap_width(Some(wrap_width), cx) {
+ editor.snapshot(cx)
+ } else {
+ snapshot
+ }
+ };
+
+ let wrap_guides = editor
+ .wrap_guides(cx)
+ .iter()
+ .map(|(guide, active)| (self.column_pixels(*guide, cx), *active))
+ .collect();
+
+ let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height;
+ if let EditorMode::AutoHeight { max_lines } = snapshot.mode {
+ size.set_y(
+ scroll_height
+ .min(constraint.max_along(Axis::Vertical))
+ .max(constraint.min_along(Axis::Vertical))
+ .max(line_height)
+ .min(line_height * max_lines as f32),
+ )
+ } else if let EditorMode::SingleLine = snapshot.mode {
+ size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
+ } else if size.y().is_infinite() {
+ size.set_y(scroll_height);
+ }
+ let gutter_size = vec2f(gutter_width, size.y());
+ let text_size = vec2f(text_width, size.y());
+
+ let autoscroll_horizontally = editor.autoscroll_vertically(size.y(), line_height, cx);
+ let mut snapshot = editor.snapshot(cx);
+
+ let scroll_position = snapshot.scroll_position();
+ // The scroll position is a fractional point, the whole number of which represents
+ // the top of the window in terms of display rows.
+ let start_row = scroll_position.y() as u32;
+ let height_in_lines = size.y() / line_height;
+ let max_row = snapshot.max_point().row();
+
+ // Add 1 to ensure selections bleed off screen
+ let end_row = 1 + cmp::min(
+ (scroll_position.y() + height_in_lines).ceil() as u32,
+ max_row,
+ );
+
+ let start_anchor = if start_row == 0 {
+ Anchor::min()
+ } else {
+ snapshot
+ .buffer_snapshot
+ .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&snapshot, Bias::Left))
+ };
+ let end_anchor = if end_row > max_row {
+ Anchor::max()
+ } else {
+ snapshot
+ .buffer_snapshot
+ .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
+ };
+
+ let mut selections: Vec<(SelectionStyle, Vec<SelectionLayout>)> = Vec::new();
+ let mut active_rows = BTreeMap::new();
+ let mut fold_ranges = Vec::new();
+ let is_singleton = editor.is_singleton(cx);
+
+ let highlighted_rows = editor.highlighted_rows();
+ let theme = theme::current(cx);
+ let highlighted_ranges = editor.background_highlights_in_range(
+ start_anchor..end_anchor,
+ &snapshot.display_snapshot,
+ theme.as_ref(),
+ );
+
+ fold_ranges.extend(
+ snapshot
+ .folds_in_range(start_anchor..end_anchor)
+ .map(|anchor| {
+ let start = anchor.start.to_point(&snapshot.buffer_snapshot);
+ (
+ start.row,
+ start.to_display_point(&snapshot.display_snapshot)
+ ..anchor.end.to_display_point(&snapshot),
+ )
+ }),
+ );
+
+ let mut newest_selection_head = None;
+
+ if editor.show_local_selections {
+ let mut local_selections: Vec<Selection<Point>> = editor
+ .selections
+ .disjoint_in_range(start_anchor..end_anchor, cx);
+ local_selections.extend(editor.selections.pending(cx));
+ let mut layouts = Vec::new();
+ let newest = editor.selections.newest(cx);
+ for selection in local_selections.drain(..) {
+ let is_empty = selection.start == selection.end;
+ let is_newest = selection == newest;
+
+ let layout = SelectionLayout::new(
+ selection,
+ editor.selections.line_mode,
+ editor.cursor_shape,
+ &snapshot.display_snapshot,
+ is_newest,
+ true,
+ );
+ if is_newest {
+ newest_selection_head = Some(layout.head);
+ }
+
+ for row in cmp::max(layout.active_rows.start, start_row)
+ ..=cmp::min(layout.active_rows.end, end_row)
+ {
+ let contains_non_empty_selection = active_rows.entry(row).or_insert(!is_empty);
+ *contains_non_empty_selection |= !is_empty;
+ }
+ layouts.push(layout);
+ }
+
+ selections.push((style.selection, layouts));
+ }
+
+ if let Some(collaboration_hub) = &editor.collaboration_hub {
+ // When following someone, render the local selections in their color.
+ if let Some(leader_id) = editor.leader_peer_id {
+ if let Some(collaborator) = collaboration_hub.collaborators(cx).get(&leader_id) {
+ if let Some(participant_index) = collaboration_hub
+ .user_participant_indices(cx)
+ .get(&collaborator.user_id)
+ {
+ if let Some((local_selection_style, _)) = selections.first_mut() {
+ *local_selection_style =
+ style.selection_style_for_room_participant(participant_index.0);
+ }
+ }
+ }
+ }
+
+ let mut remote_selections = HashMap::default();
+ for selection in snapshot.remote_selections_in_range(
+ &(start_anchor..end_anchor),
+ collaboration_hub.as_ref(),
+ cx,
+ ) {
+ let selection_style = if let Some(participant_index) = selection.participant_index {
+ style.selection_style_for_room_participant(participant_index.0)
+ } else {
+ style.absent_selection
+ };
+
+ // Don't re-render the leader's selections, since the local selections
+ // match theirs.
+ if Some(selection.peer_id) == editor.leader_peer_id {
+ continue;
+ }
+
+ remote_selections
+ .entry(selection.replica_id)
+ .or_insert((selection_style, Vec::new()))
+ .1
+ .push(SelectionLayout::new(
+ selection.selection,
+ selection.line_mode,
+ selection.cursor_shape,
+ &snapshot.display_snapshot,
+ false,
+ false,
+ ));
+ }
+
+ selections.extend(remote_selections.into_values());
+ }
+
+ let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
+ let show_scrollbars = match scrollbar_settings.show {
+ ShowScrollbar::Auto => {
+ // Git
+ (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs())
+ ||
+ // Selections
+ (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty())
+ // Scrollmanager
+ || editor.scroll_manager.scrollbars_visible()
+ }
+ ShowScrollbar::System => editor.scroll_manager.scrollbars_visible(),
+ ShowScrollbar::Always => true,
+ ShowScrollbar::Never => false,
+ };
+
+ let fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)> = fold_ranges
+ .into_iter()
+ .map(|(id, fold)| {
+ let color = self
+ .style
+ .folds
+ .ellipses
+ .background
+ .style_for(&mut cx.mouse_state::<FoldMarkers>(id as usize))
+ .color;
+
+ (id, fold, color)
+ })
+ .collect();
+
+ let head_for_relative = newest_selection_head.unwrap_or_else(|| {
+ let newest = editor.selections.newest::<Point>(cx);
+ SelectionLayout::new(
+ newest,
+ editor.selections.line_mode,
+ editor.cursor_shape,
+ &snapshot.display_snapshot,
+ true,
+ true,
+ )
+ .head
+ });
+
+ let (line_number_layouts, fold_statuses) = self.layout_line_numbers(
+ start_row..end_row,
+ &active_rows,
+ head_for_relative,
+ is_singleton,
+ &snapshot,
+ cx,
+ );
+
+ let display_hunks = self.layout_git_gutters(start_row..end_row, &snapshot);
+
+ let scrollbar_row_range = scroll_position.y()..(scroll_position.y() + height_in_lines);
+
+ let mut max_visible_line_width = 0.0;
+ let line_layouts =
+ self.layout_lines(start_row..end_row, &line_number_layouts, &snapshot, cx);
+ for line_with_invisibles in &line_layouts {
+ if line_with_invisibles.line.width() > max_visible_line_width {
+ max_visible_line_width = line_with_invisibles.line.width();
+ }
+ }
+
+ let style = self.style.clone();
+ let longest_line_width = layout_line(
+ snapshot.longest_row(),
+ &snapshot,
+ &style,
+ cx.text_layout_cache(),
+ )
+ .width();
+ let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.x();
+ let em_width = style.text.em_width(cx.font_cache());
+ let (scroll_width, blocks) = self.layout_blocks(
+ start_row..end_row,
+ &snapshot,
+ size.x(),
+ scroll_width,
+ gutter_padding,
+ gutter_width,
+ em_width,
+ gutter_width + gutter_margin,
+ line_height,
+ &style,
+ &line_layouts,
+ editor,
+ cx,
+ );
+
+ let scroll_max = vec2f(
+ ((scroll_width - text_size.x()) / em_width).max(0.0),
+ max_row as f32,
+ );
+
+ let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x());
+
+ let autoscrolled = if autoscroll_horizontally {
+ editor.autoscroll_horizontally(
+ start_row,
+ text_size.x(),
+ scroll_width,
+ em_width,
+ &line_layouts,
+ cx,
+ )
+ } else {
+ false
+ };
+
+ if clamped || autoscrolled {
+ snapshot = editor.snapshot(cx);
+ }
+
+ let style = editor.style(cx);
+
+ let mut context_menu = None;
+ let mut code_actions_indicator = None;
+ if let Some(newest_selection_head) = newest_selection_head {
+ if (start_row..end_row).contains(&newest_selection_head.row()) {
+ if editor.context_menu_visible() {
+ context_menu =
+ editor.render_context_menu(newest_selection_head, style.clone(), cx);
+ }
+
+ let active = matches!(
+ editor.context_menu.read().as_ref(),
+ Some(crate::ContextMenu::CodeActions(_))
+ );
+
+ code_actions_indicator = editor
+ .render_code_actions_indicator(&style, active, cx)
+ .map(|indicator| (newest_selection_head.row(), indicator));
+ }
+ }
+
+ let visible_rows = start_row..start_row + line_layouts.len() as u32;
+ let mut hover = editor.hover_state.render(
+ &snapshot,
+ &style,
+ visible_rows,
+ editor.workspace.as_ref().map(|(w, _)| w.clone()),
+ cx,
+ );
+ let mode = editor.mode;
+
+ let mut fold_indicators = editor.render_fold_indicators(
+ fold_statuses,
+ &style,
+ editor.gutter_hovered,
+ line_height,
+ gutter_margin,
+ cx,
+ );
+
+ if let Some((_, context_menu)) = context_menu.as_mut() {
+ context_menu.layout(
+ SizeConstraint {
+ min: Vector2F::zero(),
+ max: vec2f(
+ cx.window_size().x() * 0.7,
+ (12. * line_height).min((size.y() - line_height) / 2.),
+ ),
+ },
+ editor,
+ cx,
+ );
+ }
+
+ if let Some((_, indicator)) = code_actions_indicator.as_mut() {
+ indicator.layout(
+ SizeConstraint::strict_along(
+ Axis::Vertical,
+ line_height * style.code_actions.vertical_scale,
+ ),
+ editor,
+ cx,
+ );
+ }
+
+ for fold_indicator in fold_indicators.iter_mut() {
+ if let Some(indicator) = fold_indicator.as_mut() {
+ indicator.layout(
+ SizeConstraint::strict_along(
+ Axis::Vertical,
+ line_height * style.code_actions.vertical_scale,
+ ),
+ editor,
+ cx,
+ );
+ }
+ }
+
+ if let Some((_, hover_popovers)) = hover.as_mut() {
+ for hover_popover in hover_popovers.iter_mut() {
+ hover_popover.layout(
+ SizeConstraint {
+ min: Vector2F::zero(),
+ max: vec2f(
+ (120. * em_width) // Default size
+ .min(size.x() / 2.) // Shrink to half of the editor width
+ .max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
+ (16. * line_height) // Default size
+ .min(size.y() / 2.) // Shrink to half of the editor height
+ .max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
+ ),
+ },
+ editor,
+ cx,
+ );
+ }
+ }
+
+ let invisible_symbol_font_size = self.style.text.font_size / 2.0;
+ let invisible_symbol_style = RunStyle {
+ color: self.style.whitespace,
+ font_id: self.style.text.font_id,
+ underline: Default::default(),
+ };
+
+ (
+ size,
+ LayoutState {
+ mode,
+ position_map: Arc::new(PositionMap {
+ size,
+ scroll_max,
+ line_layouts,
+ line_height,
+ em_width,
+ em_advance,
+ snapshot,
+ }),
+ visible_display_row_range: start_row..end_row,
+ wrap_guides,
+ gutter_size,
+ gutter_padding,
+ text_size,
+ scrollbar_row_range,
+ show_scrollbars,
+ is_singleton,
+ max_row,
+ gutter_margin,
+ active_rows,
+ highlighted_rows,
+ highlighted_ranges,
+ fold_ranges,
+ line_number_layouts,
+ display_hunks,
+ blocks,
+ selections,
+ context_menu,
+ code_actions_indicator,
+ fold_indicators,
+ tab_invisible: cx.text_layout_cache().layout_str(
+ "โ",
+ invisible_symbol_font_size,
+ &[("โ".len(), invisible_symbol_style)],
+ ),
+ space_invisible: cx.text_layout_cache().layout_str(
+ "โข",
+ invisible_symbol_font_size,
+ &[("โข".len(), invisible_symbol_style)],
+ ),
+ hover_popovers: hover,
+ },
+ )
+ }
+
+ fn paint(
+ &mut self,
+ bounds: RectF,
+ visible_bounds: RectF,
+ layout: &mut Self::LayoutState,
+ editor: &mut Editor,
+ cx: &mut ViewContext<Editor>,
+ ) -> Self::PaintState {
+ let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
+ cx.scene().push_layer(Some(visible_bounds));
+
+ let gutter_bounds = RectF::new(bounds.origin(), layout.gutter_size);
+ let text_bounds = RectF::new(
+ bounds.origin() + vec2f(layout.gutter_size.x(), 0.0),
+ layout.text_size,
+ );
+
+ Self::attach_mouse_handlers(
+ &layout.position_map,
+ layout.hover_popovers.is_some(),
+ visible_bounds,
+ text_bounds,
+ gutter_bounds,
+ bounds,
+ cx,
+ );
+
+ self.paint_background(gutter_bounds, text_bounds, layout, cx);
+ if layout.gutter_size.x() > 0. {
+ self.paint_gutter(gutter_bounds, visible_bounds, layout, editor, cx);
+ }
+ self.paint_text(text_bounds, visible_bounds, layout, editor, cx);
+
+ cx.scene().push_layer(Some(bounds));
+ if !layout.blocks.is_empty() {
+ self.paint_blocks(bounds, visible_bounds, layout, editor, cx);
+ }
+ self.paint_scrollbar(bounds, layout, &editor, cx);
+ cx.scene().pop_layer();
+ cx.scene().pop_layer();
+ }
+
+ fn rect_for_text_range(
+ &self,
+ range_utf16: Range<usize>,
+ bounds: RectF,
+ _: RectF,
+ layout: &Self::LayoutState,
+ _: &Self::PaintState,
+ _: &Editor,
+ _: &ViewContext<Editor>,
+ ) -> Option<RectF> {
+ let text_bounds = RectF::new(
+ bounds.origin() + vec2f(layout.gutter_size.x(), 0.0),
+ layout.text_size,
+ );
+ let content_origin = text_bounds.origin() + vec2f(layout.gutter_margin, 0.);
+ let scroll_position = layout.position_map.snapshot.scroll_position();
+ let start_row = scroll_position.y() as u32;
+ let scroll_top = scroll_position.y() * layout.position_map.line_height;
+ let scroll_left = scroll_position.x() * layout.position_map.em_width;
+
+ let range_start = OffsetUtf16(range_utf16.start)
+ .to_display_point(&layout.position_map.snapshot.display_snapshot);
+ if range_start.row() < start_row {
+ return None;
+ }
+
+ let line = &layout
+ .position_map
+ .line_layouts
+ .get((range_start.row() - start_row) as usize)?
+ .line;
+ let range_start_x = line.x_for_index(range_start.column() as usize);
+ let range_start_y = range_start.row() as f32 * layout.position_map.line_height;
+ Some(RectF::new(
+ content_origin
+ + vec2f(
+ range_start_x,
+ range_start_y + layout.position_map.line_height,
+ )
+ - vec2f(scroll_left, scroll_top),
+ vec2f(
+ layout.position_map.em_width,
+ layout.position_map.line_height,
+ ),
+ ))
+ }
+
+ fn debug(
+ &self,
+ bounds: RectF,
+ _: &Self::LayoutState,
+ _: &Self::PaintState,
+ _: &Editor,
+ _: &ViewContext<Editor>,
+ ) -> json::Value {
+ json!({
+ "type": "BufferElement",
+ "bounds": bounds.to_json()
+ })
+ }
+}
+
+type BufferRow = u32;
+
+pub struct LayoutState {
+ position_map: Arc<PositionMap>,
+ gutter_size: Vector2F,
+ gutter_padding: f32,
+ gutter_margin: f32,
+ text_size: Vector2F,
+ mode: EditorMode,
+ wrap_guides: SmallVec<[(f32, bool); 2]>,
+ visible_display_row_range: Range<u32>,
+ active_rows: BTreeMap<u32, bool>,
+ highlighted_rows: Option<Range<u32>>,
+ line_number_layouts: Vec<Option<text_layout::Line>>,
+ display_hunks: Vec<DisplayDiffHunk>,
+ blocks: Vec<BlockLayout>,
+ highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
+ fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
+ selections: Vec<(SelectionStyle, Vec<SelectionLayout>)>,
+ scrollbar_row_range: Range<f32>,
+ show_scrollbars: bool,
+ is_singleton: bool,
+ max_row: u32,
+ context_menu: Option<(DisplayPoint, AnyElement<Editor>)>,
+ code_actions_indicator: Option<(u32, AnyElement<Editor>)>,
+ hover_popovers: Option<(DisplayPoint, Vec<AnyElement<Editor>>)>,
+ fold_indicators: Vec<Option<AnyElement<Editor>>>,
+ tab_invisible: Line,
+ space_invisible: Line,
+}
+
+struct PositionMap {
+ size: Vector2F,
+ line_height: f32,
+ scroll_max: Vector2F,
+ em_width: f32,
+ em_advance: f32,
+ line_layouts: Vec<LineWithInvisibles>,
+ snapshot: EditorSnapshot,
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct PointForPosition {
+ pub previous_valid: DisplayPoint,
+ pub next_valid: DisplayPoint,
+ pub exact_unclipped: DisplayPoint,
+ pub column_overshoot_after_line_end: u32,
+}
+
+impl PointForPosition {
+ #[cfg(test)]
+ pub fn valid(valid: DisplayPoint) -> Self {
+ Self {
+ previous_valid: valid,
+ next_valid: valid,
+ exact_unclipped: valid,
+ column_overshoot_after_line_end: 0,
+ }
+ }
+
+ pub fn as_valid(&self) -> Option<DisplayPoint> {
+ if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped {
+ Some(self.previous_valid)
+ } else {
+ None
+ }
+ }
+}
+
+impl PositionMap {
+ fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition {
+ let scroll_position = self.snapshot.scroll_position();
+ let position = position - text_bounds.origin();
+ let y = position.y().max(0.0).min(self.size.y());
+ let x = position.x() + (scroll_position.x() * self.em_width);
+ let row = (y / self.line_height + scroll_position.y()) as u32;
+ let (column, x_overshoot_after_line_end) = if let Some(line) = self
+ .line_layouts
+ .get(row as usize - scroll_position.y() as usize)
+ .map(|line_with_spaces| &line_with_spaces.line)
+ {
+ if let Some(ix) = line.index_for_x(x) {
+ (ix as u32, 0.0)
+ } else {
+ (line.len() as u32, 0f32.max(x - line.width()))
+ }
+ } else {
+ (0, x)
+ };
+
+ let mut exact_unclipped = DisplayPoint::new(row, column);
+ let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left);
+ let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right);
+
+ let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32;
+ *exact_unclipped.column_mut() += column_overshoot_after_line_end;
+ PointForPosition {
+ previous_valid,
+ next_valid,
+ exact_unclipped,
+ column_overshoot_after_line_end,
+ }
+ }
+}
+
+struct BlockLayout {
+ row: u32,
+ element: AnyElement<Editor>,
+ style: BlockStyle,
+}
+
+fn layout_line(
+ row: u32,
+ snapshot: &EditorSnapshot,
+ style: &EditorStyle,
+ layout_cache: &TextLayoutCache,
+) -> text_layout::Line {
+ let mut line = snapshot.line(row);
+
+ if line.len() > MAX_LINE_LEN {
+ let mut len = MAX_LINE_LEN;
+ while !line.is_char_boundary(len) {
+ len -= 1;
+ }
+
+ line.truncate(len);
+ }
+
+ layout_cache.layout_str(
+ &line,
+ style.text.font_size,
+ &[(
+ snapshot.line_len(row) as usize,
+ RunStyle {
+ font_id: style.text.font_id,
+ color: Color::black(),
+ underline: Default::default(),
+ },
+ )],
+ )
+}
+
+#[derive(Debug)]
+pub struct Cursor {
+ origin: Vector2F,
+ block_width: f32,
+ line_height: f32,
+ color: Color,
+ shape: CursorShape,
+ block_text: Option<Line>,
+}
+
+impl Cursor {
+ pub fn new(
+ origin: Vector2F,
+ block_width: f32,
+ line_height: f32,
+ color: Color,
+ shape: CursorShape,
+ block_text: Option<Line>,
+ ) -> Cursor {
+ Cursor {
+ origin,
+ block_width,
+ line_height,
+ color,
+ shape,
+ block_text,
+ }
+ }
+
+ pub fn bounding_rect(&self, origin: Vector2F) -> RectF {
+ RectF::new(
+ self.origin + origin,
+ vec2f(self.block_width, self.line_height),
+ )
+ }
+
+ pub fn paint(&self, origin: Vector2F, cx: &mut WindowContext) {
+ let bounds = match self.shape {
+ CursorShape::Bar => RectF::new(self.origin + origin, vec2f(2.0, self.line_height)),
+ CursorShape::Block | CursorShape::Hollow => RectF::new(
+ self.origin + origin,
+ vec2f(self.block_width, self.line_height),
+ ),
+ CursorShape::Underscore => RectF::new(
+ self.origin + origin + Vector2F::new(0.0, self.line_height - 2.0),
+ vec2f(self.block_width, 2.0),
+ ),
+ };
+
+ //Draw background or border quad
+ if matches!(self.shape, CursorShape::Hollow) {
+ cx.scene().push_quad(Quad {
+ bounds,
+ background: None,
+ border: Border::all(1., self.color).into(),
+ corner_radii: Default::default(),
+ });
+ } else {
+ cx.scene().push_quad(Quad {
+ bounds,
+ background: Some(self.color),
+ border: Default::default(),
+ corner_radii: Default::default(),
+ });
+ }
+
+ if let Some(block_text) = &self.block_text {
+ block_text.paint(self.origin + origin, bounds, self.line_height, cx);
+ }
+ }
+
+ pub fn shape(&self) -> CursorShape {
+ self.shape
+ }
+}
+
+#[derive(Debug)]
+pub struct HighlightedRange {
+ pub start_y: f32,
+ pub line_height: f32,
+ pub lines: Vec<HighlightedRangeLine>,
+ pub color: Color,
+ pub corner_radius: f32,
+}
+
+#[derive(Debug)]
+pub struct HighlightedRangeLine {
+ pub start_x: f32,
+ pub end_x: f32,
+}
+
+impl HighlightedRange {
+ pub fn paint(&self, bounds: RectF, cx: &mut WindowContext) {
+ if self.lines.len() >= 2 && self.lines[0].start_x > self.lines[1].end_x {
+ self.paint_lines(self.start_y, &self.lines[0..1], bounds, cx);
+ self.paint_lines(
+ self.start_y + self.line_height,
+ &self.lines[1..],
+ bounds,
+ cx,
+ );
+ } else {
+ self.paint_lines(self.start_y, &self.lines, bounds, cx);
+ }
+ }
+
+ fn paint_lines(
+ &self,
+ start_y: f32,
+ lines: &[HighlightedRangeLine],
+ bounds: RectF,
+ cx: &mut WindowContext,
+ ) {
+ if lines.is_empty() {
+ return;
+ }
+
+ let mut path = PathBuilder::new();
+ let first_line = lines.first().unwrap();
+ let last_line = lines.last().unwrap();
+
+ let first_top_left = vec2f(first_line.start_x, start_y);
+ let first_top_right = vec2f(first_line.end_x, start_y);
+
+ let curve_height = vec2f(0., self.corner_radius);
+ let curve_width = |start_x: f32, end_x: f32| {
+ let max = (end_x - start_x) / 2.;
+ let width = if max < self.corner_radius {
+ max
+ } else {
+ self.corner_radius
+ };
+
+ vec2f(width, 0.)
+ };
+
+ let top_curve_width = curve_width(first_line.start_x, first_line.end_x);
+ path.reset(first_top_right - top_curve_width);
+ path.curve_to(first_top_right + curve_height, first_top_right);
+
+ let mut iter = lines.iter().enumerate().peekable();
+ while let Some((ix, line)) = iter.next() {
+ let bottom_right = vec2f(line.end_x, start_y + (ix + 1) as f32 * self.line_height);
+
+ if let Some((_, next_line)) = iter.peek() {
+ let next_top_right = vec2f(next_line.end_x, bottom_right.y());
+
+ match next_top_right.x().partial_cmp(&bottom_right.x()).unwrap() {
+ Ordering::Equal => {
+ path.line_to(bottom_right);
+ }
+ Ordering::Less => {
+ let curve_width = curve_width(next_top_right.x(), bottom_right.x());
+ path.line_to(bottom_right - curve_height);
+ if self.corner_radius > 0. {
+ path.curve_to(bottom_right - curve_width, bottom_right);
+ }
+ path.line_to(next_top_right + curve_width);
+ if self.corner_radius > 0. {
+ path.curve_to(next_top_right + curve_height, next_top_right);
+ }
+ }
+ Ordering::Greater => {
+ let curve_width = curve_width(bottom_right.x(), next_top_right.x());
+ path.line_to(bottom_right - curve_height);
+ if self.corner_radius > 0. {
+ path.curve_to(bottom_right + curve_width, bottom_right);
+ }
+ path.line_to(next_top_right - curve_width);
+ if self.corner_radius > 0. {
+ path.curve_to(next_top_right + curve_height, next_top_right);
+ }
+ }
+ }
+ } else {
+ let curve_width = curve_width(line.start_x, line.end_x);
+ path.line_to(bottom_right - curve_height);
+ if self.corner_radius > 0. {
+ path.curve_to(bottom_right - curve_width, bottom_right);
+ }
+
+ let bottom_left = vec2f(line.start_x, bottom_right.y());
+ path.line_to(bottom_left + curve_width);
+ if self.corner_radius > 0. {
+ path.curve_to(bottom_left - curve_height, bottom_left);
+ }
+ }
+ }
+
+ if first_line.start_x > last_line.start_x {
+ let curve_width = curve_width(last_line.start_x, first_line.start_x);
+ let second_top_left = vec2f(last_line.start_x, start_y + self.line_height);
+ path.line_to(second_top_left + curve_height);
+ if self.corner_radius > 0. {
+ path.curve_to(second_top_left + curve_width, second_top_left);
+ }
+ let first_bottom_left = vec2f(first_line.start_x, second_top_left.y());
+ path.line_to(first_bottom_left - curve_width);
+ if self.corner_radius > 0. {
+ path.curve_to(first_bottom_left - curve_height, first_bottom_left);
+ }
+ }
+
+ path.line_to(first_top_left + curve_height);
+ if self.corner_radius > 0. {
+ path.curve_to(first_top_left + top_curve_width, first_top_left);
+ }
+ path.line_to(first_top_right - top_curve_width);
+
+ cx.scene().push_path(path.build(self.color, Some(bounds)));
+ }
+}
+
+fn range_to_bounds(
+ range: &Range<DisplayPoint>,
+ content_origin: Vector2F,
+ scroll_left: f32,
+ scroll_top: f32,
+ visible_row_range: &Range<u32>,
+ line_end_overshoot: f32,
+ position_map: &PositionMap,
+) -> impl Iterator<Item = RectF> {
+ let mut bounds: SmallVec<[RectF; 1]> = SmallVec::new();
+
+ if range.start == range.end {
+ return bounds.into_iter();
+ }
+
+ let start_row = visible_row_range.start;
+ let end_row = visible_row_range.end;
+
+ let row_range = if range.end.column() == 0 {
+ cmp::max(range.start.row(), start_row)..cmp::min(range.end.row(), end_row)
+ } else {
+ cmp::max(range.start.row(), start_row)..cmp::min(range.end.row() + 1, end_row)
+ };
+
+ let first_y =
+ content_origin.y() + row_range.start as f32 * position_map.line_height - scroll_top;
+
+ for (idx, row) in row_range.enumerate() {
+ let line_layout = &position_map.line_layouts[(row - start_row) as usize].line;
+
+ let start_x = if row == range.start.row() {
+ content_origin.x() + line_layout.x_for_index(range.start.column() as usize)
+ - scroll_left
+ } else {
+ content_origin.x() - scroll_left
+ };
+
+ let end_x = if row == range.end.row() {
+ content_origin.x() + line_layout.x_for_index(range.end.column() as usize) - scroll_left
+ } else {
+ content_origin.x() + line_layout.width() + line_end_overshoot - scroll_left
+ };
+
+ bounds.push(RectF::from_points(
+ vec2f(start_x, first_y + position_map.line_height * idx as f32),
+ vec2f(end_x, first_y + position_map.line_height * (idx + 1) as f32),
+ ))
+ }
+
+ bounds.into_iter()
+}
+
+pub fn scale_vertical_mouse_autoscroll_delta(delta: f32) -> f32 {
+ delta.powf(1.5) / 100.0
+}
+
+fn scale_horizontal_mouse_autoscroll_delta(delta: f32) -> f32 {
+ delta.powf(1.2) / 300.0
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::{BlockDisposition, BlockProperties},
+ editor_tests::{init_test, update_test_language_settings},
+ Editor, MultiBuffer,
+ };
+ use gpui::TestAppContext;
+ use language::language_settings;
+ use log::info;
+ use std::{num::NonZeroU32, sync::Arc};
+ use util::test::sample_text;
+
+ #[gpui::test]
+ fn test_layout_line_numbers(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx);
+ Editor::new(EditorMode::Full, buffer, None, None, cx)
+ })
+ .root(cx);
+ let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+
+ let layouts = editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ element
+ .layout_line_numbers(
+ 0..6,
+ &Default::default(),
+ DisplayPoint::new(0, 0),
+ false,
+ &snapshot,
+ cx,
+ )
+ .0
+ });
+ assert_eq!(layouts.len(), 6);
+
+ let relative_rows = editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3))
+ });
+ assert_eq!(relative_rows[&0], 3);
+ assert_eq!(relative_rows[&1], 2);
+ assert_eq!(relative_rows[&2], 1);
+ // current line has no relative number
+ assert_eq!(relative_rows[&4], 1);
+ assert_eq!(relative_rows[&5], 2);
+
+ // works if cursor is before screen
+ let relative_rows = editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+
+ element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1))
+ });
+ assert_eq!(relative_rows.len(), 3);
+ assert_eq!(relative_rows[&3], 2);
+ assert_eq!(relative_rows[&4], 3);
+ assert_eq!(relative_rows[&5], 4);
+
+ // works if cursor is after screen
+ let relative_rows = editor.update(cx, |editor, cx| {
+ let snapshot = editor.snapshot(cx);
+
+ element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6))
+ });
+ assert_eq!(relative_rows.len(), 3);
+ assert_eq!(relative_rows[&0], 5);
+ assert_eq!(relative_rows[&1], 4);
+ assert_eq!(relative_rows[&2], 3);
+ }
+
+ #[gpui::test]
+ async fn test_vim_visual_selections(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&(sample_text(6, 6, 'a') + "\n"), cx);
+ Editor::new(EditorMode::Full, buffer, None, None, cx)
+ })
+ .root(cx);
+ let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+ let (_, state) = editor.update(cx, |editor, cx| {
+ editor.cursor_shape = CursorShape::Block;
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([
+ Point::new(0, 0)..Point::new(1, 0),
+ Point::new(3, 2)..Point::new(3, 3),
+ Point::new(5, 6)..Point::new(6, 0),
+ ]);
+ });
+ element.layout(
+ SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+ editor,
+ cx,
+ )
+ });
+ assert_eq!(state.selections.len(), 1);
+ let local_selections = &state.selections[0].1;
+ assert_eq!(local_selections.len(), 3);
+ // moves cursor back one line
+ assert_eq!(local_selections[0].head, DisplayPoint::new(0, 6));
+ assert_eq!(
+ local_selections[0].range,
+ DisplayPoint::new(0, 0)..DisplayPoint::new(1, 0)
+ );
+
+ // moves cursor back one column
+ assert_eq!(
+ local_selections[1].range,
+ DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3)
+ );
+ assert_eq!(local_selections[1].head, DisplayPoint::new(3, 2));
+
+ // leaves cursor on the max point
+ assert_eq!(
+ local_selections[2].range,
+ DisplayPoint::new(5, 6)..DisplayPoint::new(6, 0)
+ );
+ assert_eq!(local_selections[2].head, DisplayPoint::new(6, 0));
+
+ // active lines does not include 1 (even though the range of the selection does)
+ assert_eq!(
+ state.active_rows.keys().cloned().collect::<Vec<u32>>(),
+ vec![0, 3, 5, 6]
+ );
+
+ // multi-buffer support
+ // in DisplayPoint co-ordinates, this is what we're dealing with:
+ // 0: [[file
+ // 1: header]]
+ // 2: aaaaaa
+ // 3: bbbbbb
+ // 4: cccccc
+ // 5:
+ // 6: ...
+ // 7: ffffff
+ // 8: gggggg
+ // 9: hhhhhh
+ // 10:
+ // 11: [[file
+ // 12: header]]
+ // 13: bbbbbb
+ // 14: cccccc
+ // 15: dddddd
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_multi(
+ [
+ (
+ &(sample_text(8, 6, 'a') + "\n"),
+ vec![
+ Point::new(0, 0)..Point::new(3, 0),
+ Point::new(4, 0)..Point::new(7, 0),
+ ],
+ ),
+ (
+ &(sample_text(8, 6, 'a') + "\n"),
+ vec![Point::new(1, 0)..Point::new(3, 0)],
+ ),
+ ],
+ cx,
+ );
+ Editor::new(EditorMode::Full, buffer, None, None, cx)
+ })
+ .root(cx);
+ let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+ let (_, state) = editor.update(cx, |editor, cx| {
+ editor.cursor_shape = CursorShape::Block;
+ editor.change_selections(None, cx, |s| {
+ s.select_display_ranges([
+ DisplayPoint::new(4, 0)..DisplayPoint::new(7, 0),
+ DisplayPoint::new(10, 0)..DisplayPoint::new(13, 0),
+ ]);
+ });
+ element.layout(
+ SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+ editor,
+ cx,
+ )
+ });
+
+ assert_eq!(state.selections.len(), 1);
+ let local_selections = &state.selections[0].1;
+ assert_eq!(local_selections.len(), 2);
+
+ // moves cursor on excerpt boundary back a line
+ // and doesn't allow selection to bleed through
+ assert_eq!(
+ local_selections[0].range,
+ DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0)
+ );
+ assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0));
+
+ // moves cursor on buffer boundary back two lines
+ // and doesn't allow selection to bleed through
+ assert_eq!(
+ local_selections[1].range,
+ DisplayPoint::new(10, 0)..DisplayPoint::new(11, 0)
+ );
+ assert_eq!(local_selections[1].head, DisplayPoint::new(10, 0));
+ }
+
+ #[gpui::test]
+ fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) {
+ init_test(cx, |_| {});
+
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple("", cx);
+ Editor::new(EditorMode::Full, buffer, None, None, cx)
+ })
+ .root(cx);
+
+ editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text("hello", cx);
+ editor.insert_blocks(
+ [BlockProperties {
+ style: BlockStyle::Fixed,
+ disposition: BlockDisposition::Above,
+ height: 3,
+ position: Anchor::min(),
+ render: Arc::new(|_| Empty::new().into_any()),
+ }],
+ None,
+ cx,
+ );
+
+ // Blur the editor so that it displays placeholder text.
+ cx.blur();
+ });
+
+ let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+ let (size, mut state) = editor.update(cx, |editor, cx| {
+ element.layout(
+ SizeConstraint::new(vec2f(500., 500.), vec2f(500., 500.)),
+ editor,
+ cx,
+ )
+ });
+
+ assert_eq!(state.position_map.line_layouts.len(), 4);
+ assert_eq!(
+ state
+ .line_number_layouts
+ .iter()
+ .map(Option::is_some)
+ .collect::<Vec<_>>(),
+ &[false, false, false, true]
+ );
+
+ // Don't panic.
+ let bounds = RectF::new(Default::default(), size);
+ editor.update(cx, |editor, cx| {
+ element.paint(bounds, bounds, &mut state, editor, cx);
+ });
+ }
+
+ #[gpui::test]
+ fn test_all_invisibles_drawing(cx: &mut TestAppContext) {
+ const TAB_SIZE: u32 = 4;
+
+ let input_text = "\t \t|\t| a b";
+ let expected_invisibles = vec![
+ Invisible::Tab {
+ line_start_offset: 0,
+ },
+ Invisible::Whitespace {
+ line_offset: TAB_SIZE as usize,
+ },
+ Invisible::Tab {
+ line_start_offset: TAB_SIZE as usize + 1,
+ },
+ Invisible::Tab {
+ line_start_offset: TAB_SIZE as usize * 2 + 1,
+ },
+ Invisible::Whitespace {
+ line_offset: TAB_SIZE as usize * 3 + 1,
+ },
+ Invisible::Whitespace {
+ line_offset: TAB_SIZE as usize * 3 + 3,
+ },
+ ];
+ assert_eq!(
+ expected_invisibles.len(),
+ input_text
+ .chars()
+ .filter(|initial_char| initial_char.is_whitespace())
+ .count(),
+ "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
+ );
+
+ init_test(cx, |s| {
+ s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+ s.defaults.tab_size = NonZeroU32::new(TAB_SIZE);
+ });
+
+ let actual_invisibles =
+ collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, 500.0);
+
+ assert_eq!(expected_invisibles, actual_invisibles);
+ }
+
+ #[gpui::test]
+ fn test_invisibles_dont_appear_in_certain_editors(cx: &mut TestAppContext) {
+ init_test(cx, |s| {
+ s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+ s.defaults.tab_size = NonZeroU32::new(4);
+ });
+
+ for editor_mode_without_invisibles in [
+ EditorMode::SingleLine,
+ EditorMode::AutoHeight { max_lines: 100 },
+ ] {
+ let invisibles = collect_invisibles_from_new_editor(
+ cx,
+ editor_mode_without_invisibles,
+ "\t\t\t| | a b",
+ 500.0,
+ );
+ assert!(invisibles.is_empty(),
+ "For editor mode {editor_mode_without_invisibles:?} no invisibles was expected but got {invisibles:?}");
+ }
+ }
+
+ #[gpui::test]
+ fn test_wrapped_invisibles_drawing(cx: &mut TestAppContext) {
+ let tab_size = 4;
+ let input_text = "a\tbcd ".repeat(9);
+ let repeated_invisibles = [
+ Invisible::Tab {
+ line_start_offset: 1,
+ },
+ Invisible::Whitespace {
+ line_offset: tab_size as usize + 3,
+ },
+ Invisible::Whitespace {
+ line_offset: tab_size as usize + 4,
+ },
+ Invisible::Whitespace {
+ line_offset: tab_size as usize + 5,
+ },
+ ];
+ let expected_invisibles = std::iter::once(repeated_invisibles)
+ .cycle()
+ .take(9)
+ .flatten()
+ .collect::<Vec<_>>();
+ assert_eq!(
+ expected_invisibles.len(),
+ input_text
+ .chars()
+ .filter(|initial_char| initial_char.is_whitespace())
+ .count(),
+ "Hardcoded expected invisibles differ from the actual ones in '{input_text}'"
+ );
+ info!("Expected invisibles: {expected_invisibles:?}");
+
+ init_test(cx, |_| {});
+
+ // Put the same string with repeating whitespace pattern into editors of various size,
+ // take deliberately small steps during resizing, to put all whitespace kinds near the wrap point.
+ let resize_step = 10.0;
+ let mut editor_width = 200.0;
+ while editor_width <= 1000.0 {
+ update_test_language_settings(cx, |s| {
+ s.defaults.tab_size = NonZeroU32::new(tab_size);
+ s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All);
+ s.defaults.preferred_line_length = Some(editor_width as u32);
+ s.defaults.soft_wrap = Some(language_settings::SoftWrap::PreferredLineLength);
+ });
+
+ let actual_invisibles =
+ collect_invisibles_from_new_editor(cx, EditorMode::Full, &input_text, editor_width);
+
+ // Whatever the editor size is, ensure it has the same invisible kinds in the same order
+ // (no good guarantees about the offsets: wrapping could trigger padding and its tests should check the offsets).
+ let mut i = 0;
+ for (actual_index, actual_invisible) in actual_invisibles.iter().enumerate() {
+ i = actual_index;
+ match expected_invisibles.get(i) {
+ Some(expected_invisible) => match (expected_invisible, actual_invisible) {
+ (Invisible::Whitespace { .. }, Invisible::Whitespace { .. })
+ | (Invisible::Tab { .. }, Invisible::Tab { .. }) => {}
+ _ => {
+ panic!("At index {i}, expected invisible {expected_invisible:?} does not match actual {actual_invisible:?} by kind. Actual invisibles: {actual_invisibles:?}")
+ }
+ },
+ None => panic!("Unexpected extra invisible {actual_invisible:?} at index {i}"),
+ }
+ }
+ let missing_expected_invisibles = &expected_invisibles[i + 1..];
+ assert!(
+ missing_expected_invisibles.is_empty(),
+ "Missing expected invisibles after index {i}: {missing_expected_invisibles:?}"
+ );
+
+ editor_width += resize_step;
+ }
+ }
+
+ fn collect_invisibles_from_new_editor(
+ cx: &mut TestAppContext,
+ editor_mode: EditorMode,
+ input_text: &str,
+ editor_width: f32,
+ ) -> Vec<Invisible> {
+ info!(
+ "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'"
+ );
+ let editor = cx
+ .add_window(|cx| {
+ let buffer = MultiBuffer::build_simple(&input_text, cx);
+ Editor::new(editor_mode, buffer, None, None, cx)
+ })
+ .root(cx);
+
+ let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx)));
+ let (_, layout_state) = editor.update(cx, |editor, cx| {
+ editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
+ editor.set_wrap_width(Some(editor_width), cx);
+
+ element.layout(
+ SizeConstraint::new(vec2f(editor_width, 500.), vec2f(editor_width, 500.)),
+ editor,
+ cx,
+ )
+ });
+
+ layout_state
+ .position_map
+ .line_layouts
+ .iter()
+ .map(|line_with_invisibles| &line_with_invisibles.invisibles)
+ .flatten()
+ .cloned()
+ .collect()
+ }
+}
@@ -0,0 +1,282 @@
+use std::ops::Range;
+
+use git::diff::{DiffHunk, DiffHunkStatus};
+use language::Point;
+
+use crate::{
+ display_map::{DisplaySnapshot, ToDisplayPoint},
+ AnchorRangeExt,
+};
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum DisplayDiffHunk {
+ Folded {
+ display_row: u32,
+ },
+
+ Unfolded {
+ display_row_range: Range<u32>,
+ status: DiffHunkStatus,
+ },
+}
+
+impl DisplayDiffHunk {
+ pub fn start_display_row(&self) -> u32 {
+ match self {
+ &DisplayDiffHunk::Folded { display_row } => display_row,
+ DisplayDiffHunk::Unfolded {
+ display_row_range, ..
+ } => display_row_range.start,
+ }
+ }
+
+ pub fn contains_display_row(&self, display_row: u32) -> bool {
+ let range = match self {
+ &DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
+
+ DisplayDiffHunk::Unfolded {
+ display_row_range, ..
+ } => display_row_range.start..=display_row_range.end,
+ };
+
+ range.contains(&display_row)
+ }
+}
+
+pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
+ let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
+ let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
+ let hunk_end_point_sub = Point::new(
+ hunk.buffer_range
+ .end
+ .saturating_sub(1)
+ .max(hunk.buffer_range.start),
+ 0,
+ );
+
+ let is_removal = hunk.status() == DiffHunkStatus::Removed;
+
+ let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0);
+ let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
+ let folds_range = folds_start..folds_end;
+
+ let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
+ let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
+ let fold_point_range = fold_point_range.start..=fold_point_range.end;
+
+ let folded_start = fold_point_range.contains(&hunk_start_point);
+ let folded_end = fold_point_range.contains(&hunk_end_point_sub);
+ let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
+
+ (folded_start && folded_end) || (is_removal && folded_start_sub)
+ });
+
+ if let Some(fold) = containing_fold {
+ let row = fold.start.to_display_point(snapshot).row();
+ DisplayDiffHunk::Folded { display_row: row }
+ } else {
+ let start = hunk_start_point.to_display_point(snapshot).row();
+
+ let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
+ let hunk_end_point = Point::new(hunk_end_row, 0);
+ let end = hunk_end_point.to_display_point(snapshot).row();
+
+ DisplayDiffHunk::Unfolded {
+ display_row_range: start..end,
+ status: hunk.status(),
+ }
+ }
+}
+
+#[cfg(any(test, feature = "test_support"))]
+mod tests {
+ use crate::editor_tests::init_test;
+ use crate::Point;
+ use gpui::TestAppContext;
+ use multi_buffer::{ExcerptRange, MultiBuffer};
+ use project::{FakeFs, Project};
+ use unindent::Unindent;
+ #[gpui::test]
+ async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
+ use git::diff::DiffHunkStatus;
+ init_test(cx, |_| {});
+
+ let fs = FakeFs::new(cx.background());
+ let project = Project::test(fs, [], cx).await;
+
+ // buffer has two modified hunks with two rows each
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.create_buffer(
+ "
+ 1.zero
+ 1.ONE
+ 1.TWO
+ 1.three
+ 1.FOUR
+ 1.FIVE
+ 1.six
+ "
+ .unindent()
+ .as_str(),
+ None,
+ cx,
+ )
+ })
+ .unwrap();
+ buffer_1.update(cx, |buffer, cx| {
+ buffer.set_diff_base(
+ Some(
+ "
+ 1.zero
+ 1.one
+ 1.two
+ 1.three
+ 1.four
+ 1.five
+ 1.six
+ "
+ .unindent(),
+ ),
+ cx,
+ );
+ });
+
+ // buffer has a deletion hunk and an insertion hunk
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.create_buffer(
+ "
+ 2.zero
+ 2.one
+ 2.two
+ 2.three
+ 2.four
+ 2.five
+ 2.six
+ "
+ .unindent()
+ .as_str(),
+ None,
+ cx,
+ )
+ })
+ .unwrap();
+ buffer_2.update(cx, |buffer, cx| {
+ buffer.set_diff_base(
+ Some(
+ "
+ 2.zero
+ 2.one
+ 2.one-and-a-half
+ 2.two
+ 2.three
+ 2.four
+ 2.six
+ "
+ .unindent(),
+ ),
+ cx,
+ );
+ });
+
+ cx.foreground().run_until_parked();
+
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ // excerpt ends in the middle of a modified hunk
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 5),
+ primary: Default::default(),
+ },
+ // excerpt begins in the middle of a modified hunk
+ ExcerptRange {
+ context: Point::new(5, 0)..Point::new(6, 5),
+ primary: Default::default(),
+ },
+ ],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [
+ // excerpt ends at a deletion
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 5),
+ primary: Default::default(),
+ },
+ // excerpt starts at a deletion
+ ExcerptRange {
+ context: Point::new(2, 0)..Point::new(2, 5),
+ primary: Default::default(),
+ },
+ // excerpt fully contains a deletion hunk
+ ExcerptRange {
+ context: Point::new(1, 0)..Point::new(2, 5),
+ primary: Default::default(),
+ },
+ // excerpt fully contains an insertion hunk
+ ExcerptRange {
+ context: Point::new(4, 0)..Point::new(6, 5),
+ primary: Default::default(),
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
+
+ let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
+
+ assert_eq!(
+ snapshot.text(),
+ "
+ 1.zero
+ 1.ONE
+ 1.FIVE
+ 1.six
+ 2.zero
+ 2.one
+ 2.two
+ 2.one
+ 2.two
+ 2.four
+ 2.five
+ 2.six"
+ .unindent()
+ );
+
+ let expected = [
+ (DiffHunkStatus::Modified, 1..2),
+ (DiffHunkStatus::Modified, 2..3),
+ //TODO: Define better when and where removed hunks show up at range extremities
+ (DiffHunkStatus::Removed, 6..6),
+ (DiffHunkStatus::Removed, 8..8),
+ (DiffHunkStatus::Added, 10..11),
+ ];
+
+ assert_eq!(
+ snapshot
+ .git_diff_hunks_in_range(0..12)
+ .map(|hunk| (hunk.status(), hunk.buffer_range))
+ .collect::<Vec<_>>(),
+ &expected,
+ );
+
+ assert_eq!(
+ snapshot
+ .git_diff_hunks_in_range_rev(0..12)
+ .map(|hunk| (hunk.status(), hunk.buffer_range))
+ .collect::<Vec<_>>(),
+ expected
+ .iter()
+ .rev()
+ .cloned()
+ .collect::<Vec<_>>()
+ .as_slice(),
+ );
+ }
+}
@@ -0,0 +1,138 @@
+use gpui::ViewContext;
+
+use crate::{Editor, RangeToAnchorExt};
+
+enum MatchingBracketHighlight {}
+
+pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+ editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
+
+ let newest_selection = editor.selections.newest::<usize>(cx);
+ // Don't highlight brackets if the selection isn't empty
+ if !newest_selection.is_empty() {
+ return;
+ }
+
+ let head = newest_selection.head();
+ let snapshot = editor.snapshot(cx);
+ if let Some((opening_range, closing_range)) = snapshot
+ .buffer_snapshot
+ .innermost_enclosing_bracket_ranges(head..head)
+ {
+ editor.highlight_background::<MatchingBracketHighlight>(
+ vec![
+ opening_range.to_anchors(&snapshot.buffer_snapshot),
+ closing_range.to_anchors(&snapshot.buffer_snapshot),
+ ],
+ |theme| theme.editor.document_highlight_read_background,
+ cx,
+ )
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+ use indoc::indoc;
+ use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
+
+ #[gpui::test]
+ async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new(
+ Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ brackets: BracketPairConfig {
+ pairs: vec![
+ BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: false,
+ newline: true,
+ },
+ BracketPair {
+ start: "(".to_string(),
+ end: ")".to_string(),
+ close: false,
+ newline: true,
+ },
+ ],
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_brackets_query(indoc! {r#"
+ ("{" @open "}" @close)
+ ("(" @open ")" @close)
+ "#})
+ .unwrap(),
+ Default::default(),
+ cx,
+ )
+ .await;
+
+ // positioning cursor inside bracket highlights both
+ cx.set_state(indoc! {r#"
+ pub fn test("Test หargument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn testยซ(ยป"Test argument"ยซ)ยป {
+ another_test(1, 2, 3);
+ }
+ "#});
+
+ cx.set_state(indoc! {r#"
+ pub fn test("Test argument") {
+ another_test(1, ห2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") {
+ another_testยซ(ยป1, 2, 3ยซ)ยป;
+ }
+ "#});
+
+ cx.set_state(indoc! {r#"
+ pub fn test("Test argument") {
+ anotherห_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") ยซ{ยป
+ another_test(1, 2, 3);
+ ยซ}ยป
+ "#});
+
+ // positioning outside of brackets removes highlight
+ cx.set_state(indoc! {r#"
+ pub fหn test("Test argument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+
+ // non empty selection dismisses highlight
+ cx.set_state(indoc! {r#"
+ pub fn test("Teยซst argหยปument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
+ pub fn test("Test argument") {
+ another_test(1, 2, 3);
+ }
+ "#});
+ }
+}
@@ -0,0 +1,1329 @@
+use crate::{
+ display_map::{InlayOffset, ToDisplayPoint},
+ link_go_to_definition::{InlayHighlight, RangeInEditor},
+ Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
+ ExcerptId, RangeToAnchorExt,
+};
+use futures::FutureExt;
+use gpui::{
+ actions,
+ elements::{Flex, MouseEventHandler, Padding, ParentElement, Text},
+ platform::{CursorStyle, MouseButton},
+ AnyElement, AppContext, Element, ModelHandle, Task, ViewContext, WeakViewHandle,
+};
+use language::{
+ markdown, Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry, ParsedMarkdown,
+};
+use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
+use std::{ops::Range, sync::Arc, time::Duration};
+use util::TryFutureExt;
+use workspace::Workspace;
+
+pub const HOVER_DELAY_MILLIS: u64 = 350;
+pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
+
+pub const MIN_POPOVER_CHARACTER_WIDTH: f32 = 20.;
+pub const MIN_POPOVER_LINE_HEIGHT: f32 = 4.;
+pub const HOVER_POPOVER_GAP: f32 = 10.;
+
+actions!(editor, [Hover]);
+
+pub fn init(cx: &mut AppContext) {
+ cx.add_action(hover);
+}
+
+/// Bindable action which uses the most recent selection head to trigger a hover
+pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
+ let head = editor.selections.newest_display(cx).head();
+ show_hover(editor, head, true, cx);
+}
+
+/// The internal hover action dispatches between `show_hover` or `hide_hover`
+/// depending on whether a point to hover over is provided.
+pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewContext<Editor>) {
+ if settings::get::<EditorSettings>(cx).hover_popover_enabled {
+ if let Some(point) = point {
+ show_hover(editor, point, false, cx);
+ } else {
+ hide_hover(editor, cx);
+ }
+ }
+}
+
+pub struct InlayHover {
+ pub excerpt: ExcerptId,
+ pub range: InlayHighlight,
+ pub tooltip: HoverBlock,
+}
+
+pub fn find_hovered_hint_part(
+ label_parts: Vec<InlayHintLabelPart>,
+ hint_start: InlayOffset,
+ hovered_offset: InlayOffset,
+) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
+ if hovered_offset >= hint_start {
+ let mut hovered_character = (hovered_offset - hint_start).0;
+ let mut part_start = hint_start;
+ for part in label_parts {
+ let part_len = part.value.chars().count();
+ if hovered_character > part_len {
+ hovered_character -= part_len;
+ part_start.0 += part_len;
+ } else {
+ let part_end = InlayOffset(part_start.0 + part_len);
+ return Some((part, part_start..part_end));
+ }
+ }
+ }
+ None
+}
+
+pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext<Editor>) {
+ if settings::get::<EditorSettings>(cx).hover_popover_enabled {
+ if editor.pending_rename.is_some() {
+ return;
+ }
+
+ let Some(project) = editor.project.clone() else {
+ return;
+ };
+
+ if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+ if let RangeInEditor::Inlay(range) = symbol_range {
+ if range == &inlay_hover.range {
+ // Hover triggered from same location as last time. Don't show again.
+ return;
+ }
+ }
+ hide_hover(editor, cx);
+ }
+
+ let task = cx.spawn(|this, mut cx| {
+ async move {
+ cx.background()
+ .timer(Duration::from_millis(HOVER_DELAY_MILLIS))
+ .await;
+ this.update(&mut cx, |this, _| {
+ this.hover_state.diagnostic_popover = None;
+ })?;
+
+ let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
+ let blocks = vec![inlay_hover.tooltip];
+ let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
+
+ let hover_popover = InfoPopover {
+ project: project.clone(),
+ symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
+ blocks,
+ parsed_content,
+ };
+
+ this.update(&mut cx, |this, cx| {
+ // Highlight the selected symbol using a background highlight
+ this.highlight_inlay_background::<HoverState>(
+ vec![inlay_hover.range],
+ |theme| theme.editor.hover_popover.highlight,
+ cx,
+ );
+ this.hover_state.info_popover = Some(hover_popover);
+ cx.notify();
+ })?;
+
+ anyhow::Ok(())
+ }
+ .log_err()
+ });
+
+ editor.hover_state.info_task = Some(task);
+ }
+}
+
+/// Hides the type information popup.
+/// Triggered by the `Hover` action when the cursor is not over a symbol or when the
+/// selections changed.
+pub fn hide_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
+ let did_hide = editor.hover_state.info_popover.take().is_some()
+ | editor.hover_state.diagnostic_popover.take().is_some();
+
+ editor.hover_state.info_task = None;
+ editor.hover_state.triggered_from = None;
+
+ editor.clear_background_highlights::<HoverState>(cx);
+
+ if did_hide {
+ cx.notify();
+ }
+
+ did_hide
+}
+
+/// Queries the LSP and shows type info and documentation
+/// about the symbol the mouse is currently hovering over.
+/// Triggered by the `Hover` action when the cursor may be over a symbol.
+fn show_hover(
+ editor: &mut Editor,
+ point: DisplayPoint,
+ ignore_timeout: bool,
+ cx: &mut ViewContext<Editor>,
+) {
+ if editor.pending_rename.is_some() {
+ return;
+ }
+
+ let snapshot = editor.snapshot(cx);
+ let multibuffer_offset = point.to_offset(&snapshot.display_snapshot, Bias::Left);
+
+ let (buffer, buffer_position) = if let Some(output) = editor
+ .buffer
+ .read(cx)
+ .text_anchor_for_position(multibuffer_offset, cx)
+ {
+ output
+ } else {
+ return;
+ };
+
+ let excerpt_id = if let Some((excerpt_id, _, _)) = editor
+ .buffer()
+ .read(cx)
+ .excerpt_containing(multibuffer_offset, cx)
+ {
+ excerpt_id
+ } else {
+ return;
+ };
+
+ let project = if let Some(project) = editor.project.clone() {
+ project
+ } else {
+ return;
+ };
+
+ if !ignore_timeout {
+ if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
+ if symbol_range
+ .as_text_range()
+ .map(|range| {
+ range
+ .to_offset(&snapshot.buffer_snapshot)
+ .contains(&multibuffer_offset)
+ })
+ .unwrap_or(false)
+ {
+ // Hover triggered from same location as last time. Don't show again.
+ return;
+ } else {
+ hide_hover(editor, cx);
+ }
+ }
+ }
+
+ // Get input anchor
+ let anchor = snapshot
+ .buffer_snapshot
+ .anchor_at(multibuffer_offset, Bias::Left);
+
+ // Don't request again if the location is the same as the previous request
+ if let Some(triggered_from) = &editor.hover_state.triggered_from {
+ if triggered_from
+ .cmp(&anchor, &snapshot.buffer_snapshot)
+ .is_eq()
+ {
+ return;
+ }
+ }
+
+ let task = cx.spawn(|this, mut cx| {
+ async move {
+ // If we need to delay, delay a set amount initially before making the lsp request
+ let delay = if !ignore_timeout {
+ // Construct delay task to wait for later
+ let total_delay = Some(
+ cx.background()
+ .timer(Duration::from_millis(HOVER_DELAY_MILLIS)),
+ );
+
+ cx.background()
+ .timer(Duration::from_millis(HOVER_REQUEST_DELAY_MILLIS))
+ .await;
+ total_delay
+ } else {
+ None
+ };
+
+ // query the LSP for hover info
+ let hover_request = cx.update(|cx| {
+ project.update(cx, |project, cx| {
+ project.hover(&buffer, buffer_position, cx)
+ })
+ });
+
+ if let Some(delay) = delay {
+ delay.await;
+ }
+
+ // If there's a diagnostic, assign it on the hover state and notify
+ let local_diagnostic = snapshot
+ .buffer_snapshot
+ .diagnostics_in_range::<_, usize>(multibuffer_offset..multibuffer_offset, false)
+ // Find the entry with the most specific range
+ .min_by_key(|entry| entry.range.end - entry.range.start)
+ .map(|entry| DiagnosticEntry {
+ diagnostic: entry.diagnostic,
+ range: entry.range.to_anchors(&snapshot.buffer_snapshot),
+ });
+
+ // Pull the primary diagnostic out so we can jump to it if the popover is clicked
+ let primary_diagnostic = local_diagnostic.as_ref().and_then(|local_diagnostic| {
+ snapshot
+ .buffer_snapshot
+ .diagnostic_group::<usize>(local_diagnostic.diagnostic.group_id)
+ .find(|diagnostic| diagnostic.diagnostic.is_primary)
+ .map(|entry| DiagnosticEntry {
+ diagnostic: entry.diagnostic,
+ range: entry.range.to_anchors(&snapshot.buffer_snapshot),
+ })
+ });
+
+ this.update(&mut cx, |this, _| {
+ this.hover_state.diagnostic_popover =
+ local_diagnostic.map(|local_diagnostic| DiagnosticPopover {
+ local_diagnostic,
+ primary_diagnostic,
+ });
+ })?;
+
+ let hover_result = hover_request.await.ok().flatten();
+ let hover_popover = match hover_result {
+ Some(hover_result) if !hover_result.is_empty() => {
+ // Create symbol range of anchors for highlighting and filtering of future requests.
+ let range = if let Some(range) = hover_result.range {
+ let start = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(excerpt_id.clone(), range.start);
+ let end = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(excerpt_id.clone(), range.end);
+
+ start..end
+ } else {
+ anchor..anchor
+ };
+
+ let language_registry = project.update(&mut cx, |p, _| p.languages().clone());
+ let blocks = hover_result.contents;
+ let language = hover_result.language;
+ let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
+
+ Some(InfoPopover {
+ project: project.clone(),
+ symbol_range: RangeInEditor::Text(range),
+ blocks,
+ parsed_content,
+ })
+ }
+
+ _ => None,
+ };
+
+ this.update(&mut cx, |this, cx| {
+ if let Some(symbol_range) = hover_popover
+ .as_ref()
+ .and_then(|hover_popover| hover_popover.symbol_range.as_text_range())
+ {
+ // Highlight the selected symbol using a background highlight
+ this.highlight_background::<HoverState>(
+ vec![symbol_range],
+ |theme| theme.editor.hover_popover.highlight,
+ cx,
+ );
+ } else {
+ this.clear_background_highlights::<HoverState>(cx);
+ }
+
+ this.hover_state.info_popover = hover_popover;
+ cx.notify();
+ })?;
+
+ Ok::<_, anyhow::Error>(())
+ }
+ .log_err()
+ });
+
+ editor.hover_state.info_task = Some(task);
+}
+
+async fn parse_blocks(
+ blocks: &[HoverBlock],
+ language_registry: &Arc<LanguageRegistry>,
+ language: Option<Arc<Language>>,
+) -> markdown::ParsedMarkdown {
+ let mut text = String::new();
+ let mut highlights = Vec::new();
+ let mut region_ranges = Vec::new();
+ let mut regions = Vec::new();
+
+ for block in blocks {
+ match &block.kind {
+ HoverBlockKind::PlainText => {
+ markdown::new_paragraph(&mut text, &mut Vec::new());
+ text.push_str(&block.text);
+ }
+
+ HoverBlockKind::Markdown => {
+ markdown::parse_markdown_block(
+ &block.text,
+ language_registry,
+ language.clone(),
+ &mut text,
+ &mut highlights,
+ &mut region_ranges,
+ &mut regions,
+ )
+ .await
+ }
+
+ HoverBlockKind::Code { language } => {
+ if let Some(language) = language_registry
+ .language_for_name(language)
+ .now_or_never()
+ .and_then(Result::ok)
+ {
+ markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
+ } else {
+ text.push_str(&block.text);
+ }
+ }
+ }
+ }
+
+ ParsedMarkdown {
+ text: text.trim().to_string(),
+ highlights,
+ region_ranges,
+ regions,
+ }
+}
+
+#[derive(Default)]
+pub struct HoverState {
+ pub info_popover: Option<InfoPopover>,
+ pub diagnostic_popover: Option<DiagnosticPopover>,
+ pub triggered_from: Option<Anchor>,
+ pub info_task: Option<Task<Option<()>>>,
+}
+
+impl HoverState {
+ pub fn visible(&self) -> bool {
+ self.info_popover.is_some() || self.diagnostic_popover.is_some()
+ }
+
+ pub fn render(
+ &mut self,
+ snapshot: &EditorSnapshot,
+ style: &EditorStyle,
+ visible_rows: Range<u32>,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Option<(DisplayPoint, Vec<AnyElement<Editor>>)> {
+ // If there is a diagnostic, position the popovers based on that.
+ // Otherwise use the start of the hover range
+ let anchor = self
+ .diagnostic_popover
+ .as_ref()
+ .map(|diagnostic_popover| &diagnostic_popover.local_diagnostic.range.start)
+ .or_else(|| {
+ self.info_popover
+ .as_ref()
+ .map(|info_popover| match &info_popover.symbol_range {
+ RangeInEditor::Text(range) => &range.start,
+ RangeInEditor::Inlay(range) => &range.inlay_position,
+ })
+ })?;
+ let point = anchor.to_display_point(&snapshot.display_snapshot);
+
+ // Don't render if the relevant point isn't on screen
+ if !self.visible() || !visible_rows.contains(&point.row()) {
+ return None;
+ }
+
+ let mut elements = Vec::new();
+
+ if let Some(diagnostic_popover) = self.diagnostic_popover.as_ref() {
+ elements.push(diagnostic_popover.render(style, cx));
+ }
+ if let Some(info_popover) = self.info_popover.as_mut() {
+ elements.push(info_popover.render(style, workspace, cx));
+ }
+
+ Some((point, elements))
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct InfoPopover {
+ pub project: ModelHandle<Project>,
+ symbol_range: RangeInEditor,
+ pub blocks: Vec<HoverBlock>,
+ parsed_content: ParsedMarkdown,
+}
+
+impl InfoPopover {
+ pub fn render(
+ &mut self,
+ style: &EditorStyle,
+ workspace: Option<WeakViewHandle<Workspace>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> AnyElement<Editor> {
+ MouseEventHandler::new::<InfoPopover, _>(0, cx, |_, cx| {
+ Flex::column()
+ .scrollable::<HoverBlock>(0, None, cx)
+ .with_child(crate::render_parsed_markdown::<HoverBlock>(
+ &self.parsed_content,
+ style,
+ workspace,
+ cx,
+ ))
+ .contained()
+ .with_style(style.hover_popover.container)
+ })
+ .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
+ .with_cursor_style(CursorStyle::Arrow)
+ .with_padding(Padding {
+ bottom: HOVER_POPOVER_GAP,
+ top: HOVER_POPOVER_GAP,
+ ..Default::default()
+ })
+ .into_any()
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct DiagnosticPopover {
+ local_diagnostic: DiagnosticEntry<Anchor>,
+ primary_diagnostic: Option<DiagnosticEntry<Anchor>>,
+}
+
+impl DiagnosticPopover {
+ pub fn render(&self, style: &EditorStyle, cx: &mut ViewContext<Editor>) -> AnyElement<Editor> {
+ enum PrimaryDiagnostic {}
+
+ let mut text_style = style.hover_popover.prose.clone();
+ text_style.font_size = style.text.font_size;
+ let diagnostic_source_style = style.hover_popover.diagnostic_source_highlight.clone();
+
+ let text = match &self.local_diagnostic.diagnostic.source {
+ Some(source) => Text::new(
+ format!("{source}: {}", self.local_diagnostic.diagnostic.message),
+ text_style,
+ )
+ .with_highlights(vec![(0..source.len(), diagnostic_source_style)]),
+
+ None => Text::new(self.local_diagnostic.diagnostic.message.clone(), text_style),
+ };
+
+ let container_style = match self.local_diagnostic.diagnostic.severity {
+ DiagnosticSeverity::HINT => style.hover_popover.info_container,
+ DiagnosticSeverity::INFORMATION => style.hover_popover.info_container,
+ DiagnosticSeverity::WARNING => style.hover_popover.warning_container,
+ DiagnosticSeverity::ERROR => style.hover_popover.error_container,
+ _ => style.hover_popover.container,
+ };
+
+ let tooltip_style = theme::current(cx).tooltip.clone();
+
+ MouseEventHandler::new::<DiagnosticPopover, _>(0, cx, |_, _| {
+ text.with_soft_wrap(true)
+ .contained()
+ .with_style(container_style)
+ })
+ .with_padding(Padding {
+ top: HOVER_POPOVER_GAP,
+ bottom: HOVER_POPOVER_GAP,
+ ..Default::default()
+ })
+ .on_move(|_, _, _| {}) // Consume move events so they don't reach regions underneath.
+ .on_click(MouseButton::Left, |_, this, cx| {
+ this.go_to_diagnostic(&Default::default(), cx)
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .with_tooltip::<PrimaryDiagnostic>(
+ 0,
+ "Go To Diagnostic".to_string(),
+ Some(Box::new(crate::GoToDiagnostic)),
+ tooltip_style,
+ cx,
+ )
+ .into_any()
+ }
+
+ pub fn activation_info(&self) -> (usize, Anchor) {
+ let entry = self
+ .primary_diagnostic
+ .as_ref()
+ .unwrap_or(&self.local_diagnostic);
+
+ (entry.diagnostic.group_id, entry.range.start.clone())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ editor_tests::init_test,
+ element::PointForPosition,
+ inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+ link_go_to_definition::update_inlay_link_and_hover_points,
+ test::editor_lsp_test_context::EditorLspTestContext,
+ InlayId,
+ };
+ use collections::BTreeSet;
+ use gpui::fonts::{HighlightStyle, Underline, Weight};
+ use indoc::indoc;
+ use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
+ use lsp::LanguageServerId;
+ use project::{HoverBlock, HoverBlockKind};
+ use smol::stream::StreamExt;
+ use unindent::Unindent;
+ use util::test::marked_text_ranges;
+
+ #[gpui::test]
+ async fn test_mouse_hover_info_popover(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Basic hover delays and then pops without moving the mouse
+ cx.set_state(indoc! {"
+ fn หtest() { println!(); }
+ "});
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { printหln!(); }
+ "});
+
+ cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+ assert!(!cx.editor(|editor, _| editor.hover_state.visible()));
+
+ // After delay, hover should be visible.
+ let symbol_range = cx.lsp_range(indoc! {"
+ fn test() { ยซprintln!ยป(); }
+ "});
+ let mut requests =
+ cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+ Ok(Some(lsp::Hover {
+ contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: "some basic docs".to_string(),
+ }),
+ range: Some(symbol_range),
+ }))
+ });
+ cx.foreground()
+ .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+ requests.next().await;
+
+ cx.editor(|editor, _| {
+ assert!(editor.hover_state.visible());
+ assert_eq!(
+ editor.hover_state.info_popover.clone().unwrap().blocks,
+ vec![HoverBlock {
+ text: "some basic docs".to_string(),
+ kind: HoverBlockKind::Markdown,
+ },]
+ )
+ });
+
+ // Mouse moved with no hover response dismisses
+ let hover_point = cx.display_point(indoc! {"
+ fn teหst() { println!(); }
+ "});
+ let mut request = cx
+ .lsp
+ .handle_request::<lsp::request::HoverRequest, _, _>(|_, _| async move { Ok(None) });
+ cx.update_editor(|editor, cx| hover_at(editor, Some(hover_point), cx));
+ cx.foreground()
+ .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+ request.next().await;
+ cx.editor(|editor, _| {
+ assert!(!editor.hover_state.visible());
+ });
+ }
+
+ #[gpui::test]
+ async fn test_keyboard_hover_info_popover(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Hover with keyboard has no delay
+ cx.set_state(indoc! {"
+ fหn test() { println!(); }
+ "});
+ cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+ let symbol_range = cx.lsp_range(indoc! {"
+ ยซfnยป test() { println!(); }
+ "});
+ cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+ Ok(Some(lsp::Hover {
+ contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: "some other basic docs".to_string(),
+ }),
+ range: Some(symbol_range),
+ }))
+ })
+ .next()
+ .await;
+
+ cx.condition(|editor, _| editor.hover_state.visible()).await;
+ cx.editor(|editor, _| {
+ assert_eq!(
+ editor.hover_state.info_popover.clone().unwrap().blocks,
+ vec![HoverBlock {
+ text: "some other basic docs".to_string(),
+ kind: HoverBlockKind::Markdown,
+ }]
+ )
+ });
+ }
+
+ #[gpui::test]
+ async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Hover with keyboard has no delay
+ cx.set_state(indoc! {"
+ fหn test() { println!(); }
+ "});
+ cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+ let symbol_range = cx.lsp_range(indoc! {"
+ ยซfnยป test() { println!(); }
+ "});
+ cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+ Ok(Some(lsp::Hover {
+ contents: lsp::HoverContents::Array(vec![
+ lsp::MarkedString::String("regular text for hover to show".to_string()),
+ lsp::MarkedString::String("".to_string()),
+ lsp::MarkedString::LanguageString(lsp::LanguageString {
+ language: "Rust".to_string(),
+ value: "".to_string(),
+ }),
+ ]),
+ range: Some(symbol_range),
+ }))
+ })
+ .next()
+ .await;
+
+ cx.condition(|editor, _| editor.hover_state.visible()).await;
+ cx.editor(|editor, _| {
+ assert_eq!(
+ editor.hover_state.info_popover.clone().unwrap().blocks,
+ vec![HoverBlock {
+ text: "regular text for hover to show".to_string(),
+ kind: HoverBlockKind::Markdown,
+ }],
+ "No empty string hovers should be shown"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Hover with keyboard has no delay
+ cx.set_state(indoc! {"
+ fหn test() { println!(); }
+ "});
+ cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+ let symbol_range = cx.lsp_range(indoc! {"
+ ยซfnยป test() { println!(); }
+ "});
+
+ let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n";
+ let markdown_string = format!("\n```rust\n{code_str}```");
+
+ let closure_markdown_string = markdown_string.clone();
+ cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| {
+ let future_markdown_string = closure_markdown_string.clone();
+ async move {
+ Ok(Some(lsp::Hover {
+ contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: future_markdown_string,
+ }),
+ range: Some(symbol_range),
+ }))
+ }
+ })
+ .next()
+ .await;
+
+ cx.condition(|editor, _| editor.hover_state.visible()).await;
+ cx.editor(|editor, _| {
+ let blocks = editor.hover_state.info_popover.clone().unwrap().blocks;
+ assert_eq!(
+ blocks,
+ vec![HoverBlock {
+ text: markdown_string,
+ kind: HoverBlockKind::Markdown,
+ }],
+ );
+
+ let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+ assert_eq!(
+ rendered.text,
+ code_str.trim(),
+ "Should not have extra line breaks at end of rendered hover"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ // Hover with just diagnostic, pops DiagnosticPopover immediately and then
+ // info popover once request completes
+ cx.set_state(indoc! {"
+ fn teหst() { println!(); }
+ "});
+
+ // Send diagnostic to client
+ let range = cx.text_anchor_range(indoc! {"
+ fn ยซtestยป() { println!(); }
+ "});
+ cx.update_buffer(|buffer, cx| {
+ let snapshot = buffer.text_snapshot();
+ let set = DiagnosticSet::from_sorted_entries(
+ vec![DiagnosticEntry {
+ range,
+ diagnostic: Diagnostic {
+ message: "A test diagnostic message.".to_string(),
+ ..Default::default()
+ },
+ }],
+ &snapshot,
+ );
+ buffer.update_diagnostics(LanguageServerId(0), set, cx);
+ });
+
+ // Hover pops diagnostic immediately
+ cx.update_editor(|editor, cx| hover(editor, &Hover, cx));
+ cx.foreground().run_until_parked();
+
+ cx.editor(|Editor { hover_state, .. }, _| {
+ assert!(hover_state.diagnostic_popover.is_some() && hover_state.info_popover.is_none())
+ });
+
+ // Info Popover shows after request responded to
+ let range = cx.lsp_range(indoc! {"
+ fn ยซtestยป() { println!(); }
+ "});
+ cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
+ Ok(Some(lsp::Hover {
+ contents: lsp::HoverContents::Markup(lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: "some new docs".to_string(),
+ }),
+ range: Some(range),
+ }))
+ });
+ cx.foreground()
+ .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+
+ cx.foreground().run_until_parked();
+ cx.editor(|Editor { hover_state, .. }, _| {
+ hover_state.diagnostic_popover.is_some() && hover_state.info_task.is_some()
+ });
+ }
+
+ #[gpui::test]
+ fn test_render_blocks(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ cx.add_window(|cx| {
+ let editor = Editor::single_line(None, cx);
+ let style = editor.style(cx);
+
+ struct Row {
+ blocks: Vec<HoverBlock>,
+ expected_marked_text: String,
+ expected_styles: Vec<HighlightStyle>,
+ }
+
+ let rows = &[
+ // Strong emphasis
+ Row {
+ blocks: vec![HoverBlock {
+ text: "one **two** three".to_string(),
+ kind: HoverBlockKind::Markdown,
+ }],
+ expected_marked_text: "one ยซtwoยป three".to_string(),
+ expected_styles: vec![HighlightStyle {
+ weight: Some(Weight::BOLD),
+ ..Default::default()
+ }],
+ },
+ // Links
+ Row {
+ blocks: vec three".to_string(),
+ kind: HoverBlockKind::Markdown,
+ }],
+ expected_marked_text: "one ยซtwoยป three".to_string(),
+ expected_styles: vec![HighlightStyle {
+ underline: Some(Underline {
+ thickness: 1.0.into(),
+ ..Default::default()
+ }),
+ ..Default::default()
+ }],
+ },
+ // Lists
+ Row {
+ blocks: vec
+ - d"
+ .unindent(),
+ kind: HoverBlockKind::Markdown,
+ }],
+ expected_marked_text: "
+ lists:
+ - one
+ - a
+ - b
+ - two
+ - ยซcยป
+ - d"
+ .unindent(),
+ expected_styles: vec![HighlightStyle {
+ underline: Some(Underline {
+ thickness: 1.0.into(),
+ ..Default::default()
+ }),
+ ..Default::default()
+ }],
+ },
+ // Multi-paragraph list items
+ Row {
+ blocks: vec![HoverBlock {
+ text: "
+ * one two
+ three
+
+ * four five
+ * six seven
+ eight
+
+ nine
+ * ten
+ * six"
+ .unindent(),
+ kind: HoverBlockKind::Markdown,
+ }],
+ expected_marked_text: "
+ - one two three
+ - four five
+ - six seven eight
+
+ nine
+ - ten
+ - six"
+ .unindent(),
+ expected_styles: vec![HighlightStyle {
+ underline: Some(Underline {
+ thickness: 1.0.into(),
+ ..Default::default()
+ }),
+ ..Default::default()
+ }],
+ },
+ ];
+
+ for Row {
+ blocks,
+ expected_marked_text,
+ expected_styles,
+ } in &rows[0..]
+ {
+ let rendered = smol::block_on(parse_blocks(&blocks, &Default::default(), None));
+
+ let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
+ let expected_highlights = ranges
+ .into_iter()
+ .zip(expected_styles.iter().cloned())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ rendered.text, expected_text,
+ "wrong text for input {blocks:?}"
+ );
+
+ let rendered_highlights: Vec<_> = rendered
+ .highlights
+ .iter()
+ .filter_map(|(range, highlight)| {
+ let highlight = highlight.to_highlight_style(&style.syntax)?;
+ Some((range.clone(), highlight))
+ })
+ .collect();
+
+ assert_eq!(
+ rendered_highlights, expected_highlights,
+ "wrong highlights for input {blocks:?}"
+ );
+ }
+
+ editor
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Right(
+ lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions {
+ resolve_provider: Some(true),
+ ..Default::default()
+ }),
+ )),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ struct TestStruct;
+
+ // ==================
+
+ struct TestNewType<T>(T);
+
+ fn main() {
+ let variableห = TestNewType(TestStruct);
+ }
+ "});
+
+ let hint_start_offset = cx.ranges(indoc! {"
+ struct TestStruct;
+
+ // ==================
+
+ struct TestNewType<T>(T);
+
+ fn main() {
+ let variableห = TestNewType(TestStruct);
+ }
+ "})[0]
+ .start;
+ let hint_position = cx.to_lsp(hint_start_offset);
+ let new_type_target_range = cx.lsp_range(indoc! {"
+ struct TestStruct;
+
+ // ==================
+
+ struct ยซTestNewTypeยป<T>(T);
+
+ fn main() {
+ let variable = TestNewType(TestStruct);
+ }
+ "});
+ let struct_target_range = cx.lsp_range(indoc! {"
+ struct ยซTestStructยป;
+
+ // ==================
+
+ struct TestNewType<T>(T);
+
+ fn main() {
+ let variable = TestNewType(TestStruct);
+ }
+ "});
+
+ let uri = cx.buffer_lsp_url.clone();
+ let new_type_label = "TestNewType";
+ let struct_label = "TestStruct";
+ let entire_hint_label = ": TestNewType<TestStruct>";
+ let closure_uri = uri.clone();
+ cx.lsp
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_uri = closure_uri.clone();
+ async move {
+ assert_eq!(params.text_document.uri, task_uri);
+ Ok(Some(vec![lsp::InlayHint {
+ position: hint_position,
+ label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+ value: entire_hint_label.to_string(),
+ ..Default::default()
+ }]),
+ kind: Some(lsp::InlayHintKind::TYPE),
+ text_edits: None,
+ tooltip: None,
+ padding_left: Some(false),
+ padding_right: Some(false),
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| {
+ let expected_layers = vec![entire_hint_label.to_string()];
+ assert_eq!(expected_layers, cached_hint_labels(editor));
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ });
+
+ let inlay_range = cx
+ .ranges(indoc! {"
+ struct TestStruct;
+
+ // ==================
+
+ struct TestNewType<T>(T);
+
+ fn main() {
+ let variableยซ ยป= TestNewType(TestStruct);
+ }
+ "})
+ .get(0)
+ .cloned()
+ .unwrap();
+ let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let previous_valid = inlay_range.start.to_display_point(&snapshot);
+ let next_valid = inlay_range.end.to_display_point(&snapshot);
+ assert_eq!(previous_valid.row(), next_valid.row());
+ assert!(previous_valid.column() < next_valid.column());
+ let exact_unclipped = DisplayPoint::new(
+ previous_valid.row(),
+ previous_valid.column()
+ + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2)
+ as u32,
+ );
+ PointForPosition {
+ previous_valid,
+ next_valid,
+ exact_unclipped,
+ column_overshoot_after_line_end: 0,
+ }
+ });
+ cx.update_editor(|editor, cx| {
+ update_inlay_link_and_hover_points(
+ &editor.snapshot(cx),
+ new_type_hint_part_hover_position,
+ editor,
+ true,
+ false,
+ cx,
+ );
+ });
+
+ let resolve_closure_uri = uri.clone();
+ cx.lsp
+ .handle_request::<lsp::request::InlayHintResolveRequest, _, _>(
+ move |mut hint_to_resolve, _| {
+ let mut resolved_hint_positions = BTreeSet::new();
+ let task_uri = resolve_closure_uri.clone();
+ async move {
+ let inserted = resolved_hint_positions.insert(hint_to_resolve.position);
+ assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice");
+
+ // `: TestNewType<TestStruct>`
+ hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![
+ lsp::InlayHintLabelPart {
+ value: ": ".to_string(),
+ ..Default::default()
+ },
+ lsp::InlayHintLabelPart {
+ value: new_type_label.to_string(),
+ location: Some(lsp::Location {
+ uri: task_uri.clone(),
+ range: new_type_target_range,
+ }),
+ tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!(
+ "A tooltip for `{new_type_label}`"
+ ))),
+ ..Default::default()
+ },
+ lsp::InlayHintLabelPart {
+ value: "<".to_string(),
+ ..Default::default()
+ },
+ lsp::InlayHintLabelPart {
+ value: struct_label.to_string(),
+ location: Some(lsp::Location {
+ uri: task_uri,
+ range: struct_target_range,
+ }),
+ tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent(
+ lsp::MarkupContent {
+ kind: lsp::MarkupKind::Markdown,
+ value: format!("A tooltip for `{struct_label}`"),
+ },
+ )),
+ ..Default::default()
+ },
+ lsp::InlayHintLabelPart {
+ value: ">".to_string(),
+ ..Default::default()
+ },
+ ]);
+
+ Ok(hint_to_resolve)
+ }
+ },
+ )
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ cx.update_editor(|editor, cx| {
+ update_inlay_link_and_hover_points(
+ &editor.snapshot(cx),
+ new_type_hint_part_hover_position,
+ editor,
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.foreground()
+ .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| {
+ let hover_state = &editor.hover_state;
+ assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+ let popover = hover_state.info_popover.as_ref().unwrap();
+ let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+ assert_eq!(
+ popover.symbol_range,
+ RangeInEditor::Inlay(InlayHighlight {
+ inlay: InlayId::Hint(0),
+ inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+ range: ": ".len()..": ".len() + new_type_label.len(),
+ }),
+ "Popover range should match the new type label part"
+ );
+ assert_eq!(
+ popover.parsed_content.text,
+ format!("A tooltip for `{new_type_label}`"),
+ "Rendered text should not anyhow alter backticks"
+ );
+ });
+
+ let struct_hint_part_hover_position = cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let previous_valid = inlay_range.start.to_display_point(&snapshot);
+ let next_valid = inlay_range.end.to_display_point(&snapshot);
+ assert_eq!(previous_valid.row(), next_valid.row());
+ assert!(previous_valid.column() < next_valid.column());
+ let exact_unclipped = DisplayPoint::new(
+ previous_valid.row(),
+ previous_valid.column()
+ + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2)
+ as u32,
+ );
+ PointForPosition {
+ previous_valid,
+ next_valid,
+ exact_unclipped,
+ column_overshoot_after_line_end: 0,
+ }
+ });
+ cx.update_editor(|editor, cx| {
+ update_inlay_link_and_hover_points(
+ &editor.snapshot(cx),
+ struct_hint_part_hover_position,
+ editor,
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.foreground()
+ .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| {
+ let hover_state = &editor.hover_state;
+ assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some());
+ let popover = hover_state.info_popover.as_ref().unwrap();
+ let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+ assert_eq!(
+ popover.symbol_range,
+ RangeInEditor::Inlay(InlayHighlight {
+ inlay: InlayId::Hint(0),
+ inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+ range: ": ".len() + new_type_label.len() + "<".len()
+ ..": ".len() + new_type_label.len() + "<".len() + struct_label.len(),
+ }),
+ "Popover range should match the struct label part"
+ );
+ assert_eq!(
+ popover.parsed_content.text,
+ format!("A tooltip for {struct_label}"),
+ "Rendered markdown element should remove backticks from text"
+ );
+ });
+ }
+}
@@ -0,0 +1,3349 @@
+use std::{
+ cmp,
+ ops::{ControlFlow, Range},
+ sync::Arc,
+ time::Duration,
+};
+
+use crate::{
+ display_map::Inlay, Anchor, Editor, ExcerptId, InlayId, MultiBuffer, MultiBufferSnapshot,
+};
+use anyhow::Context;
+use clock::Global;
+use futures::future;
+use gpui::{ModelContext, ModelHandle, Task, ViewContext};
+use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot};
+use parking_lot::RwLock;
+use project::{InlayHint, ResolveState};
+
+use collections::{hash_map, HashMap, HashSet};
+use language::language_settings::InlayHintSettings;
+use smol::lock::Semaphore;
+use sum_tree::Bias;
+use text::{ToOffset, ToPoint};
+use util::post_inc;
+
+pub struct InlayHintCache {
+ hints: HashMap<ExcerptId, Arc<RwLock<CachedExcerptHints>>>,
+ allowed_hint_kinds: HashSet<Option<InlayHintKind>>,
+ version: usize,
+ pub(super) enabled: bool,
+ update_tasks: HashMap<ExcerptId, TasksForRanges>,
+ lsp_request_limiter: Arc<Semaphore>,
+}
+
+#[derive(Debug)]
+struct TasksForRanges {
+ tasks: Vec<Task<()>>,
+ sorted_ranges: Vec<Range<language::Anchor>>,
+}
+
+#[derive(Debug)]
+pub struct CachedExcerptHints {
+ version: usize,
+ buffer_version: Global,
+ buffer_id: u64,
+ ordered_hints: Vec<InlayId>,
+ hints_by_id: HashMap<InlayId, InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum InvalidationStrategy {
+ RefreshRequested,
+ BufferEdited,
+ None,
+}
+
+#[derive(Debug, Default)]
+pub struct InlaySplice {
+ pub to_remove: Vec<InlayId>,
+ pub to_insert: Vec<Inlay>,
+}
+
+#[derive(Debug)]
+struct ExcerptHintsUpdate {
+ excerpt_id: ExcerptId,
+ remove_from_visible: Vec<InlayId>,
+ remove_from_cache: HashSet<InlayId>,
+ add_to_cache: Vec<InlayHint>,
+}
+
+#[derive(Debug, Clone, Copy)]
+struct ExcerptQuery {
+ buffer_id: u64,
+ excerpt_id: ExcerptId,
+ cache_version: usize,
+ invalidate: InvalidationStrategy,
+ reason: &'static str,
+}
+
+impl InvalidationStrategy {
+ fn should_invalidate(&self) -> bool {
+ matches!(
+ self,
+ InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited
+ )
+ }
+}
+
+impl TasksForRanges {
+ fn new(query_ranges: QueryRanges, task: Task<()>) -> Self {
+ let mut sorted_ranges = Vec::new();
+ sorted_ranges.extend(query_ranges.before_visible);
+ sorted_ranges.extend(query_ranges.visible);
+ sorted_ranges.extend(query_ranges.after_visible);
+ Self {
+ tasks: vec![task],
+ sorted_ranges,
+ }
+ }
+
+ fn update_cached_tasks(
+ &mut self,
+ buffer_snapshot: &BufferSnapshot,
+ query_ranges: QueryRanges,
+ invalidate: InvalidationStrategy,
+ spawn_task: impl FnOnce(QueryRanges) -> Task<()>,
+ ) {
+ let query_ranges = if invalidate.should_invalidate() {
+ self.tasks.clear();
+ self.sorted_ranges.clear();
+ query_ranges
+ } else {
+ let mut non_cached_query_ranges = query_ranges;
+ non_cached_query_ranges.before_visible = non_cached_query_ranges
+ .before_visible
+ .into_iter()
+ .flat_map(|query_range| {
+ self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+ })
+ .collect();
+ non_cached_query_ranges.visible = non_cached_query_ranges
+ .visible
+ .into_iter()
+ .flat_map(|query_range| {
+ self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+ })
+ .collect();
+ non_cached_query_ranges.after_visible = non_cached_query_ranges
+ .after_visible
+ .into_iter()
+ .flat_map(|query_range| {
+ self.remove_cached_ranges_from_query(buffer_snapshot, query_range)
+ })
+ .collect();
+ non_cached_query_ranges
+ };
+
+ if !query_ranges.is_empty() {
+ self.tasks.push(spawn_task(query_ranges));
+ }
+ }
+
+ fn remove_cached_ranges_from_query(
+ &mut self,
+ buffer_snapshot: &BufferSnapshot,
+ query_range: Range<language::Anchor>,
+ ) -> Vec<Range<language::Anchor>> {
+ let mut ranges_to_query = Vec::new();
+ let mut latest_cached_range = None::<&mut Range<language::Anchor>>;
+ for cached_range in self
+ .sorted_ranges
+ .iter_mut()
+ .skip_while(|cached_range| {
+ cached_range
+ .end
+ .cmp(&query_range.start, buffer_snapshot)
+ .is_lt()
+ })
+ .take_while(|cached_range| {
+ cached_range
+ .start
+ .cmp(&query_range.end, buffer_snapshot)
+ .is_le()
+ })
+ {
+ match latest_cached_range {
+ Some(latest_cached_range) => {
+ if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset
+ {
+ ranges_to_query.push(latest_cached_range.end..cached_range.start);
+ cached_range.start = latest_cached_range.end;
+ }
+ }
+ None => {
+ if query_range
+ .start
+ .cmp(&cached_range.start, buffer_snapshot)
+ .is_lt()
+ {
+ ranges_to_query.push(query_range.start..cached_range.start);
+ cached_range.start = query_range.start;
+ }
+ }
+ }
+ latest_cached_range = Some(cached_range);
+ }
+
+ match latest_cached_range {
+ Some(latest_cached_range) => {
+ if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset {
+ ranges_to_query.push(latest_cached_range.end..query_range.end);
+ latest_cached_range.end = query_range.end;
+ }
+ }
+ None => {
+ ranges_to_query.push(query_range.clone());
+ self.sorted_ranges.push(query_range);
+ self.sorted_ranges
+ .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot));
+ }
+ }
+
+ ranges_to_query
+ }
+
+ fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range<language::Anchor>) {
+ self.sorted_ranges = self
+ .sorted_ranges
+ .drain(..)
+ .filter_map(|mut cached_range| {
+ if cached_range.start.cmp(&range.end, buffer).is_gt()
+ || cached_range.end.cmp(&range.start, buffer).is_lt()
+ {
+ Some(vec![cached_range])
+ } else if cached_range.start.cmp(&range.start, buffer).is_ge()
+ && cached_range.end.cmp(&range.end, buffer).is_le()
+ {
+ None
+ } else if range.start.cmp(&cached_range.start, buffer).is_ge()
+ && range.end.cmp(&cached_range.end, buffer).is_le()
+ {
+ Some(vec![
+ cached_range.start..range.start,
+ range.end..cached_range.end,
+ ])
+ } else if cached_range.start.cmp(&range.start, buffer).is_ge() {
+ cached_range.start = range.end;
+ Some(vec![cached_range])
+ } else {
+ cached_range.end = range.start;
+ Some(vec![cached_range])
+ }
+ })
+ .flatten()
+ .collect();
+ }
+}
+
+impl InlayHintCache {
+ pub fn new(inlay_hint_settings: InlayHintSettings) -> Self {
+ Self {
+ allowed_hint_kinds: inlay_hint_settings.enabled_inlay_hint_kinds(),
+ enabled: inlay_hint_settings.enabled,
+ hints: HashMap::default(),
+ update_tasks: HashMap::default(),
+ version: 0,
+ lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)),
+ }
+ }
+
+ pub fn update_settings(
+ &mut self,
+ multi_buffer: &ModelHandle<MultiBuffer>,
+ new_hint_settings: InlayHintSettings,
+ visible_hints: Vec<Inlay>,
+ cx: &mut ViewContext<Editor>,
+ ) -> ControlFlow<Option<InlaySplice>> {
+ let new_allowed_hint_kinds = new_hint_settings.enabled_inlay_hint_kinds();
+ match (self.enabled, new_hint_settings.enabled) {
+ (false, false) => {
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ ControlFlow::Break(None)
+ }
+ (true, true) => {
+ if new_allowed_hint_kinds == self.allowed_hint_kinds {
+ ControlFlow::Break(None)
+ } else {
+ let new_splice = self.new_allowed_hint_kinds_splice(
+ multi_buffer,
+ &visible_hints,
+ &new_allowed_hint_kinds,
+ cx,
+ );
+ if new_splice.is_some() {
+ self.version += 1;
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ }
+ ControlFlow::Break(new_splice)
+ }
+ }
+ (true, false) => {
+ self.enabled = new_hint_settings.enabled;
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ if self.hints.is_empty() {
+ ControlFlow::Break(None)
+ } else {
+ self.clear();
+ ControlFlow::Break(Some(InlaySplice {
+ to_remove: visible_hints.iter().map(|inlay| inlay.id).collect(),
+ to_insert: Vec::new(),
+ }))
+ }
+ }
+ (false, true) => {
+ self.enabled = new_hint_settings.enabled;
+ self.allowed_hint_kinds = new_allowed_hint_kinds;
+ ControlFlow::Continue(())
+ }
+ }
+ }
+
+ pub fn spawn_hint_refresh(
+ &mut self,
+ reason: &'static str,
+ excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+ invalidate: InvalidationStrategy,
+ cx: &mut ViewContext<Editor>,
+ ) -> Option<InlaySplice> {
+ if !self.enabled {
+ return None;
+ }
+
+ let mut invalidated_hints = Vec::new();
+ if invalidate.should_invalidate() {
+ self.update_tasks
+ .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id));
+ self.hints.retain(|cached_excerpt, cached_hints| {
+ let retain = excerpts_to_query.contains_key(cached_excerpt);
+ if !retain {
+ invalidated_hints.extend(cached_hints.read().ordered_hints.iter().copied());
+ }
+ retain
+ });
+ }
+ if excerpts_to_query.is_empty() && invalidated_hints.is_empty() {
+ return None;
+ }
+
+ let cache_version = self.version + 1;
+ cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ spawn_new_update_tasks(
+ editor,
+ reason,
+ excerpts_to_query,
+ invalidate,
+ cache_version,
+ cx,
+ )
+ })
+ .ok();
+ })
+ .detach();
+
+ if invalidated_hints.is_empty() {
+ None
+ } else {
+ Some(InlaySplice {
+ to_remove: invalidated_hints,
+ to_insert: Vec::new(),
+ })
+ }
+ }
+
+ fn new_allowed_hint_kinds_splice(
+ &self,
+ multi_buffer: &ModelHandle<MultiBuffer>,
+ visible_hints: &[Inlay],
+ new_kinds: &HashSet<Option<InlayHintKind>>,
+ cx: &mut ViewContext<Editor>,
+ ) -> Option<InlaySplice> {
+ let old_kinds = &self.allowed_hint_kinds;
+ if new_kinds == old_kinds {
+ return None;
+ }
+
+ let mut to_remove = Vec::new();
+ let mut to_insert = Vec::new();
+ let mut shown_hints_to_remove = visible_hints.iter().fold(
+ HashMap::<ExcerptId, Vec<(Anchor, InlayId)>>::default(),
+ |mut current_hints, inlay| {
+ current_hints
+ .entry(inlay.position.excerpt_id)
+ .or_default()
+ .push((inlay.position, inlay.id));
+ current_hints
+ },
+ );
+
+ let multi_buffer = multi_buffer.read(cx);
+ let multi_buffer_snapshot = multi_buffer.snapshot(cx);
+
+ for (excerpt_id, excerpt_cached_hints) in &self.hints {
+ let shown_excerpt_hints_to_remove =
+ shown_hints_to_remove.entry(*excerpt_id).or_default();
+ let excerpt_cached_hints = excerpt_cached_hints.read();
+ let mut excerpt_cache = excerpt_cached_hints.ordered_hints.iter().fuse().peekable();
+ shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| {
+ let Some(buffer) = shown_anchor
+ .buffer_id
+ .and_then(|buffer_id| multi_buffer.buffer(buffer_id))
+ else {
+ return false;
+ };
+ let buffer_snapshot = buffer.read(cx).snapshot();
+ loop {
+ match excerpt_cache.peek() {
+ Some(&cached_hint_id) => {
+ let cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
+ if cached_hint_id == shown_hint_id {
+ excerpt_cache.next();
+ return !new_kinds.contains(&cached_hint.kind);
+ }
+
+ match cached_hint
+ .position
+ .cmp(&shown_anchor.text_anchor, &buffer_snapshot)
+ {
+ cmp::Ordering::Less | cmp::Ordering::Equal => {
+ if !old_kinds.contains(&cached_hint.kind)
+ && new_kinds.contains(&cached_hint.kind)
+ {
+ to_insert.push(Inlay::hint(
+ cached_hint_id.id(),
+ multi_buffer_snapshot.anchor_in_excerpt(
+ *excerpt_id,
+ cached_hint.position,
+ ),
+ &cached_hint,
+ ));
+ }
+ excerpt_cache.next();
+ }
+ cmp::Ordering::Greater => return true,
+ }
+ }
+ None => return true,
+ }
+ }
+ });
+
+ for cached_hint_id in excerpt_cache {
+ let maybe_missed_cached_hint = &excerpt_cached_hints.hints_by_id[cached_hint_id];
+ let cached_hint_kind = maybe_missed_cached_hint.kind;
+ if !old_kinds.contains(&cached_hint_kind) && new_kinds.contains(&cached_hint_kind) {
+ to_insert.push(Inlay::hint(
+ cached_hint_id.id(),
+ multi_buffer_snapshot
+ .anchor_in_excerpt(*excerpt_id, maybe_missed_cached_hint.position),
+ &maybe_missed_cached_hint,
+ ));
+ }
+ }
+ }
+
+ to_remove.extend(
+ shown_hints_to_remove
+ .into_values()
+ .flatten()
+ .map(|(_, hint_id)| hint_id),
+ );
+ if to_remove.is_empty() && to_insert.is_empty() {
+ None
+ } else {
+ Some(InlaySplice {
+ to_remove,
+ to_insert,
+ })
+ }
+ }
+
+ pub fn remove_excerpts(&mut self, excerpts_removed: Vec<ExcerptId>) -> Option<InlaySplice> {
+ let mut to_remove = Vec::new();
+ for excerpt_to_remove in excerpts_removed {
+ self.update_tasks.remove(&excerpt_to_remove);
+ if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) {
+ let cached_hints = cached_hints.read();
+ to_remove.extend(cached_hints.ordered_hints.iter().copied());
+ }
+ }
+ if to_remove.is_empty() {
+ None
+ } else {
+ self.version += 1;
+ Some(InlaySplice {
+ to_remove,
+ to_insert: Vec::new(),
+ })
+ }
+ }
+
+ pub fn clear(&mut self) {
+ if !self.update_tasks.is_empty() || !self.hints.is_empty() {
+ self.version += 1;
+ }
+ self.update_tasks.clear();
+ self.hints.clear();
+ }
+
+ pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option<InlayHint> {
+ self.hints
+ .get(&excerpt_id)?
+ .read()
+ .hints_by_id
+ .get(&hint_id)
+ .cloned()
+ }
+
+ pub fn hints(&self) -> Vec<InlayHint> {
+ let mut hints = Vec::new();
+ for excerpt_hints in self.hints.values() {
+ let excerpt_hints = excerpt_hints.read();
+ hints.extend(
+ excerpt_hints
+ .ordered_hints
+ .iter()
+ .map(|id| &excerpt_hints.hints_by_id[id])
+ .cloned(),
+ );
+ }
+ hints
+ }
+
+ pub fn version(&self) -> usize {
+ self.version
+ }
+
+ pub fn spawn_hint_resolve(
+ &self,
+ buffer_id: u64,
+ excerpt_id: ExcerptId,
+ id: InlayId,
+ cx: &mut ViewContext<'_, '_, Editor>,
+ ) {
+ if let Some(excerpt_hints) = self.hints.get(&excerpt_id) {
+ let mut guard = excerpt_hints.write();
+ if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
+ if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state {
+ let hint_to_resolve = cached_hint.clone();
+ let server_id = *server_id;
+ cached_hint.resolve_state = ResolveState::Resolving;
+ drop(guard);
+ cx.spawn(|editor, mut cx| async move {
+ let resolved_hint_task = editor.update(&mut cx, |editor, cx| {
+ editor
+ .buffer()
+ .read(cx)
+ .buffer(buffer_id)
+ .and_then(|buffer| {
+ let project = editor.project.as_ref()?;
+ Some(project.update(cx, |project, cx| {
+ project.resolve_inlay_hint(
+ hint_to_resolve,
+ buffer,
+ server_id,
+ cx,
+ )
+ }))
+ })
+ })?;
+ if let Some(resolved_hint_task) = resolved_hint_task {
+ let mut resolved_hint =
+ resolved_hint_task.await.context("hint resolve task")?;
+ editor.update(&mut cx, |editor, _| {
+ if let Some(excerpt_hints) =
+ editor.inlay_hint_cache.hints.get(&excerpt_id)
+ {
+ let mut guard = excerpt_hints.write();
+ if let Some(cached_hint) = guard.hints_by_id.get_mut(&id) {
+ if cached_hint.resolve_state == ResolveState::Resolving {
+ resolved_hint.resolve_state = ResolveState::Resolved;
+ *cached_hint = resolved_hint;
+ }
+ }
+ }
+ })?;
+ }
+
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+ }
+ }
+}
+
+fn spawn_new_update_tasks(
+ editor: &mut Editor,
+ reason: &'static str,
+ excerpts_to_query: HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)>,
+ invalidate: InvalidationStrategy,
+ update_cache_version: usize,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) {
+ let visible_hints = Arc::new(editor.visible_inlay_hints(cx));
+ for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in
+ excerpts_to_query
+ {
+ if excerpt_visible_range.is_empty() {
+ continue;
+ }
+ let buffer = excerpt_buffer.read(cx);
+ let buffer_id = buffer.remote_id();
+ let buffer_snapshot = buffer.snapshot();
+ if buffer_snapshot
+ .version()
+ .changed_since(&new_task_buffer_version)
+ {
+ continue;
+ }
+
+ let cached_excerpt_hints = editor.inlay_hint_cache.hints.get(&excerpt_id).cloned();
+ if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ let cached_buffer_version = &cached_excerpt_hints.buffer_version;
+ if cached_excerpt_hints.version > update_cache_version
+ || cached_buffer_version.changed_since(&new_task_buffer_version)
+ {
+ continue;
+ }
+ };
+
+ let (multi_buffer_snapshot, Some(query_ranges)) =
+ editor.buffer.update(cx, |multi_buffer, cx| {
+ (
+ multi_buffer.snapshot(cx),
+ determine_query_ranges(
+ multi_buffer,
+ excerpt_id,
+ &excerpt_buffer,
+ excerpt_visible_range,
+ cx,
+ ),
+ )
+ })
+ else {
+ return;
+ };
+ let query = ExcerptQuery {
+ buffer_id,
+ excerpt_id,
+ cache_version: update_cache_version,
+ invalidate,
+ reason,
+ };
+
+ let new_update_task = |query_ranges| {
+ new_update_task(
+ query,
+ query_ranges,
+ multi_buffer_snapshot,
+ buffer_snapshot.clone(),
+ Arc::clone(&visible_hints),
+ cached_excerpt_hints,
+ Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter),
+ cx,
+ )
+ };
+
+ match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) {
+ hash_map::Entry::Occupied(mut o) => {
+ o.get_mut().update_cached_tasks(
+ &buffer_snapshot,
+ query_ranges,
+ invalidate,
+ new_update_task,
+ );
+ }
+ hash_map::Entry::Vacant(v) => {
+ v.insert(TasksForRanges::new(
+ query_ranges.clone(),
+ new_update_task(query_ranges),
+ ));
+ }
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+struct QueryRanges {
+ before_visible: Vec<Range<language::Anchor>>,
+ visible: Vec<Range<language::Anchor>>,
+ after_visible: Vec<Range<language::Anchor>>,
+}
+
+impl QueryRanges {
+ fn is_empty(&self) -> bool {
+ self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty()
+ }
+}
+
+fn determine_query_ranges(
+ multi_buffer: &mut MultiBuffer,
+ excerpt_id: ExcerptId,
+ excerpt_buffer: &ModelHandle<Buffer>,
+ excerpt_visible_range: Range<usize>,
+ cx: &mut ModelContext<'_, MultiBuffer>,
+) -> Option<QueryRanges> {
+ let full_excerpt_range = multi_buffer
+ .excerpts_for_buffer(excerpt_buffer, cx)
+ .into_iter()
+ .find(|(id, _)| id == &excerpt_id)
+ .map(|(_, range)| range.context)?;
+ let buffer = excerpt_buffer.read(cx);
+ let snapshot = buffer.snapshot();
+ let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start;
+
+ let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end {
+ return None;
+ } else {
+ vec![
+ buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left))
+ ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)),
+ ]
+ };
+
+ let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot);
+ let after_visible_range_start = excerpt_visible_range
+ .end
+ .saturating_add(1)
+ .min(full_excerpt_range_end_offset)
+ .min(buffer.len());
+ let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset {
+ Vec::new()
+ } else {
+ let after_range_end_offset = after_visible_range_start
+ .saturating_add(excerpt_visible_len)
+ .min(full_excerpt_range_end_offset)
+ .min(buffer.len());
+ vec![
+ buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left))
+ ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)),
+ ]
+ };
+
+ let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot);
+ let before_visible_range_end = excerpt_visible_range
+ .start
+ .saturating_sub(1)
+ .max(full_excerpt_range_start_offset);
+ let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset {
+ Vec::new()
+ } else {
+ let before_range_start_offset = before_visible_range_end
+ .saturating_sub(excerpt_visible_len)
+ .max(full_excerpt_range_start_offset);
+ vec![
+ buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left))
+ ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)),
+ ]
+ };
+
+ Some(QueryRanges {
+ before_visible: before_visible_range,
+ visible: visible_range,
+ after_visible: after_visible_range,
+ })
+}
+
+const MAX_CONCURRENT_LSP_REQUESTS: usize = 5;
+const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400;
+
+fn new_update_task(
+ query: ExcerptQuery,
+ query_ranges: QueryRanges,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ buffer_snapshot: BufferSnapshot,
+ visible_hints: Arc<Vec<Inlay>>,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ lsp_request_limiter: Arc<Semaphore>,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) -> Task<()> {
+ cx.spawn(|editor, mut cx| async move {
+ let closure_cx = cx.clone();
+ let fetch_and_update_hints = |invalidate, range| {
+ fetch_and_update_hints(
+ editor.clone(),
+ multi_buffer_snapshot.clone(),
+ buffer_snapshot.clone(),
+ Arc::clone(&visible_hints),
+ cached_excerpt_hints.as_ref().map(Arc::clone),
+ query,
+ invalidate,
+ range,
+ Arc::clone(&lsp_request_limiter),
+ closure_cx.clone(),
+ )
+ };
+ let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map(
+ |visible_range| async move {
+ (
+ visible_range.clone(),
+ fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range)
+ .await,
+ )
+ },
+ ))
+ .await;
+
+ let hint_delay = cx.background().timer(Duration::from_millis(
+ INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS,
+ ));
+
+ let mut query_range_failed = |range: &Range<language::Anchor>, e: anyhow::Error| {
+ log::error!("inlay hint update task for range {range:?} failed: {e:#}");
+ editor
+ .update(&mut cx, |editor, _| {
+ if let Some(task_ranges) = editor
+ .inlay_hint_cache
+ .update_tasks
+ .get_mut(&query.excerpt_id)
+ {
+ task_ranges.invalidate_range(&buffer_snapshot, &range);
+ }
+ })
+ .ok()
+ };
+
+ for (range, result) in visible_range_update_results {
+ if let Err(e) = result {
+ query_range_failed(&range, e);
+ }
+ }
+
+ hint_delay.await;
+ let invisible_range_update_results = future::join_all(
+ query_ranges
+ .before_visible
+ .into_iter()
+ .chain(query_ranges.after_visible.into_iter())
+ .map(|invisible_range| async move {
+ (
+ invisible_range.clone(),
+ fetch_and_update_hints(false, invisible_range).await,
+ )
+ }),
+ )
+ .await;
+ for (range, result) in invisible_range_update_results {
+ if let Err(e) = result {
+ query_range_failed(&range, e);
+ }
+ }
+ })
+}
+
+async fn fetch_and_update_hints(
+ editor: gpui::WeakViewHandle<Editor>,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ buffer_snapshot: BufferSnapshot,
+ visible_hints: Arc<Vec<Inlay>>,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ query: ExcerptQuery,
+ invalidate: bool,
+ fetch_range: Range<language::Anchor>,
+ lsp_request_limiter: Arc<Semaphore>,
+ mut cx: gpui::AsyncAppContext,
+) -> anyhow::Result<()> {
+ let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() {
+ (None, false)
+ } else {
+ match lsp_request_limiter.try_acquire() {
+ Some(guard) => (Some(guard), false),
+ None => (Some(lsp_request_limiter.acquire().await), true),
+ }
+ };
+ let fetch_range_to_log =
+ fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot);
+ let inlay_hints_fetch_task = editor
+ .update(&mut cx, |editor, cx| {
+ if got_throttled {
+ let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) {
+ Some((_, _, current_visible_range)) => {
+ let visible_offset_length = current_visible_range.len();
+ let double_visible_range = current_visible_range
+ .start
+ .saturating_sub(visible_offset_length)
+ ..current_visible_range
+ .end
+ .saturating_add(visible_offset_length)
+ .min(buffer_snapshot.len());
+ !double_visible_range
+ .contains(&fetch_range.start.to_offset(&buffer_snapshot))
+ && !double_visible_range
+ .contains(&fetch_range.end.to_offset(&buffer_snapshot))
+ },
+ None => true,
+ };
+ if query_not_around_visible_range {
+ log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping.");
+ if let Some(task_ranges) = editor
+ .inlay_hint_cache
+ .update_tasks
+ .get_mut(&query.excerpt_id)
+ {
+ task_ranges.invalidate_range(&buffer_snapshot, &fetch_range);
+ }
+ return None;
+ }
+ }
+ editor
+ .buffer()
+ .read(cx)
+ .buffer(query.buffer_id)
+ .and_then(|buffer| {
+ let project = editor.project.as_ref()?;
+ Some(project.update(cx, |project, cx| {
+ project.inlay_hints(buffer, fetch_range.clone(), cx)
+ }))
+ })
+ })
+ .ok()
+ .flatten();
+ let new_hints = match inlay_hints_fetch_task {
+ Some(fetch_task) => {
+ log::debug!(
+ "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}",
+ query_reason = query.reason,
+ );
+ log::trace!(
+ "Currently visible hints: {visible_hints:?}, cached hints present: {}",
+ cached_excerpt_hints.is_some(),
+ );
+ fetch_task.await.context("inlay hint fetch task")?
+ }
+ None => return Ok(()),
+ };
+ drop(lsp_request_guard);
+ log::debug!(
+ "Fetched {} hints for range {fetch_range_to_log:?}",
+ new_hints.len()
+ );
+ log::trace!("Fetched hints: {new_hints:?}");
+
+ let background_task_buffer_snapshot = buffer_snapshot.clone();
+ let backround_fetch_range = fetch_range.clone();
+ let new_update = cx
+ .background()
+ .spawn(async move {
+ calculate_hint_updates(
+ query.excerpt_id,
+ invalidate,
+ backround_fetch_range,
+ new_hints,
+ &background_task_buffer_snapshot,
+ cached_excerpt_hints,
+ &visible_hints,
+ )
+ })
+ .await;
+ if let Some(new_update) = new_update {
+ log::debug!(
+ "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}",
+ new_update.remove_from_visible.len(),
+ new_update.remove_from_cache.len(),
+ new_update.add_to_cache.len()
+ );
+ log::trace!("New update: {new_update:?}");
+ editor
+ .update(&mut cx, |editor, cx| {
+ apply_hint_update(
+ editor,
+ new_update,
+ query,
+ invalidate,
+ buffer_snapshot,
+ multi_buffer_snapshot,
+ cx,
+ );
+ })
+ .ok();
+ }
+ Ok(())
+}
+
+fn calculate_hint_updates(
+ excerpt_id: ExcerptId,
+ invalidate: bool,
+ fetch_range: Range<language::Anchor>,
+ new_excerpt_hints: Vec<InlayHint>,
+ buffer_snapshot: &BufferSnapshot,
+ cached_excerpt_hints: Option<Arc<RwLock<CachedExcerptHints>>>,
+ visible_hints: &[Inlay],
+) -> Option<ExcerptHintsUpdate> {
+ let mut add_to_cache = Vec::<InlayHint>::new();
+ let mut excerpt_hints_to_persist = HashMap::default();
+ for new_hint in new_excerpt_hints {
+ if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) {
+ continue;
+ }
+ let missing_from_cache = match &cached_excerpt_hints {
+ Some(cached_excerpt_hints) => {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ match cached_excerpt_hints
+ .ordered_hints
+ .binary_search_by(|probe| {
+ cached_excerpt_hints.hints_by_id[probe]
+ .position
+ .cmp(&new_hint.position, buffer_snapshot)
+ }) {
+ Ok(ix) => {
+ let mut missing_from_cache = true;
+ for id in &cached_excerpt_hints.ordered_hints[ix..] {
+ let cached_hint = &cached_excerpt_hints.hints_by_id[id];
+ if new_hint
+ .position
+ .cmp(&cached_hint.position, buffer_snapshot)
+ .is_gt()
+ {
+ break;
+ }
+ if cached_hint == &new_hint {
+ excerpt_hints_to_persist.insert(*id, cached_hint.kind);
+ missing_from_cache = false;
+ }
+ }
+ missing_from_cache
+ }
+ Err(_) => true,
+ }
+ }
+ None => true,
+ };
+ if missing_from_cache {
+ add_to_cache.push(new_hint);
+ }
+ }
+
+ let mut remove_from_visible = Vec::new();
+ let mut remove_from_cache = HashSet::default();
+ if invalidate {
+ remove_from_visible.extend(
+ visible_hints
+ .iter()
+ .filter(|hint| hint.position.excerpt_id == excerpt_id)
+ .map(|inlay_hint| inlay_hint.id)
+ .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)),
+ );
+
+ if let Some(cached_excerpt_hints) = &cached_excerpt_hints {
+ let cached_excerpt_hints = cached_excerpt_hints.read();
+ remove_from_cache.extend(
+ cached_excerpt_hints
+ .ordered_hints
+ .iter()
+ .filter(|cached_inlay_id| {
+ !excerpt_hints_to_persist.contains_key(cached_inlay_id)
+ })
+ .copied(),
+ );
+ }
+ }
+
+ if remove_from_visible.is_empty() && remove_from_cache.is_empty() && add_to_cache.is_empty() {
+ None
+ } else {
+ Some(ExcerptHintsUpdate {
+ excerpt_id,
+ remove_from_visible,
+ remove_from_cache,
+ add_to_cache,
+ })
+ }
+}
+
+fn contains_position(
+ range: &Range<language::Anchor>,
+ position: language::Anchor,
+ buffer_snapshot: &BufferSnapshot,
+) -> bool {
+ range.start.cmp(&position, buffer_snapshot).is_le()
+ && range.end.cmp(&position, buffer_snapshot).is_ge()
+}
+
+fn apply_hint_update(
+ editor: &mut Editor,
+ new_update: ExcerptHintsUpdate,
+ query: ExcerptQuery,
+ invalidate: bool,
+ buffer_snapshot: BufferSnapshot,
+ multi_buffer_snapshot: MultiBufferSnapshot,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) {
+ let cached_excerpt_hints = editor
+ .inlay_hint_cache
+ .hints
+ .entry(new_update.excerpt_id)
+ .or_insert_with(|| {
+ Arc::new(RwLock::new(CachedExcerptHints {
+ version: query.cache_version,
+ buffer_version: buffer_snapshot.version().clone(),
+ buffer_id: query.buffer_id,
+ ordered_hints: Vec::new(),
+ hints_by_id: HashMap::default(),
+ }))
+ });
+ let mut cached_excerpt_hints = cached_excerpt_hints.write();
+ match query.cache_version.cmp(&cached_excerpt_hints.version) {
+ cmp::Ordering::Less => return,
+ cmp::Ordering::Greater | cmp::Ordering::Equal => {
+ cached_excerpt_hints.version = query.cache_version;
+ }
+ }
+
+ let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty();
+ cached_excerpt_hints
+ .ordered_hints
+ .retain(|hint_id| !new_update.remove_from_cache.contains(hint_id));
+ cached_excerpt_hints
+ .hints_by_id
+ .retain(|hint_id, _| !new_update.remove_from_cache.contains(hint_id));
+ let mut splice = InlaySplice {
+ to_remove: new_update.remove_from_visible,
+ to_insert: Vec::new(),
+ };
+ for new_hint in new_update.add_to_cache {
+ let insert_position = match cached_excerpt_hints
+ .ordered_hints
+ .binary_search_by(|probe| {
+ cached_excerpt_hints.hints_by_id[probe]
+ .position
+ .cmp(&new_hint.position, &buffer_snapshot)
+ }) {
+ Ok(i) => {
+ let mut insert_position = Some(i);
+ for id in &cached_excerpt_hints.ordered_hints[i..] {
+ let cached_hint = &cached_excerpt_hints.hints_by_id[id];
+ if new_hint
+ .position
+ .cmp(&cached_hint.position, &buffer_snapshot)
+ .is_gt()
+ {
+ break;
+ }
+ if cached_hint.text() == new_hint.text() {
+ insert_position = None;
+ break;
+ }
+ }
+ insert_position
+ }
+ Err(i) => Some(i),
+ };
+
+ if let Some(insert_position) = insert_position {
+ let new_inlay_id = post_inc(&mut editor.next_inlay_id);
+ if editor
+ .inlay_hint_cache
+ .allowed_hint_kinds
+ .contains(&new_hint.kind)
+ {
+ let new_hint_position =
+ multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position);
+ splice
+ .to_insert
+ .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint));
+ }
+ let new_id = InlayId::Hint(new_inlay_id);
+ cached_excerpt_hints.hints_by_id.insert(new_id, new_hint);
+ cached_excerpt_hints
+ .ordered_hints
+ .insert(insert_position, new_id);
+ cached_inlays_changed = true;
+ }
+ }
+ cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone();
+ drop(cached_excerpt_hints);
+
+ if invalidate {
+ let mut outdated_excerpt_caches = HashSet::default();
+ for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints {
+ let excerpt_hints = excerpt_hints.read();
+ if excerpt_hints.buffer_id == query.buffer_id
+ && excerpt_id != &query.excerpt_id
+ && buffer_snapshot
+ .version()
+ .changed_since(&excerpt_hints.buffer_version)
+ {
+ outdated_excerpt_caches.insert(*excerpt_id);
+ splice
+ .to_remove
+ .extend(excerpt_hints.ordered_hints.iter().copied());
+ }
+ }
+ cached_inlays_changed |= !outdated_excerpt_caches.is_empty();
+ editor
+ .inlay_hint_cache
+ .hints
+ .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id));
+ }
+
+ let InlaySplice {
+ to_remove,
+ to_insert,
+ } = splice;
+ let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty();
+ if cached_inlays_changed || displayed_inlays_changed {
+ editor.inlay_hint_cache.version += 1;
+ }
+ if displayed_inlays_changed {
+ editor.splice_inlay_hints(to_remove, to_insert, cx)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
+
+ use crate::{
+ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
+ serde_json::json,
+ ExcerptRange,
+ };
+ use futures::StreamExt;
+ use gpui::{executor::Deterministic, TestAppContext, ViewHandle};
+ use itertools::Itertools;
+ use language::{
+ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig,
+ };
+ use lsp::FakeLanguageServer;
+ use parking_lot::Mutex;
+ use project::{FakeFs, Project};
+ use settings::SettingsStore;
+ use text::{Point, ToPoint};
+ use workspace::Workspace;
+
+ use crate::editor_tests::update_test_language_settings;
+
+ use super::*;
+
+ #[gpui::test]
+ async fn test_basic_cache_update_with_duplicate_hints(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ let current_call_id =
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ let mut new_hints = Vec::with_capacity(2 * current_call_id as usize);
+ for _ in 0..2 {
+ let mut i = current_call_id;
+ loop {
+ new_hints.push(lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ });
+ if i == 0 {
+ break;
+ }
+ i -= 1;
+ }
+ }
+
+ Ok(Some(new_hints))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some change", cx);
+ edits_made += 1;
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string(), "1".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get new hints after an edit"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ edits_made += 1;
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get new hints after hint refresh/ request"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_cache_update_on_lsp_completion_tasks(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ let current_call_id =
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, current_call_id),
+ label: lsp::InlayHintLabel::String(current_call_id.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ let progress_token = "test_progress_token";
+ fake_server
+ .request::<lsp::request::WorkDoneProgressCreate>(lsp::WorkDoneProgressCreateParams {
+ token: lsp::ProgressToken::String(progress_token.to_string()),
+ })
+ .await
+ .expect("work done progress create request failed");
+ cx.foreground().run_until_parked();
+ fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+ token: lsp::ProgressToken::String(progress_token.to_string()),
+ value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::Begin(
+ lsp::WorkDoneProgressBegin::default(),
+ )),
+ });
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should not update hints while the work task is running"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ edits_made,
+ "Should not update the cache while the work task is running"
+ );
+ });
+
+ fake_server.notify::<lsp::notification::Progress>(lsp::ProgressParams {
+ token: lsp::ProgressToken::String(progress_token.to_string()),
+ value: lsp::ProgressParamsValue::WorkDone(lsp::WorkDoneProgress::End(
+ lsp::WorkDoneProgressEnd::default(),
+ )),
+ });
+ cx.foreground().run_until_parked();
+
+ edits_made += 1;
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["1".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "New hints should be queried after the work task is done"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ edits_made,
+ "Cache version should udpate once after the work task is done"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_no_hint_updates_for_unrelated_language_files(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.md": "Test md file with some text",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let mut rs_fake_servers = None;
+ let mut md_fake_servers = None;
+ for (name, path_suffix) in [("Rust", "rs"), ("Markdown", "md")] {
+ let mut language = Language::new(
+ LanguageConfig {
+ name: name.into(),
+ path_suffixes: vec![path_suffix.to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ name,
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ match name {
+ "Rust" => rs_fake_servers = Some(fake_servers),
+ "Markdown" => md_fake_servers = Some(fake_servers),
+ _ => unreachable!(),
+ }
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::new(language));
+ });
+ }
+
+ let _rs_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let rs_fake_server = rs_fake_servers.unwrap().next().await.unwrap();
+ let rs_editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let rs_lsp_request_count = Arc::new(AtomicU32::new(0));
+ rs_fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&rs_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ rs_editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ 1,
+ "Rust editor update the cache version after every cache/view change"
+ );
+ });
+
+ cx.foreground().run_until_parked();
+ let _md_buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/other.md", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let md_fake_server = md_fake_servers.unwrap().next().await.unwrap();
+ let md_editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "other.md"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let md_lsp_request_count = Arc::new(AtomicU32::new(0));
+ md_fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&md_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/other.md").unwrap(),
+ );
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ md_editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Markdown editor should have a separate verison, repeating Rust editor rules"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, 1);
+ });
+
+ rs_editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some rs change", cx);
+ });
+ cx.foreground().run_until_parked();
+ rs_editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["1".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Rust inlay cache should change after the edit"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ 2,
+ "Every time hint cache changes, cache version should be incremented"
+ );
+ });
+ md_editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["0".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Markdown editor should not be affected by Rust editor changes"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, 1);
+ });
+
+ md_editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input("some md change", cx);
+ });
+ cx.foreground().run_until_parked();
+ md_editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["1".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Rust editor should not be affected by Markdown editor changes"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, 2);
+ });
+ rs_editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["1".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Markdown editor should also change independently"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, 2);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hint_setting_changes(cx: &mut gpui::TestAppContext) {
+ let allowed_hint_kinds = HashSet::from_iter([None, Some(InlayHintKind::Type)]);
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: allowed_hint_kinds.contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: allowed_hint_kinds.contains(&None),
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let another_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+ async move {
+ Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst);
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ Ok(Some(vec![
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 1),
+ label: lsp::InlayHintLabel::String("type hint".to_string()),
+ kind: Some(lsp::InlayHintKind::TYPE),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 2),
+ label: lsp::InlayHintLabel::String("parameter hint".to_string()),
+ kind: Some(lsp::InlayHintKind::PARAMETER),
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ lsp::InlayHint {
+ position: lsp::Position::new(0, 3),
+ label: lsp::InlayHintLabel::String("other hint".to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ },
+ ]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ let mut edits_made = 1;
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 1,
+ "Should query new hints once"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its first hints when opening the editor"
+ );
+ assert_eq!(
+ vec!["other hint".to_string(), "type hint".to_string()],
+ visible_hint_labels(editor, cx)
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor update the cache version after every cache/view change"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should load new hints twice"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Cached hints should not change due to allowed hint kinds settings update"
+ );
+ assert_eq!(
+ vec!["other hint".to_string(), "type hint".to_string()],
+ visible_hint_labels(editor, cx)
+ );
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ edits_made,
+ "Should not update cache version due to new loaded hints being the same"
+ );
+ });
+
+ for (new_allowed_hint_kinds, expected_visible_hints) in [
+ (HashSet::from_iter([None]), vec!["other hint".to_string()]),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Type)]),
+ vec!["type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Parameter)]),
+ vec!["parameter hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([None, Some(InlayHintKind::Type)]),
+ vec!["other hint".to_string(), "type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([None, Some(InlayHintKind::Parameter)]),
+ vec!["other hint".to_string(), "parameter hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([Some(InlayHintKind::Type), Some(InlayHintKind::Parameter)]),
+ vec!["parameter hint".to_string(), "type hint".to_string()],
+ ),
+ (
+ HashSet::from_iter([
+ None,
+ Some(InlayHintKind::Type),
+ Some(InlayHintKind::Parameter),
+ ]),
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ ),
+ ] {
+ edits_made += 1;
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: new_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: new_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints on allowed hint kinds change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its cached hints unchanged after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ expected_visible_hints,
+ visible_hint_labels(editor, cx),
+ "Should get its visible hints filtered after the settings change for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, new_allowed_hint_kinds,
+ "Cache should use editor settings to get the allowed hint kinds for hint kinds {new_allowed_hint_kinds:?}"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should update the cache version after every cache/view change for hint kinds {new_allowed_hint_kinds:?} due to visible hints change"
+ );
+ });
+ }
+
+ edits_made += 1;
+ let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]);
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: false,
+ show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: another_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: another_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints when hints got disabled"
+ );
+ assert!(
+ cached_hint_labels(editor).is_empty(),
+ "Should clear the cache when hints got disabled"
+ );
+ assert!(
+ visible_hint_labels(editor, cx).is_empty(),
+ "Should clear visible hints when hints got disabled"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, another_allowed_hint_kinds,
+ "Should update its allowed hint kinds even when hints got disabled"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "The editor should update the cache version after hints got disabled"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should not load new hints when they got disabled"
+ );
+ assert!(cached_hint_labels(editor).is_empty());
+ assert!(visible_hint_labels(editor, cx).is_empty());
+ assert_eq!(
+ editor.inlay_hint_cache().version, edits_made,
+ "The editor should not update the cache version after /refresh query without updates"
+ );
+ });
+
+ let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]);
+ edits_made += 1;
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)),
+ show_parameter_hints: final_allowed_hint_kinds
+ .contains(&Some(InlayHintKind::Parameter)),
+ show_other_hints: final_allowed_hint_kinds.contains(&None),
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 3,
+ "Should query for new hints when they got reenabled"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ "Should get its cached hints fully repopulated after the hints got reenabled"
+ );
+ assert_eq!(
+ vec!["parameter hint".to_string()],
+ visible_hint_labels(editor, cx),
+ "Should get its visible hints repopulated and filtered after the h"
+ );
+ let inlay_cache = editor.inlay_hint_cache();
+ assert_eq!(
+ inlay_cache.allowed_hint_kinds, final_allowed_hint_kinds,
+ "Cache should update editor settings when hints got reenabled"
+ );
+ assert_eq!(
+ inlay_cache.version, edits_made,
+ "Cache should update its version after hints got reenabled"
+ );
+ });
+
+ fake_server
+ .request::<lsp::request::InlayHintRefreshRequest>(())
+ .await
+ .expect("inlay refresh request failed");
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 4,
+ "Should query for new hints again"
+ );
+ assert_eq!(
+ vec![
+ "other hint".to_string(),
+ "parameter hint".to_string(),
+ "type hint".to_string(),
+ ],
+ cached_hint_labels(editor),
+ );
+ assert_eq!(
+ vec!["parameter hint".to_string()],
+ visible_hint_labels(editor, cx),
+ );
+ assert_eq!(editor.inlay_hint_cache().version, edits_made);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_hint_request_cancellation(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+ let fake_server = Arc::new(fake_server);
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let another_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&another_lsp_request_count);
+ async move {
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+
+ let mut expected_changes = Vec::new();
+ for change_after_opening in [
+ "initial change #1",
+ "initial change #2",
+ "initial change #3",
+ ] {
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(change_after_opening, cx);
+ });
+ expected_changes.push(change_after_opening);
+ }
+
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let current_text = editor.text(cx);
+ for change in &expected_changes {
+ assert!(
+ current_text.contains(change),
+ "Should apply all changes made"
+ );
+ }
+ assert_eq!(
+ lsp_request_count.load(Ordering::Relaxed),
+ 2,
+ "Should query new hints twice: for editor init and for the last edit that interrupted all others"
+ );
+ let expected_hints = vec!["2".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get hints from the last edit landed only"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version, 1,
+ "Only one update should be registered in the cache after all cancellations"
+ );
+ });
+
+ let mut edits = Vec::new();
+ for async_later_change in [
+ "another change #1",
+ "another change #2",
+ "another change #3",
+ ] {
+ expected_changes.push(async_later_change);
+ let task_editor = editor.clone();
+ let mut task_cx = cx.clone();
+ edits.push(cx.foreground().spawn(async move {
+ task_editor.update(&mut task_cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| s.select_ranges([13..13]));
+ editor.handle_input(async_later_change, cx);
+ });
+ }));
+ }
+ let _ = future::join_all(edits).await;
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let current_text = editor.text(cx);
+ for change in &expected_changes {
+ assert!(
+ current_text.contains(change),
+ "Should apply all changes made"
+ );
+ }
+ assert_eq!(
+ lsp_request_count.load(Ordering::SeqCst),
+ 3,
+ "Should query new hints one more time, for the last edit only"
+ );
+ let expected_hints = vec!["3".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should get hints from the last edit landed only"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ 2,
+ "Should update the cache version once more, for the new change"
+ );
+ });
+ }
+
+ #[gpui::test(iterations = 10)]
+ async fn test_large_buffer_inlay_requests_split(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", "let i = 5;\n".repeat(500)),
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let lsp_request_ranges = Arc::new(Mutex::new(Vec::new()));
+ let lsp_request_count = Arc::new(AtomicUsize::new(0));
+ let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges);
+ let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_ranges = Arc::clone(&closure_lsp_request_ranges);
+ let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+
+ task_lsp_request_ranges.lock().push(params.range);
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
+ Ok(Some(vec![lsp::InlayHint {
+ position: params.range.end,
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ fn editor_visible_range(
+ editor: &ViewHandle<Editor>,
+ cx: &mut gpui::TestAppContext,
+ ) -> Range<Point> {
+ let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx));
+ assert_eq!(
+ ranges.len(),
+ 1,
+ "Single buffer should produce a single excerpt with visible range"
+ );
+ let (_, (excerpt_buffer, _, excerpt_visible_range)) =
+ ranges.into_iter().next().unwrap();
+ excerpt_buffer.update(cx, |buffer, _| {
+ let snapshot = buffer.snapshot();
+ let start = buffer
+ .anchor_before(excerpt_visible_range.start)
+ .to_point(&snapshot);
+ let end = buffer
+ .anchor_after(excerpt_visible_range.end)
+ .to_point(&snapshot);
+ start..end
+ })
+ }
+
+ // in large buffers, requests are made for more than visible range of a buffer.
+ // invisible parts are queried later, to avoid excessive requests on quick typing.
+ // wait the timeout needed to get all requests.
+ cx.foreground().advance_clock(Duration::from_millis(
+ INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+ ));
+ cx.foreground().run_until_parked();
+ let initial_visible_range = editor_visible_range(&editor, cx);
+ let lsp_initial_visible_range = lsp::Range::new(
+ lsp::Position::new(
+ initial_visible_range.start.row,
+ initial_visible_range.start.column,
+ ),
+ lsp::Position::new(
+ initial_visible_range.end.row,
+ initial_visible_range.end.column,
+ ),
+ );
+ let expected_initial_query_range_end =
+ lsp::Position::new(initial_visible_range.end.row * 2, 2);
+ let mut expected_invisible_query_start = lsp_initial_visible_range.end;
+ expected_invisible_query_start.character += 1;
+ editor.update(cx, |editor, cx| {
+ let ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+ assert_eq!(ranges.len(), 2,
+ "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}");
+ let visible_query_range = &ranges[0];
+ assert_eq!(visible_query_range.start, lsp_initial_visible_range.start);
+ assert_eq!(visible_query_range.end, lsp_initial_visible_range.end);
+ let invisible_query_range = &ranges[1];
+
+ assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document");
+ assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document");
+
+ let requests_count = lsp_request_count.load(Ordering::Acquire);
+ assert_eq!(requests_count, 2, "Visible + invisible request");
+ let expected_hints = vec!["1".to_string(), "2".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should have hints from both LSP requests made for a big file"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range");
+ assert_eq!(
+ editor.inlay_hint_cache().version, requests_count,
+ "LSP queries should've bumped the cache version"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+ editor.scroll_screen(&ScrollAmount::Page(1.0), cx);
+ });
+ cx.foreground().advance_clock(Duration::from_millis(
+ INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+ ));
+ cx.foreground().run_until_parked();
+ let visible_range_after_scrolls = editor_visible_range(&editor, cx);
+ let visible_line_count =
+ editor.update(cx, |editor, _| editor.visible_line_count().unwrap());
+ let selection_in_cached_range = editor.update(cx, |editor, cx| {
+ let ranges = lsp_request_ranges
+ .lock()
+ .drain(..)
+ .sorted_by_key(|r| r.start)
+ .collect::<Vec<_>>();
+ assert_eq!(
+ ranges.len(),
+ 2,
+ "Should query 2 ranges after both scrolls, but got: {ranges:?}"
+ );
+ let first_scroll = &ranges[0];
+ let second_scroll = &ranges[1];
+ assert_eq!(
+ first_scroll.end, second_scroll.start,
+ "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}"
+ );
+ assert_eq!(
+ first_scroll.start, expected_initial_query_range_end,
+ "First scroll should start the query right after the end of the original scroll",
+ );
+ assert_eq!(
+ second_scroll.end,
+ lsp::Position::new(
+ visible_range_after_scrolls.end.row
+ + visible_line_count.ceil() as u32,
+ 1,
+ ),
+ "Second scroll should query one more screen down after the end of the visible range"
+ );
+
+ let lsp_requests = lsp_request_count.load(Ordering::Acquire);
+ assert_eq!(lsp_requests, 4, "Should query for hints after every scroll");
+ let expected_hints = vec![
+ "1".to_string(),
+ "2".to_string(),
+ "3".to_string(),
+ "4".to_string(),
+ ];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should have hints from the new LSP response after the edit"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ lsp_requests,
+ "Should update the cache for every LSP response with hints added"
+ );
+
+ let mut selection_in_cached_range = visible_range_after_scrolls.end;
+ selection_in_cached_range.row -= visible_line_count.ceil() as u32;
+ selection_in_cached_range
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::center()), cx, |s| {
+ s.select_ranges([selection_in_cached_range..selection_in_cached_range])
+ });
+ });
+ cx.foreground().advance_clock(Duration::from_millis(
+ INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+ ));
+ cx.foreground().run_until_parked();
+ editor.update(cx, |_, _| {
+ let ranges = lsp_request_ranges
+ .lock()
+ .drain(..)
+ .sorted_by_key(|r| r.start)
+ .collect::<Vec<_>>();
+ assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints");
+ assert_eq!(lsp_request_count.load(Ordering::Acquire), 4);
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.handle_input("++++more text++++", cx);
+ });
+ cx.foreground().advance_clock(Duration::from_millis(
+ INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+ ));
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let mut ranges = lsp_request_ranges.lock().drain(..).collect::<Vec<_>>();
+ ranges.sort_by_key(|r| r.start);
+
+ assert_eq!(ranges.len(), 3,
+ "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}");
+ let above_query_range = &ranges[0];
+ let visible_query_range = &ranges[1];
+ let below_query_range = &ranges[2];
+ assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line,
+ "Above range {above_query_range:?} should be before visible range {visible_query_range:?}");
+ assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line,
+ "Visible range {visible_query_range:?} should be before below range {below_query_range:?}");
+ assert!(above_query_range.start.line < selection_in_cached_range.row,
+ "Hints should be queried with the selected range after the query range start");
+ assert!(below_query_range.end.line > selection_in_cached_range.row,
+ "Hints should be queried with the selected range before the query range end");
+ assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32,
+ "Hints query range should contain one more screen before");
+ assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32,
+ "Hints query range should contain one more screen after");
+
+ let lsp_requests = lsp_request_count.load(Ordering::Acquire);
+ assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried");
+ let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()];
+ assert_eq!(expected_hints, cached_hint_labels(editor),
+ "Should have hints from the new LSP response after the edit");
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added");
+ });
+ }
+
+ #[gpui::test(iterations = 10)]
+ async fn test_multiple_excerpts_large_multibuffer(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+ "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::clone(&language))
+ });
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "main.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "other.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(4, 0)..Point::new(11, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(22, 0)..Point::new(33, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(44, 0)..Point::new(55, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(56, 0)..Point::new(66, 0),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(67, 0)..Point::new(77, 0),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 1)..Point::new(2, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(4, 1)..Point::new(11, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(22, 1)..Point::new(33, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(44, 1)..Point::new(55, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(56, 1)..Point::new(66, 1),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(67, 1)..Point::new(77, 1),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
+
+ deterministic.run_until_parked();
+ cx.foreground().run_until_parked();
+ let editor = cx
+ .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+ .root(cx);
+ let editor_edited = Arc::new(AtomicBool::new(false));
+ let fake_server = fake_servers.next().await.unwrap();
+ let closure_editor_edited = Arc::clone(&editor_edited);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_editor_edited = Arc::clone(&closure_editor_edited);
+ async move {
+ let hint_text = if params.text_document.uri
+ == lsp::Url::from_file_path("/a/main.rs").unwrap()
+ {
+ "main hint"
+ } else if params.text_document.uri
+ == lsp::Url::from_file_path("/a/other.rs").unwrap()
+ {
+ "other hint"
+ } else {
+ panic!("unexpected uri: {:?}", params.text_document.uri);
+ };
+
+ // one hint per excerpt
+ let positions = [
+ lsp::Position::new(0, 2),
+ lsp::Position::new(4, 2),
+ lsp::Position::new(22, 2),
+ lsp::Position::new(44, 2),
+ lsp::Position::new(56, 2),
+ lsp::Position::new(67, 2),
+ ];
+ let out_of_range_hint = lsp::InlayHint {
+ position: lsp::Position::new(
+ params.range.start.line + 99,
+ params.range.start.character + 99,
+ ),
+ label: lsp::InlayHintLabel::String(
+ "out of excerpt range, should be ignored".to_string(),
+ ),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ };
+
+ let edited = task_editor_edited.load(Ordering::Acquire);
+ Ok(Some(
+ std::iter::once(out_of_range_hint)
+ .chain(positions.into_iter().enumerate().map(|(i, position)| {
+ lsp::InlayHint {
+ position,
+ label: lsp::InlayHintLabel::String(format!(
+ "{hint_text}{} #{i}",
+ if edited { "(edited)" } else { "" },
+ )),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }
+ }))
+ .collect(),
+ ))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ ];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison");
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+ });
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
+ });
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ ];
+ assert_eq!(expected_hints, cached_hint_labels(editor),
+ "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits");
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(),
+ "Due to every excerpt having one hint, we update cache per new excerpt scrolled");
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
+ });
+ });
+ cx.foreground().advance_clock(Duration::from_millis(
+ INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100,
+ ));
+ cx.foreground().run_until_parked();
+ let last_scroll_update_version = editor.update(cx, |editor, cx| {
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_hints, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched");
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, expected_hints.len());
+ expected_hints.len()
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(Some(Autoscroll::Next), cx, |s| {
+ s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
+ });
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec![
+ "main hint #0".to_string(),
+ "main hint #1".to_string(),
+ "main hint #2".to_string(),
+ "main hint #3".to_string(),
+ "main hint #4".to_string(),
+ "main hint #5".to_string(),
+ "other hint #0".to_string(),
+ "other hint #1".to_string(),
+ "other hint #2".to_string(),
+ "other hint #3".to_string(),
+ "other hint #4".to_string(),
+ "other hint #5".to_string(),
+ ];
+ assert_eq!(expected_hints, cached_hint_labels(editor),
+ "After multibuffer was scrolled to the end, further scrolls up should not bring more hints");
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer");
+ });
+
+ editor_edited.store(true, Ordering::Release);
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(56, 0)..Point::new(56, 0)])
+ });
+ editor.handle_input("++++more text++++", cx);
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec![
+ "main hint(edited) #0".to_string(),
+ "main hint(edited) #1".to_string(),
+ "main hint(edited) #2".to_string(),
+ "main hint(edited) #3".to_string(),
+ "main hint(edited) #4".to_string(),
+ "main hint(edited) #5".to_string(),
+ "other hint(edited) #0".to_string(),
+ "other hint(edited) #1".to_string(),
+ ];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "After multibuffer edit, editor gets scolled back to the last selection; \
+all hints should be invalidated and requeried for all of its visible excerpts"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+
+ let current_cache_version = editor.inlay_hint_cache().version;
+ let minimum_expected_version = last_scroll_update_version + expected_hints.len();
+ assert!(
+ current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1,
+ "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_excerpts_removed(
+ deterministic: Arc<Deterministic>,
+ cx: &mut gpui::TestAppContext,
+ ) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: false,
+ show_parameter_hints: false,
+ show_other_hints: false,
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let language = Arc::new(language);
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::<Vec<_>>().join("")),
+ "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::<Vec<_>>().join("")),
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| {
+ project.languages().add(Arc::clone(&language))
+ });
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let buffer_1 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "main.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let buffer_2 = project
+ .update(cx, |project, cx| {
+ project.open_buffer((worktree_id, "other.rs"), cx)
+ })
+ .await
+ .unwrap();
+ let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
+ let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| {
+ let buffer_1_excerpts = multibuffer.push_excerpts(
+ buffer_1.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 0)..Point::new(2, 0),
+ primary: None,
+ }],
+ cx,
+ );
+ let buffer_2_excerpts = multibuffer.push_excerpts(
+ buffer_2.clone(),
+ [ExcerptRange {
+ context: Point::new(0, 1)..Point::new(2, 1),
+ primary: None,
+ }],
+ cx,
+ );
+ (buffer_1_excerpts, buffer_2_excerpts)
+ });
+
+ assert!(!buffer_1_excerpts.is_empty());
+ assert!(!buffer_2_excerpts.is_empty());
+
+ deterministic.run_until_parked();
+ cx.foreground().run_until_parked();
+ let editor = cx
+ .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx))
+ .root(cx);
+ let editor_edited = Arc::new(AtomicBool::new(false));
+ let fake_server = fake_servers.next().await.unwrap();
+ let closure_editor_edited = Arc::clone(&editor_edited);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_editor_edited = Arc::clone(&closure_editor_edited);
+ async move {
+ let hint_text = if params.text_document.uri
+ == lsp::Url::from_file_path("/a/main.rs").unwrap()
+ {
+ "main hint"
+ } else if params.text_document.uri
+ == lsp::Url::from_file_path("/a/other.rs").unwrap()
+ {
+ "other hint"
+ } else {
+ panic!("unexpected uri: {:?}", params.text_document.uri);
+ };
+
+ let positions = [
+ lsp::Position::new(0, 2),
+ lsp::Position::new(4, 2),
+ lsp::Position::new(22, 2),
+ lsp::Position::new(44, 2),
+ lsp::Position::new(56, 2),
+ lsp::Position::new(67, 2),
+ ];
+ let out_of_range_hint = lsp::InlayHint {
+ position: lsp::Position::new(
+ params.range.start.line + 99,
+ params.range.start.character + 99,
+ ),
+ label: lsp::InlayHintLabel::String(
+ "out of excerpt range, should be ignored".to_string(),
+ ),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ };
+
+ let edited = task_editor_edited.load(Ordering::Acquire);
+ Ok(Some(
+ std::iter::once(out_of_range_hint)
+ .chain(positions.into_iter().enumerate().map(|(i, position)| {
+ lsp::InlayHint {
+ position,
+ label: lsp::InlayHintLabel::String(format!(
+ "{hint_text}{} #{i}",
+ if edited { "(edited)" } else { "" },
+ )),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }
+ }))
+ .collect(),
+ ))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ vec!["main hint #0".to_string(), "other hint #0".to_string()],
+ cached_hint_labels(editor),
+ "Cache should update for both excerpts despite hints display was disabled"
+ );
+ assert!(
+ visible_hint_labels(editor, cx).is_empty(),
+ "All hints are disabled and should not be shown despite being present in the cache"
+ );
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ 2,
+ "Cache should update once per excerpt query"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.buffer().update(cx, |multibuffer, cx| {
+ multibuffer.remove_excerpts(buffer_2_excerpts, cx)
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert_eq!(
+ vec!["main hint #0".to_string()],
+ cached_hint_labels(editor),
+ "For the removed excerpt, should clean corresponding cached hints"
+ );
+ assert!(
+ visible_hint_labels(editor, cx).is_empty(),
+ "All hints are disabled and should not be shown despite being present in the cache"
+ );
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ 3,
+ "Excerpt removal should trigger a cache update"
+ );
+ });
+
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["main hint #0".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Hint display settings change should not change the cache"
+ );
+ assert_eq!(
+ expected_hints,
+ visible_hint_labels(editor, cx),
+ "Settings change should make cached hints visible"
+ );
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ 4,
+ "Settings change should trigger a cache update"
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_inside_char_boundary_range_hints(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "โ".repeat(10)).repeat(500)),
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path("/a/main.rs").unwrap(),
+ );
+ let query_start = params.range.start;
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1;
+ Ok(Some(vec![lsp::InlayHint {
+ position: query_start,
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ editor.change_selections(None, cx, |s| {
+ s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["1".to_string()];
+ assert_eq!(expected_hints, cached_hint_labels(editor));
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, 1);
+ });
+ }
+
+ #[gpui::test]
+ async fn test_toggle_inlay_hints(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: false,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let (file_with_hints, editor, fake_server) = prepare_test_objects(cx).await;
+
+ editor.update(cx, |editor, cx| {
+ editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+ });
+ cx.foreground().start_waiting();
+ let lsp_request_count = Arc::new(AtomicU32::new(0));
+ let closure_lsp_request_count = Arc::clone(&lsp_request_count);
+ fake_server
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let task_lsp_request_count = Arc::clone(&closure_lsp_request_count);
+ async move {
+ assert_eq!(
+ params.text_document.uri,
+ lsp::Url::from_file_path(file_with_hints).unwrap(),
+ );
+
+ let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::SeqCst) + 1;
+ Ok(Some(vec![lsp::InlayHint {
+ position: lsp::Position::new(0, i),
+ label: lsp::InlayHintLabel::String(i.to_string()),
+ kind: None,
+ text_edits: None,
+ tooltip: None,
+ padding_left: None,
+ padding_right: None,
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["1".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should display inlays after toggle despite them disabled in settings"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(
+ editor.inlay_hint_cache().version,
+ 1,
+ "First toggle should be cache's first update"
+ );
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert!(
+ cached_hint_labels(editor).is_empty(),
+ "Should clear hints after 2nd toggle"
+ );
+ assert!(visible_hint_labels(editor, cx).is_empty());
+ assert_eq!(editor.inlay_hint_cache().version, 2);
+ });
+
+ update_test_language_settings(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["2".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should query LSP hints for the 2nd time after enabling hints in settings"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, 3);
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ assert!(
+ cached_hint_labels(editor).is_empty(),
+ "Should clear hints after enabling in settings and a 3rd toggle"
+ );
+ assert!(visible_hint_labels(editor, cx).is_empty());
+ assert_eq!(editor.inlay_hint_cache().version, 4);
+ });
+
+ editor.update(cx, |editor, cx| {
+ editor.toggle_inlay_hints(&crate::ToggleInlayHints, cx)
+ });
+ cx.foreground().run_until_parked();
+ editor.update(cx, |editor, cx| {
+ let expected_hints = vec!["3".to_string()];
+ assert_eq!(
+ expected_hints,
+ cached_hint_labels(editor),
+ "Should query LSP hints for the 3rd time after enabling hints in settings and toggling them back on"
+ );
+ assert_eq!(expected_hints, visible_hint_labels(editor, cx));
+ assert_eq!(editor.inlay_hint_cache().version, 5);
+ });
+ }
+
+ pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) {
+ cx.foreground().forbid_parking();
+
+ cx.update(|cx| {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ client::init_settings(cx);
+ language::init(cx);
+ Project::init_settings(cx);
+ workspace::init_settings(cx);
+ crate::init(cx);
+ });
+
+ update_test_language_settings(cx, f);
+ }
+
+ async fn prepare_test_objects(
+ cx: &mut TestAppContext,
+ ) -> (&'static str, ViewHandle<Editor>, FakeLanguageServer) {
+ let mut language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ );
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities: lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ ..Default::default()
+ }))
+ .await;
+
+ let fs = FakeFs::new(cx.background());
+ fs.insert_tree(
+ "/a",
+ json!({
+ "main.rs": "fn main() { a } // and some long comment to ensure inlays are not trimmed out",
+ "other.rs": "// Test file",
+ }),
+ )
+ .await;
+
+ let project = Project::test(fs, ["/a".as_ref()], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+ let workspace = cx
+ .add_window(|cx| Workspace::test_new(project.clone(), cx))
+ .root(cx);
+ let worktree_id = workspace.update(cx, |workspace, cx| {
+ workspace.project().read_with(cx, |project, cx| {
+ project.worktrees(cx).next().unwrap().read(cx).id()
+ })
+ });
+
+ let _buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/a/main.rs", cx)
+ })
+ .await
+ .unwrap();
+ cx.foreground().run_until_parked();
+ cx.foreground().start_waiting();
+ let fake_server = fake_servers.next().await.unwrap();
+ let editor = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path((worktree_id, "main.rs"), None, true, cx)
+ })
+ .await
+ .unwrap()
+ .downcast::<Editor>()
+ .unwrap();
+
+ editor.update(cx, |editor, cx| {
+ assert!(cached_hint_labels(editor).is_empty());
+ assert!(visible_hint_labels(editor, cx).is_empty());
+ assert_eq!(editor.inlay_hint_cache().version, 0);
+ });
+
+ ("/a/main.rs", editor, fake_server)
+ }
+
+ pub fn cached_hint_labels(editor: &Editor) -> Vec<String> {
+ let mut labels = Vec::new();
+ for (_, excerpt_hints) in &editor.inlay_hint_cache().hints {
+ let excerpt_hints = excerpt_hints.read();
+ for id in &excerpt_hints.ordered_hints {
+ labels.push(excerpt_hints.hints_by_id[id].text());
+ }
+ }
+
+ labels.sort();
+ labels
+ }
+
+ pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec<String> {
+ let mut hints = editor
+ .visible_inlay_hints(cx)
+ .into_iter()
+ .map(|hint| hint.text.to_string())
+ .collect::<Vec<_>>();
+ hints.sort();
+ hints
+ }
+}
@@ -0,0 +1,1327 @@
+use crate::{
+ display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
+ movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
+ Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
+};
+use anyhow::{Context, Result};
+use collections::HashSet;
+use futures::future::try_join_all;
+use gpui::{
+ elements::*,
+ geometry::vector::{vec2f, Vector2F},
+ AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext,
+ ViewHandle, WeakViewHandle,
+};
+use language::{
+ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point,
+ SelectionGoal,
+};
+use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
+use rpc::proto::{self, update_view, PeerId};
+use smallvec::SmallVec;
+use std::{
+ borrow::Cow,
+ cmp::{self, Ordering},
+ fmt::Write,
+ iter,
+ ops::Range,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use text::Selection;
+use util::{
+ paths::{PathExt, FILE_ROW_COLUMN_DELIMITER},
+ ResultExt, TryFutureExt,
+};
+use workspace::item::{BreadcrumbText, FollowableItemHandle};
+use workspace::{
+ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
+ searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
+ ItemId, ItemNavHistory, Pane, StatusItemView, ToolbarItemLocation, ViewId, Workspace,
+ WorkspaceId,
+};
+
+pub const MAX_TAB_TITLE_LEN: usize = 24;
+
+impl FollowableItem for Editor {
+ fn remote_id(&self) -> Option<ViewId> {
+ self.remote_id
+ }
+
+ fn from_state_proto(
+ pane: ViewHandle<workspace::Pane>,
+ workspace: ViewHandle<Workspace>,
+ remote_id: ViewId,
+ state: &mut Option<proto::view::Variant>,
+ cx: &mut AppContext,
+ ) -> Option<Task<Result<ViewHandle<Self>>>> {
+ let project = workspace.read(cx).project().to_owned();
+ let Some(proto::view::Variant::Editor(_)) = state else {
+ return None;
+ };
+ let Some(proto::view::Variant::Editor(state)) = state.take() else {
+ unreachable!()
+ };
+
+ let client = project.read(cx).client();
+ let replica_id = project.read(cx).replica_id();
+ let buffer_ids = state
+ .excerpts
+ .iter()
+ .map(|excerpt| excerpt.buffer_id)
+ .collect::<HashSet<_>>();
+ let buffers = project.update(cx, |project, cx| {
+ buffer_ids
+ .iter()
+ .map(|id| project.open_buffer_by_id(*id, cx))
+ .collect::<Vec<_>>()
+ });
+
+ let pane = pane.downgrade();
+ Some(cx.spawn(|mut cx| async move {
+ let mut buffers = futures::future::try_join_all(buffers).await?;
+ let editor = pane.read_with(&cx, |pane, cx| {
+ let mut editors = pane.items_of_type::<Self>();
+ editors.find(|editor| {
+ let ids_match = editor.remote_id(&client, cx) == Some(remote_id);
+ let singleton_buffer_matches = state.singleton
+ && buffers.first()
+ == editor.read(cx).buffer.read(cx).as_singleton().as_ref();
+ ids_match || singleton_buffer_matches
+ })
+ })?;
+
+ let editor = if let Some(editor) = editor {
+ editor
+ } else {
+ pane.update(&mut cx, |_, cx| {
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer;
+ if state.singleton && buffers.len() == 1 {
+ multibuffer = MultiBuffer::singleton(buffers.pop().unwrap(), cx)
+ } else {
+ multibuffer = MultiBuffer::new(replica_id);
+ let mut excerpts = state.excerpts.into_iter().peekable();
+ while let Some(excerpt) = excerpts.peek() {
+ let buffer_id = excerpt.buffer_id;
+ let buffer_excerpts = iter::from_fn(|| {
+ let excerpt = excerpts.peek()?;
+ (excerpt.buffer_id == buffer_id)
+ .then(|| excerpts.next().unwrap())
+ });
+ let buffer =
+ buffers.iter().find(|b| b.read(cx).remote_id() == buffer_id);
+ if let Some(buffer) = buffer {
+ multibuffer.push_excerpts(
+ buffer.clone(),
+ buffer_excerpts.filter_map(deserialize_excerpt_range),
+ cx,
+ );
+ }
+ }
+ };
+
+ if let Some(title) = &state.title {
+ multibuffer = multibuffer.with_title(title.clone())
+ }
+
+ multibuffer
+ });
+
+ cx.add_view(|cx| {
+ let mut editor =
+ Editor::for_multibuffer(multibuffer, Some(project.clone()), cx);
+ editor.remote_id = Some(remote_id);
+ editor
+ })
+ })?
+ };
+
+ update_editor_from_message(
+ editor.downgrade(),
+ project,
+ proto::update_view::Editor {
+ selections: state.selections,
+ pending_selection: state.pending_selection,
+ scroll_top_anchor: state.scroll_top_anchor,
+ scroll_x: state.scroll_x,
+ scroll_y: state.scroll_y,
+ ..Default::default()
+ },
+ &mut cx,
+ )
+ .await?;
+
+ Ok(editor)
+ }))
+ }
+
+ fn set_leader_peer_id(&mut self, leader_peer_id: Option<PeerId>, cx: &mut ViewContext<Self>) {
+ self.leader_peer_id = leader_peer_id;
+ if self.leader_peer_id.is_some() {
+ self.buffer.update(cx, |buffer, cx| {
+ buffer.remove_active_selections(cx);
+ });
+ } else {
+ self.buffer.update(cx, |buffer, cx| {
+ if self.focused {
+ buffer.set_active_selections(
+ &self.selections.disjoint_anchors(),
+ self.selections.line_mode,
+ self.cursor_shape,
+ cx,
+ );
+ }
+ });
+ }
+ cx.notify();
+ }
+
+ fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
+ let buffer = self.buffer.read(cx);
+ let scroll_anchor = self.scroll_manager.anchor();
+ let excerpts = buffer
+ .read(cx)
+ .excerpts()
+ .map(|(id, buffer, range)| proto::Excerpt {
+ id: id.to_proto(),
+ buffer_id: buffer.remote_id(),
+ context_start: Some(serialize_text_anchor(&range.context.start)),
+ context_end: Some(serialize_text_anchor(&range.context.end)),
+ primary_start: range
+ .primary
+ .as_ref()
+ .map(|range| serialize_text_anchor(&range.start)),
+ primary_end: range
+ .primary
+ .as_ref()
+ .map(|range| serialize_text_anchor(&range.end)),
+ })
+ .collect();
+
+ Some(proto::view::Variant::Editor(proto::view::Editor {
+ singleton: buffer.is_singleton(),
+ title: (!buffer.is_singleton()).then(|| buffer.title(cx).into()),
+ excerpts,
+ scroll_top_anchor: Some(serialize_anchor(&scroll_anchor.anchor)),
+ scroll_x: scroll_anchor.offset.x(),
+ scroll_y: scroll_anchor.offset.y(),
+ selections: self
+ .selections
+ .disjoint_anchors()
+ .iter()
+ .map(serialize_selection)
+ .collect(),
+ pending_selection: self
+ .selections
+ .pending_anchor()
+ .as_ref()
+ .map(serialize_selection),
+ }))
+ }
+
+ fn add_event_to_update_proto(
+ &self,
+ event: &Self::Event,
+ update: &mut Option<proto::update_view::Variant>,
+ cx: &AppContext,
+ ) -> bool {
+ let update =
+ update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default()));
+
+ match update {
+ proto::update_view::Variant::Editor(update) => match event {
+ Event::ExcerptsAdded {
+ buffer,
+ predecessor,
+ excerpts,
+ } => {
+ let buffer_id = buffer.read(cx).remote_id();
+ let mut excerpts = excerpts.iter();
+ if let Some((id, range)) = excerpts.next() {
+ update.inserted_excerpts.push(proto::ExcerptInsertion {
+ previous_excerpt_id: Some(predecessor.to_proto()),
+ excerpt: serialize_excerpt(buffer_id, id, range),
+ });
+ update.inserted_excerpts.extend(excerpts.map(|(id, range)| {
+ proto::ExcerptInsertion {
+ previous_excerpt_id: None,
+ excerpt: serialize_excerpt(buffer_id, id, range),
+ }
+ }))
+ }
+ true
+ }
+ Event::ExcerptsRemoved { ids } => {
+ update
+ .deleted_excerpts
+ .extend(ids.iter().map(ExcerptId::to_proto));
+ true
+ }
+ Event::ScrollPositionChanged { .. } => {
+ let scroll_anchor = self.scroll_manager.anchor();
+ update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor));
+ update.scroll_x = scroll_anchor.offset.x();
+ update.scroll_y = scroll_anchor.offset.y();
+ true
+ }
+ Event::SelectionsChanged { .. } => {
+ update.selections = self
+ .selections
+ .disjoint_anchors()
+ .iter()
+ .map(serialize_selection)
+ .collect();
+ update.pending_selection = self
+ .selections
+ .pending_anchor()
+ .as_ref()
+ .map(serialize_selection);
+ true
+ }
+ _ => false,
+ },
+ }
+ }
+
+ fn apply_update_proto(
+ &mut self,
+ project: &ModelHandle<Project>,
+ message: update_view::Variant,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ let update_view::Variant::Editor(message) = message;
+ let project = project.clone();
+ cx.spawn(|this, mut cx| async move {
+ update_editor_from_message(this, project, message, &mut cx).await
+ })
+ }
+
+ fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool {
+ match event {
+ Event::Edited => true,
+ Event::SelectionsChanged { local } => *local,
+ Event::ScrollPositionChanged { local, .. } => *local,
+ _ => false,
+ }
+ }
+
+ fn is_project_item(&self, _cx: &AppContext) -> bool {
+ true
+ }
+}
+
+async fn update_editor_from_message(
+ this: WeakViewHandle<Editor>,
+ project: ModelHandle<Project>,
+ message: proto::update_view::Editor,
+ cx: &mut AsyncAppContext,
+) -> Result<()> {
+ // Open all of the buffers of which excerpts were added to the editor.
+ let inserted_excerpt_buffer_ids = message
+ .inserted_excerpts
+ .iter()
+ .filter_map(|insertion| Some(insertion.excerpt.as_ref()?.buffer_id))
+ .collect::<HashSet<_>>();
+ let inserted_excerpt_buffers = project.update(cx, |project, cx| {
+ inserted_excerpt_buffer_ids
+ .into_iter()
+ .map(|id| project.open_buffer_by_id(id, cx))
+ .collect::<Vec<_>>()
+ });
+ let _inserted_excerpt_buffers = try_join_all(inserted_excerpt_buffers).await?;
+
+ // Update the editor's excerpts.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |multibuffer, cx| {
+ let mut removed_excerpt_ids = message
+ .deleted_excerpts
+ .into_iter()
+ .map(ExcerptId::from_proto)
+ .collect::<Vec<_>>();
+ removed_excerpt_ids.sort_by({
+ let multibuffer = multibuffer.read(cx);
+ move |a, b| a.cmp(&b, &multibuffer)
+ });
+
+ let mut insertions = message.inserted_excerpts.into_iter().peekable();
+ while let Some(insertion) = insertions.next() {
+ let Some(excerpt) = insertion.excerpt else {
+ continue;
+ };
+ let Some(previous_excerpt_id) = insertion.previous_excerpt_id else {
+ continue;
+ };
+ let buffer_id = excerpt.buffer_id;
+ let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
+ continue;
+ };
+
+ let adjacent_excerpts = iter::from_fn(|| {
+ let insertion = insertions.peek()?;
+ if insertion.previous_excerpt_id.is_none()
+ && insertion.excerpt.as_ref()?.buffer_id == buffer_id
+ {
+ insertions.next()?.excerpt
+ } else {
+ None
+ }
+ });
+
+ multibuffer.insert_excerpts_with_ids_after(
+ ExcerptId::from_proto(previous_excerpt_id),
+ buffer,
+ [excerpt]
+ .into_iter()
+ .chain(adjacent_excerpts)
+ .filter_map(|excerpt| {
+ Some((
+ ExcerptId::from_proto(excerpt.id),
+ deserialize_excerpt_range(excerpt)?,
+ ))
+ }),
+ cx,
+ );
+ }
+
+ multibuffer.remove_excerpts(removed_excerpt_ids, cx);
+ });
+ })?;
+
+ // Deserialize the editor state.
+ let (selections, pending_selection, scroll_top_anchor) = this.update(cx, |editor, cx| {
+ let buffer = editor.buffer.read(cx).read(cx);
+ let selections = message
+ .selections
+ .into_iter()
+ .filter_map(|selection| deserialize_selection(&buffer, selection))
+ .collect::<Vec<_>>();
+ let pending_selection = message
+ .pending_selection
+ .and_then(|selection| deserialize_selection(&buffer, selection));
+ let scroll_top_anchor = message
+ .scroll_top_anchor
+ .and_then(|anchor| deserialize_anchor(&buffer, anchor));
+ anyhow::Ok((selections, pending_selection, scroll_top_anchor))
+ })??;
+
+ // Wait until the buffer has received all of the operations referenced by
+ // the editor's new state.
+ this.update(cx, |editor, cx| {
+ editor.buffer.update(cx, |buffer, cx| {
+ buffer.wait_for_anchors(
+ selections
+ .iter()
+ .chain(pending_selection.as_ref())
+ .flat_map(|selection| [selection.start, selection.end])
+ .chain(scroll_top_anchor),
+ cx,
+ )
+ })
+ })?
+ .await?;
+
+ // Update the editor's state.
+ this.update(cx, |editor, cx| {
+ if !selections.is_empty() || pending_selection.is_some() {
+ editor.set_selections_from_remote(selections, pending_selection, cx);
+ editor.request_autoscroll_remotely(Autoscroll::newest(), cx);
+ } else if let Some(scroll_top_anchor) = scroll_top_anchor {
+ editor.set_scroll_anchor_remote(
+ ScrollAnchor {
+ anchor: scroll_top_anchor,
+ offset: vec2f(message.scroll_x, message.scroll_y),
+ },
+ cx,
+ );
+ }
+ })?;
+ Ok(())
+}
+
+fn serialize_excerpt(
+ buffer_id: u64,
+ id: &ExcerptId,
+ range: &ExcerptRange<language::Anchor>,
+) -> Option<proto::Excerpt> {
+ Some(proto::Excerpt {
+ id: id.to_proto(),
+ buffer_id,
+ context_start: Some(serialize_text_anchor(&range.context.start)),
+ context_end: Some(serialize_text_anchor(&range.context.end)),
+ primary_start: range
+ .primary
+ .as_ref()
+ .map(|r| serialize_text_anchor(&r.start)),
+ primary_end: range
+ .primary
+ .as_ref()
+ .map(|r| serialize_text_anchor(&r.end)),
+ })
+}
+
+fn serialize_selection(selection: &Selection<Anchor>) -> proto::Selection {
+ proto::Selection {
+ id: selection.id as u64,
+ start: Some(serialize_anchor(&selection.start)),
+ end: Some(serialize_anchor(&selection.end)),
+ reversed: selection.reversed,
+ }
+}
+
+fn serialize_anchor(anchor: &Anchor) -> proto::EditorAnchor {
+ proto::EditorAnchor {
+ excerpt_id: anchor.excerpt_id.to_proto(),
+ anchor: Some(serialize_text_anchor(&anchor.text_anchor)),
+ }
+}
+
+fn deserialize_excerpt_range(excerpt: proto::Excerpt) -> Option<ExcerptRange<language::Anchor>> {
+ let context = {
+ let start = language::proto::deserialize_anchor(excerpt.context_start?)?;
+ let end = language::proto::deserialize_anchor(excerpt.context_end?)?;
+ start..end
+ };
+ let primary = excerpt
+ .primary_start
+ .zip(excerpt.primary_end)
+ .and_then(|(start, end)| {
+ let start = language::proto::deserialize_anchor(start)?;
+ let end = language::proto::deserialize_anchor(end)?;
+ Some(start..end)
+ });
+ Some(ExcerptRange { context, primary })
+}
+
+fn deserialize_selection(
+ buffer: &MultiBufferSnapshot,
+ selection: proto::Selection,
+) -> Option<Selection<Anchor>> {
+ Some(Selection {
+ id: selection.id as usize,
+ start: deserialize_anchor(buffer, selection.start?)?,
+ end: deserialize_anchor(buffer, selection.end?)?,
+ reversed: selection.reversed,
+ goal: SelectionGoal::None,
+ })
+}
+
+fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) -> Option<Anchor> {
+ let excerpt_id = ExcerptId::from_proto(anchor.excerpt_id);
+ Some(Anchor {
+ excerpt_id,
+ text_anchor: language::proto::deserialize_anchor(anchor.anchor?)?,
+ buffer_id: buffer.buffer_id_for_excerpt(excerpt_id),
+ })
+}
+
+impl Item for Editor {
+ fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
+ if let Ok(data) = data.downcast::<NavigationData>() {
+ let newest_selection = self.selections.newest::<Point>(cx);
+ let buffer = self.buffer.read(cx).read(cx);
+ let offset = if buffer.can_resolve(&data.cursor_anchor) {
+ data.cursor_anchor.to_point(&buffer)
+ } else {
+ buffer.clip_point(data.cursor_position, Bias::Left)
+ };
+
+ let mut scroll_anchor = data.scroll_anchor;
+ if !buffer.can_resolve(&scroll_anchor.anchor) {
+ scroll_anchor.anchor = buffer.anchor_before(
+ buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
+ );
+ }
+
+ drop(buffer);
+
+ if newest_selection.head() == offset {
+ false
+ } else {
+ let nav_history = self.nav_history.take();
+ self.set_scroll_anchor(scroll_anchor, cx);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([offset..offset])
+ });
+ self.nav_history = nav_history;
+ true
+ }
+ } else {
+ false
+ }
+ }
+
+ fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
+ let file_path = self
+ .buffer()
+ .read(cx)
+ .as_singleton()?
+ .read(cx)
+ .file()
+ .and_then(|f| f.as_local())?
+ .abs_path(cx);
+
+ let file_path = file_path.compact().to_string_lossy().to_string();
+
+ Some(file_path.into())
+ }
+
+ fn tab_description<'a>(&'a self, detail: usize, cx: &'a AppContext) -> Option<Cow<str>> {
+ match path_for_buffer(&self.buffer, detail, true, cx)? {
+ Cow::Borrowed(path) => Some(path.to_string_lossy()),
+ Cow::Owned(path) => Some(path.to_string_lossy().to_string().into()),
+ }
+ }
+
+ fn tab_content<T: 'static>(
+ &self,
+ detail: Option<usize>,
+ style: &theme::Tab,
+ cx: &AppContext,
+ ) -> AnyElement<T> {
+ Flex::row()
+ .with_child(Label::new(self.title(cx).to_string(), style.label.clone()).into_any())
+ .with_children(detail.and_then(|detail| {
+ let path = path_for_buffer(&self.buffer, detail, false, cx)?;
+ let description = path.to_string_lossy();
+ Some(
+ Label::new(
+ util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN),
+ style.description.text.clone(),
+ )
+ .contained()
+ .with_style(style.description.container)
+ .aligned(),
+ )
+ }))
+ .align_children_center()
+ .into_any()
+ }
+
+ fn for_each_project_item(&self, cx: &AppContext, f: &mut dyn FnMut(usize, &dyn project::Item)) {
+ self.buffer
+ .read(cx)
+ .for_each_buffer(|buffer| f(buffer.id(), buffer.read(cx)));
+ }
+
+ fn is_singleton(&self, cx: &AppContext) -> bool {
+ self.buffer.read(cx).is_singleton()
+ }
+
+ fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
+ where
+ Self: Sized,
+ {
+ Some(self.clone(cx))
+ }
+
+ fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
+ self.nav_history = Some(history);
+ }
+
+ fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ let selection = self.selections.newest_anchor();
+ self.push_to_nav_history(selection.head(), None, cx);
+ }
+
+ fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
+ hide_link_definition(self, cx);
+ self.link_go_to_definition_state.last_trigger_point = None;
+ }
+
+ fn is_dirty(&self, cx: &AppContext) -> bool {
+ self.buffer().read(cx).read(cx).is_dirty()
+ }
+
+ fn has_conflict(&self, cx: &AppContext) -> bool {
+ self.buffer().read(cx).read(cx).has_conflict()
+ }
+
+ fn can_save(&self, cx: &AppContext) -> bool {
+ let buffer = &self.buffer().read(cx);
+ if let Some(buffer) = buffer.as_singleton() {
+ buffer.read(cx).project_path(cx).is_some()
+ } else {
+ true
+ }
+ }
+
+ fn save(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ self.report_editor_event("save", None, cx);
+ let format = self.perform_format(project.clone(), FormatTrigger::Save, cx);
+ let buffers = self.buffer().clone().read(cx).all_buffers();
+ cx.spawn(|_, mut cx| async move {
+ format.await?;
+
+ if buffers.len() == 1 {
+ project
+ .update(&mut cx, |project, cx| project.save_buffers(buffers, cx))
+ .await?;
+ } else {
+ // For multi-buffers, only save those ones that contain changes. For clean buffers
+ // we simulate saving by calling `Buffer::did_save`, so that language servers or
+ // other downstream listeners of save events get notified.
+ let (dirty_buffers, clean_buffers) = buffers.into_iter().partition(|buffer| {
+ buffer.read_with(&cx, |buffer, _| buffer.is_dirty() || buffer.has_conflict())
+ });
+
+ project
+ .update(&mut cx, |project, cx| {
+ project.save_buffers(dirty_buffers, cx)
+ })
+ .await?;
+ for buffer in clean_buffers {
+ buffer.update(&mut cx, |buffer, cx| {
+ let version = buffer.saved_version().clone();
+ let fingerprint = buffer.saved_version_fingerprint();
+ let mtime = buffer.saved_mtime();
+ buffer.did_save(version, fingerprint, mtime, cx);
+ });
+ }
+ }
+
+ Ok(())
+ })
+ }
+
+ fn save_as(
+ &mut self,
+ project: ModelHandle<Project>,
+ abs_path: PathBuf,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ let buffer = self
+ .buffer()
+ .read(cx)
+ .as_singleton()
+ .expect("cannot call save_as on an excerpt list");
+
+ let file_extension = abs_path
+ .extension()
+ .map(|a| a.to_string_lossy().to_string());
+ self.report_editor_event("save", file_extension, cx);
+
+ project.update(cx, |project, cx| {
+ project.save_buffer_as(buffer, abs_path, cx)
+ })
+ }
+
+ fn reload(
+ &mut self,
+ project: ModelHandle<Project>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Result<()>> {
+ let buffer = self.buffer().clone();
+ let buffers = self.buffer.read(cx).all_buffers();
+ let reload_buffers =
+ project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
+ cx.spawn(|this, mut cx| async move {
+ let transaction = reload_buffers.log_err().await;
+ this.update(&mut cx, |editor, cx| {
+ editor.request_autoscroll(Autoscroll::fit(), cx)
+ })?;
+ buffer.update(&mut cx, |buffer, cx| {
+ if let Some(transaction) = transaction {
+ if !buffer.is_singleton() {
+ buffer.push_transaction(&transaction.0, cx);
+ }
+ }
+ });
+ Ok(())
+ })
+ }
+
+ fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
+ let mut result = SmallVec::new();
+ match event {
+ Event::Closed => result.push(ItemEvent::CloseItem),
+ Event::Saved | Event::TitleChanged => {
+ result.push(ItemEvent::UpdateTab);
+ result.push(ItemEvent::UpdateBreadcrumbs);
+ }
+ Event::Reparsed => {
+ result.push(ItemEvent::UpdateBreadcrumbs);
+ }
+ Event::SelectionsChanged { local } if *local => {
+ result.push(ItemEvent::UpdateBreadcrumbs);
+ }
+ Event::DirtyChanged => {
+ result.push(ItemEvent::UpdateTab);
+ }
+ Event::BufferEdited => {
+ result.push(ItemEvent::Edit);
+ result.push(ItemEvent::UpdateBreadcrumbs);
+ }
+ _ => {}
+ }
+ result
+ }
+
+ fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
+ Some(Box::new(handle.clone()))
+ }
+
+ fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
+ self.pixel_position_of_newest_cursor
+ }
+
+ fn breadcrumb_location(&self) -> ToolbarItemLocation {
+ ToolbarItemLocation::PrimaryLeft { flex: None }
+ }
+
+ fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
+ let cursor = self.selections.newest_anchor().head();
+ let multibuffer = &self.buffer().read(cx);
+ let (buffer_id, symbols) =
+ multibuffer.symbols_containing(cursor, Some(&theme.editor.syntax), cx)?;
+ let buffer = multibuffer.buffer(buffer_id)?;
+
+ let buffer = buffer.read(cx);
+ let filename = buffer
+ .snapshot()
+ .resolve_file_path(
+ cx,
+ self.project
+ .as_ref()
+ .map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
+ .unwrap_or_default(),
+ )
+ .map(|path| path.to_string_lossy().to_string())
+ .unwrap_or_else(|| "untitled".to_string());
+
+ let mut breadcrumbs = vec![BreadcrumbText {
+ text: filename,
+ highlights: None,
+ }];
+ breadcrumbs.extend(symbols.into_iter().map(|symbol| BreadcrumbText {
+ text: symbol.text,
+ highlights: Some(symbol.highlight_ranges),
+ }));
+ Some(breadcrumbs)
+ }
+
+ fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
+ let workspace_id = workspace.database_id();
+ let item_id = cx.view_id();
+ self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
+
+ fn serialize(
+ buffer: ModelHandle<Buffer>,
+ workspace_id: WorkspaceId,
+ item_id: ItemId,
+ cx: &mut AppContext,
+ ) {
+ if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
+ let path = file.abs_path(cx);
+
+ cx.background()
+ .spawn(async move {
+ DB.save_path(item_id, workspace_id, path.clone())
+ .await
+ .log_err()
+ })
+ .detach();
+ }
+ }
+
+ if let Some(buffer) = self.buffer().read(cx).as_singleton() {
+ serialize(buffer.clone(), workspace_id, item_id, cx);
+
+ cx.subscribe(&buffer, |this, buffer, event, cx| {
+ if let Some((_, workspace_id)) = this.workspace.as_ref() {
+ if let language::Event::FileHandleChanged = event {
+ serialize(buffer, *workspace_id, cx.view_id(), cx);
+ }
+ }
+ })
+ .detach();
+ }
+ }
+
+ fn serialized_item_kind() -> Option<&'static str> {
+ Some("Editor")
+ }
+
+ fn deserialize(
+ project: ModelHandle<Project>,
+ _workspace: WeakViewHandle<Workspace>,
+ workspace_id: workspace::WorkspaceId,
+ item_id: ItemId,
+ cx: &mut ViewContext<Pane>,
+ ) -> Task<Result<ViewHandle<Self>>> {
+ let project_item: Result<_> = project.update(cx, |project, cx| {
+ // Look up the path with this key associated, create a self with that path
+ let path = DB
+ .get_path(item_id, workspace_id)?
+ .context("No path stored for this editor")?;
+
+ let (worktree, path) = project
+ .find_local_worktree(&path, cx)
+ .with_context(|| format!("No worktree for path: {path:?}"))?;
+ let project_path = ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path: path.into(),
+ };
+
+ Ok(project.open_path(project_path, cx))
+ });
+
+ project_item
+ .map(|project_item| {
+ cx.spawn(|pane, mut cx| async move {
+ let (_, project_item) = project_item.await?;
+ let buffer = project_item
+ .downcast::<Buffer>()
+ .context("Project item at stored path was not a buffer")?;
+ Ok(pane.update(&mut cx, |_, cx| {
+ cx.add_view(|cx| {
+ let mut editor = Editor::for_buffer(buffer, Some(project), cx);
+ editor.read_scroll_position_from_db(item_id, workspace_id, cx);
+ editor
+ })
+ })?)
+ })
+ })
+ .unwrap_or_else(|error| Task::ready(Err(error)))
+ }
+}
+
+impl ProjectItem for Editor {
+ type Item = Buffer;
+
+ fn for_project_item(
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<Buffer>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ Self::for_buffer(buffer, Some(project), cx)
+ }
+}
+
+pub(crate) enum BufferSearchHighlights {}
+impl SearchableItem for Editor {
+ type Match = Range<Anchor>;
+
+ fn to_search_event(
+ &mut self,
+ event: &Self::Event,
+ _: &mut ViewContext<Self>,
+ ) -> Option<SearchEvent> {
+ match event {
+ Event::BufferEdited => Some(SearchEvent::MatchesInvalidated),
+ Event::SelectionsChanged { .. } => {
+ if self.selections.disjoint_anchors().len() == 1 {
+ Some(SearchEvent::ActiveMatchChanged)
+ } else {
+ None
+ }
+ }
+ _ => None,
+ }
+ }
+
+ fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
+ self.clear_background_highlights::<BufferSearchHighlights>(cx);
+ }
+
+ fn update_matches(&mut self, matches: Vec<Range<Anchor>>, cx: &mut ViewContext<Self>) {
+ self.highlight_background::<BufferSearchHighlights>(
+ matches,
+ |theme| theme.search.match_background,
+ cx,
+ );
+ }
+
+ fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
+ let display_map = self.snapshot(cx).display_snapshot;
+ let selection = self.selections.newest::<usize>(cx);
+ if selection.start == selection.end {
+ let point = selection.start.to_display_point(&display_map);
+ let range = surrounding_word(&display_map, point);
+ let range = range.start.to_offset(&display_map, Bias::Left)
+ ..range.end.to_offset(&display_map, Bias::Right);
+ let text: String = display_map.buffer_snapshot.text_for_range(range).collect();
+ if text.trim().is_empty() {
+ String::new()
+ } else {
+ text
+ }
+ } else {
+ display_map
+ .buffer_snapshot
+ .text_for_range(selection.start..selection.end)
+ .collect()
+ }
+ }
+
+ fn activate_match(
+ &mut self,
+ index: usize,
+ matches: Vec<Range<Anchor>>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.unfold_ranges([matches[index].clone()], false, true, cx);
+ let range = self.range_for_match(&matches[index]);
+ self.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges([range]);
+ })
+ }
+
+ fn select_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
+ self.unfold_ranges(matches.clone(), false, false, cx);
+ let mut ranges = Vec::new();
+ for m in &matches {
+ ranges.push(self.range_for_match(&m))
+ }
+ self.change_selections(None, cx, |s| s.select_ranges(ranges));
+ }
+ fn replace(
+ &mut self,
+ identifier: &Self::Match,
+ query: &SearchQuery,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let text = self.buffer.read(cx);
+ let text = text.snapshot(cx);
+ let text = text.text_for_range(identifier.clone()).collect::<Vec<_>>();
+ let text: Cow<_> = if text.len() == 1 {
+ text.first().cloned().unwrap().into()
+ } else {
+ let joined_chunks = text.join("");
+ joined_chunks.into()
+ };
+
+ if let Some(replacement) = query.replacement_for(&text) {
+ self.transact(cx, |this, cx| {
+ this.edit([(identifier.clone(), Arc::from(&*replacement))], cx);
+ });
+ }
+ }
+ fn match_index_for_direction(
+ &mut self,
+ matches: &Vec<Range<Anchor>>,
+ current_index: usize,
+ direction: Direction,
+ count: usize,
+ cx: &mut ViewContext<Self>,
+ ) -> usize {
+ let buffer = self.buffer().read(cx).snapshot(cx);
+ let current_index_position = if self.selections.disjoint_anchors().len() == 1 {
+ self.selections.newest_anchor().head()
+ } else {
+ matches[current_index].start
+ };
+
+ let mut count = count % matches.len();
+ if count == 0 {
+ return current_index;
+ }
+ match direction {
+ Direction::Next => {
+ if matches[current_index]
+ .start
+ .cmp(¤t_index_position, &buffer)
+ .is_gt()
+ {
+ count = count - 1
+ }
+
+ (current_index + count) % matches.len()
+ }
+ Direction::Prev => {
+ if matches[current_index]
+ .end
+ .cmp(¤t_index_position, &buffer)
+ .is_lt()
+ {
+ count = count - 1;
+ }
+
+ if current_index >= count {
+ current_index - count
+ } else {
+ matches.len() - (count - current_index)
+ }
+ }
+ }
+ }
+
+ fn find_matches(
+ &mut self,
+ query: Arc<project::search::SearchQuery>,
+ cx: &mut ViewContext<Self>,
+ ) -> Task<Vec<Range<Anchor>>> {
+ let buffer = self.buffer().read(cx).snapshot(cx);
+ cx.background().spawn(async move {
+ let mut ranges = Vec::new();
+ if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() {
+ ranges.extend(
+ query
+ .search(excerpt_buffer, None)
+ .await
+ .into_iter()
+ .map(|range| {
+ buffer.anchor_after(range.start)..buffer.anchor_before(range.end)
+ }),
+ );
+ } else {
+ for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) {
+ let excerpt_range = excerpt.range.context.to_offset(&excerpt.buffer);
+ ranges.extend(
+ query
+ .search(&excerpt.buffer, Some(excerpt_range.clone()))
+ .await
+ .into_iter()
+ .map(|range| {
+ let start = excerpt
+ .buffer
+ .anchor_after(excerpt_range.start + range.start);
+ let end = excerpt
+ .buffer
+ .anchor_before(excerpt_range.start + range.end);
+ buffer.anchor_in_excerpt(excerpt.id.clone(), start)
+ ..buffer.anchor_in_excerpt(excerpt.id.clone(), end)
+ }),
+ );
+ }
+ }
+ ranges
+ })
+ }
+
+ fn active_match_index(
+ &mut self,
+ matches: Vec<Range<Anchor>>,
+ cx: &mut ViewContext<Self>,
+ ) -> Option<usize> {
+ active_match_index(
+ &matches,
+ &self.selections.newest_anchor().head(),
+ &self.buffer().read(cx).snapshot(cx),
+ )
+ }
+}
+
+pub fn active_match_index(
+ ranges: &[Range<Anchor>],
+ cursor: &Anchor,
+ buffer: &MultiBufferSnapshot,
+) -> Option<usize> {
+ if ranges.is_empty() {
+ None
+ } else {
+ match ranges.binary_search_by(|probe| {
+ if probe.end.cmp(cursor, &*buffer).is_lt() {
+ Ordering::Less
+ } else if probe.start.cmp(cursor, &*buffer).is_gt() {
+ Ordering::Greater
+ } else {
+ Ordering::Equal
+ }
+ }) {
+ Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)),
+ }
+ }
+}
+
+pub struct CursorPosition {
+ position: Option<Point>,
+ selected_count: usize,
+ _observe_active_editor: Option<Subscription>,
+}
+
+impl Default for CursorPosition {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl CursorPosition {
+ pub fn new() -> Self {
+ Self {
+ position: None,
+ selected_count: 0,
+ _observe_active_editor: None,
+ }
+ }
+
+ fn update_position(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+ let editor = editor.read(cx);
+ let buffer = editor.buffer().read(cx).snapshot(cx);
+
+ self.selected_count = 0;
+ let mut last_selection: Option<Selection<usize>> = None;
+ for selection in editor.selections.all::<usize>(cx) {
+ self.selected_count += selection.end - selection.start;
+ if last_selection
+ .as_ref()
+ .map_or(true, |last_selection| selection.id > last_selection.id)
+ {
+ last_selection = Some(selection);
+ }
+ }
+ self.position = last_selection.map(|s| s.head().to_point(&buffer));
+
+ cx.notify();
+ }
+}
+
+impl Entity for CursorPosition {
+ type Event = ();
+}
+
+impl View for CursorPosition {
+ fn ui_name() -> &'static str {
+ "CursorPosition"
+ }
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+ if let Some(position) = self.position {
+ let theme = &theme::current(cx).workspace.status_bar;
+ let mut text = format!(
+ "{}{FILE_ROW_COLUMN_DELIMITER}{}",
+ position.row + 1,
+ position.column + 1
+ );
+ if self.selected_count > 0 {
+ write!(text, " ({} selected)", self.selected_count).unwrap();
+ }
+ Label::new(text, theme.cursor_position.clone()).into_any()
+ } else {
+ Empty::new().into_any()
+ }
+ }
+}
+
+impl StatusItemView for CursorPosition {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+ self._observe_active_editor = Some(cx.observe(&editor, Self::update_position));
+ self.update_position(editor, cx);
+ } else {
+ self.position = None;
+ self._observe_active_editor = None;
+ }
+
+ cx.notify();
+ }
+}
+
+fn path_for_buffer<'a>(
+ buffer: &ModelHandle<MultiBuffer>,
+ height: usize,
+ include_filename: bool,
+ cx: &'a AppContext,
+) -> Option<Cow<'a, Path>> {
+ let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
+ path_for_file(file.as_ref(), height, include_filename, cx)
+}
+
+fn path_for_file<'a>(
+ file: &'a dyn language::File,
+ mut height: usize,
+ include_filename: bool,
+ cx: &'a AppContext,
+) -> Option<Cow<'a, Path>> {
+ // Ensure we always render at least the filename.
+ height += 1;
+
+ let mut prefix = file.path().as_ref();
+ while height > 0 {
+ if let Some(parent) = prefix.parent() {
+ prefix = parent;
+ height -= 1;
+ } else {
+ break;
+ }
+ }
+
+ // Here we could have just always used `full_path`, but that is very
+ // allocation-heavy and so we try to use a `Cow<Path>` if we haven't
+ // traversed all the way up to the worktree's root.
+ if height > 0 {
+ let full_path = file.full_path(cx);
+ if include_filename {
+ Some(full_path.into())
+ } else {
+ Some(full_path.parent()?.to_path_buf().into())
+ }
+ } else {
+ let mut path = file.path().strip_prefix(prefix).ok()?;
+ if !include_filename {
+ path = path.parent()?;
+ }
+ Some(path.into())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use gpui::AppContext;
+ use std::{
+ path::{Path, PathBuf},
+ sync::Arc,
+ time::SystemTime,
+ };
+
+ #[gpui::test]
+ fn test_path_for_file(cx: &mut AppContext) {
+ let file = TestFile {
+ path: Path::new("").into(),
+ full_path: PathBuf::from(""),
+ };
+ assert_eq!(path_for_file(&file, 0, false, cx), None);
+ }
+
+ struct TestFile {
+ path: Arc<Path>,
+ full_path: PathBuf,
+ }
+
+ impl language::File for TestFile {
+ fn path(&self) -> &Arc<Path> {
+ &self.path
+ }
+
+ fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
+ self.full_path.clone()
+ }
+
+ fn as_local(&self) -> Option<&dyn language::LocalFile> {
+ unimplemented!()
+ }
+
+ fn mtime(&self) -> SystemTime {
+ unimplemented!()
+ }
+
+ fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr {
+ unimplemented!()
+ }
+
+ fn worktree_id(&self) -> usize {
+ 0
+ }
+
+ fn is_deleted(&self) -> bool {
+ unimplemented!()
+ }
+
+ fn as_any(&self) -> &dyn std::any::Any {
+ unimplemented!()
+ }
+
+ fn to_proto(&self) -> rpc::proto::File {
+ unimplemented!()
+ }
+ }
+}
@@ -0,0 +1,1269 @@
+use crate::{
+ display_map::DisplaySnapshot,
+ element::PointForPosition,
+ hover_popover::{self, InlayHover},
+ Anchor, DisplayPoint, Editor, EditorSnapshot, InlayId, SelectPhase,
+};
+use gpui::{Task, ViewContext};
+use language::{Bias, ToOffset};
+use lsp::LanguageServerId;
+use project::{
+ HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink,
+ ResolveState,
+};
+use std::ops::Range;
+use util::TryFutureExt;
+
+#[derive(Debug, Default)]
+pub struct LinkGoToDefinitionState {
+ pub last_trigger_point: Option<TriggerPoint>,
+ pub symbol_range: Option<RangeInEditor>,
+ pub kind: Option<LinkDefinitionKind>,
+ pub definitions: Vec<GoToDefinitionLink>,
+ pub task: Option<Task<Option<()>>>,
+}
+
+#[derive(Debug, Eq, PartialEq, Clone)]
+pub enum RangeInEditor {
+ Text(Range<Anchor>),
+ Inlay(InlayHighlight),
+}
+
+impl RangeInEditor {
+ pub fn as_text_range(&self) -> Option<Range<Anchor>> {
+ match self {
+ Self::Text(range) => Some(range.clone()),
+ Self::Inlay(_) => None,
+ }
+ }
+
+ fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
+ match (self, trigger_point) {
+ (Self::Text(range), TriggerPoint::Text(point)) => {
+ let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
+ point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge()
+ }
+ (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {
+ highlight.inlay == point.inlay
+ && highlight.range.contains(&point.range.start)
+ && highlight.range.contains(&point.range.end)
+ }
+ (Self::Inlay(_), TriggerPoint::Text(_))
+ | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum GoToDefinitionTrigger {
+ Text(DisplayPoint),
+ InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone)]
+pub enum GoToDefinitionLink {
+ Text(LocationLink),
+ InlayHint(lsp::Location, LanguageServerId),
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct InlayHighlight {
+ pub inlay: InlayId,
+ pub inlay_position: Anchor,
+ pub range: Range<usize>,
+}
+
+#[derive(Debug, Clone)]
+pub enum TriggerPoint {
+ Text(Anchor),
+ InlayHint(InlayHighlight, lsp::Location, LanguageServerId),
+}
+
+impl TriggerPoint {
+ pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind {
+ match self {
+ TriggerPoint::Text(_) => {
+ if shift {
+ LinkDefinitionKind::Type
+ } else {
+ LinkDefinitionKind::Symbol
+ }
+ }
+ TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type,
+ }
+ }
+
+ fn anchor(&self) -> &Anchor {
+ match self {
+ TriggerPoint::Text(anchor) => anchor,
+ TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,
+ }
+ }
+}
+
+pub fn update_go_to_definition_link(
+ editor: &mut Editor,
+ origin: Option<GoToDefinitionTrigger>,
+ cmd_held: bool,
+ shift_held: bool,
+ cx: &mut ViewContext<Editor>,
+) {
+ let pending_nonempty_selection = editor.has_pending_nonempty_selection();
+
+ // Store new mouse point as an anchor
+ let snapshot = editor.snapshot(cx);
+ let trigger_point = match origin {
+ Some(GoToDefinitionTrigger::Text(p)) => {
+ Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before(
+ p.to_offset(&snapshot.display_snapshot, Bias::Left),
+ )))
+ }
+ Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => {
+ Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id))
+ }
+ None => None,
+ };
+
+ // If the new point is the same as the previously stored one, return early
+ if let (Some(a), Some(b)) = (
+ &trigger_point,
+ &editor.link_go_to_definition_state.last_trigger_point,
+ ) {
+ match (a, b) {
+ (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => {
+ if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() {
+ return;
+ }
+ }
+ (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => {
+ if range_a == range_b {
+ return;
+ }
+ }
+ _ => {}
+ }
+ }
+
+ editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone();
+
+ if pending_nonempty_selection {
+ hide_link_definition(editor, cx);
+ return;
+ }
+
+ if cmd_held {
+ if let Some(trigger_point) = trigger_point {
+ let kind = trigger_point.definition_kind(shift_held);
+ show_link_definition(kind, editor, trigger_point, snapshot, cx);
+ return;
+ }
+ }
+
+ hide_link_definition(editor, cx);
+}
+
+pub fn update_inlay_link_and_hover_points(
+ snapshot: &DisplaySnapshot,
+ point_for_position: PointForPosition,
+ editor: &mut Editor,
+ cmd_held: bool,
+ shift_held: bool,
+ cx: &mut ViewContext<'_, '_, Editor>,
+) {
+ let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 {
+ Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left))
+ } else {
+ None
+ };
+ let mut go_to_definition_updated = false;
+ let mut hover_updated = false;
+ if let Some(hovered_offset) = hovered_offset {
+ let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+ let previous_valid_anchor = buffer_snapshot.anchor_at(
+ point_for_position.previous_valid.to_point(snapshot),
+ Bias::Left,
+ );
+ let next_valid_anchor = buffer_snapshot.anchor_at(
+ point_for_position.next_valid.to_point(snapshot),
+ Bias::Right,
+ );
+ if let Some(hovered_hint) = editor
+ .visible_inlay_hints(cx)
+ .into_iter()
+ .skip_while(|hint| {
+ hint.position
+ .cmp(&previous_valid_anchor, &buffer_snapshot)
+ .is_lt()
+ })
+ .take_while(|hint| {
+ hint.position
+ .cmp(&next_valid_anchor, &buffer_snapshot)
+ .is_le()
+ })
+ .max_by_key(|hint| hint.id)
+ {
+ let inlay_hint_cache = editor.inlay_hint_cache();
+ let excerpt_id = previous_valid_anchor.excerpt_id;
+ if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) {
+ match cached_hint.resolve_state {
+ ResolveState::CanResolve(_, _) => {
+ if let Some(buffer_id) = previous_valid_anchor.buffer_id {
+ inlay_hint_cache.spawn_hint_resolve(
+ buffer_id,
+ excerpt_id,
+ hovered_hint.id,
+ cx,
+ );
+ }
+ }
+ ResolveState::Resolved => {
+ let mut extra_shift_left = 0;
+ let mut extra_shift_right = 0;
+ if cached_hint.padding_left {
+ extra_shift_left += 1;
+ extra_shift_right += 1;
+ }
+ if cached_hint.padding_right {
+ extra_shift_right += 1;
+ }
+ match cached_hint.label {
+ project::InlayHintLabel::String(_) => {
+ if let Some(tooltip) = cached_hint.tooltip {
+ hover_popover::hover_at_inlay(
+ editor,
+ InlayHover {
+ excerpt: excerpt_id,
+ tooltip: match tooltip {
+ InlayHintTooltip::String(text) => HoverBlock {
+ text,
+ kind: HoverBlockKind::PlainText,
+ },
+ InlayHintTooltip::MarkupContent(content) => {
+ HoverBlock {
+ text: content.value,
+ kind: content.kind,
+ }
+ }
+ },
+ range: InlayHighlight {
+ inlay: hovered_hint.id,
+ inlay_position: hovered_hint.position,
+ range: extra_shift_left
+ ..hovered_hint.text.len() + extra_shift_right,
+ },
+ },
+ cx,
+ );
+ hover_updated = true;
+ }
+ }
+ project::InlayHintLabel::LabelParts(label_parts) => {
+ let hint_start =
+ snapshot.anchor_to_inlay_offset(hovered_hint.position);
+ if let Some((hovered_hint_part, part_range)) =
+ hover_popover::find_hovered_hint_part(
+ label_parts,
+ hint_start,
+ hovered_offset,
+ )
+ {
+ let highlight_start =
+ (part_range.start - hint_start).0 + extra_shift_left;
+ let highlight_end =
+ (part_range.end - hint_start).0 + extra_shift_right;
+ let highlight = InlayHighlight {
+ inlay: hovered_hint.id,
+ inlay_position: hovered_hint.position,
+ range: highlight_start..highlight_end,
+ };
+ if let Some(tooltip) = hovered_hint_part.tooltip {
+ hover_popover::hover_at_inlay(
+ editor,
+ InlayHover {
+ excerpt: excerpt_id,
+ tooltip: match tooltip {
+ InlayHintLabelPartTooltip::String(text) => {
+ HoverBlock {
+ text,
+ kind: HoverBlockKind::PlainText,
+ }
+ }
+ InlayHintLabelPartTooltip::MarkupContent(
+ content,
+ ) => HoverBlock {
+ text: content.value,
+ kind: content.kind,
+ },
+ },
+ range: highlight.clone(),
+ },
+ cx,
+ );
+ hover_updated = true;
+ }
+ if let Some((language_server_id, location)) =
+ hovered_hint_part.location
+ {
+ go_to_definition_updated = true;
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::InlayHint(
+ highlight,
+ location,
+ language_server_id,
+ )),
+ cmd_held,
+ shift_held,
+ cx,
+ );
+ }
+ }
+ }
+ };
+ }
+ ResolveState::Resolving => {}
+ }
+ }
+ }
+ }
+
+ if !go_to_definition_updated {
+ update_go_to_definition_link(editor, None, cmd_held, shift_held, cx);
+ }
+ if !hover_updated {
+ hover_popover::hover_at(editor, None, cx);
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub enum LinkDefinitionKind {
+ Symbol,
+ Type,
+}
+
+pub fn show_link_definition(
+ definition_kind: LinkDefinitionKind,
+ editor: &mut Editor,
+ trigger_point: TriggerPoint,
+ snapshot: EditorSnapshot,
+ cx: &mut ViewContext<Editor>,
+) {
+ let same_kind = editor.link_go_to_definition_state.kind == Some(definition_kind);
+ if !same_kind {
+ hide_link_definition(editor, cx);
+ }
+
+ if editor.pending_rename.is_some() {
+ return;
+ }
+
+ let trigger_anchor = trigger_point.anchor();
+ let (buffer, buffer_position) = if let Some(output) = editor
+ .buffer
+ .read(cx)
+ .text_anchor_for_position(trigger_anchor.clone(), cx)
+ {
+ output
+ } else {
+ return;
+ };
+
+ let excerpt_id = if let Some((excerpt_id, _, _)) = editor
+ .buffer()
+ .read(cx)
+ .excerpt_containing(trigger_anchor.clone(), cx)
+ {
+ excerpt_id
+ } else {
+ return;
+ };
+
+ let project = if let Some(project) = editor.project.clone() {
+ project
+ } else {
+ return;
+ };
+
+ // Don't request again if the location is within the symbol region of a previous request with the same kind
+ if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range {
+ if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) {
+ return;
+ }
+ }
+
+ let task = cx.spawn(|this, mut cx| {
+ async move {
+ let result = match &trigger_point {
+ TriggerPoint::Text(_) => {
+ // query the LSP for definition info
+ cx.update(|cx| {
+ project.update(cx, |project, cx| match definition_kind {
+ LinkDefinitionKind::Symbol => {
+ project.definition(&buffer, buffer_position, cx)
+ }
+
+ LinkDefinitionKind::Type => {
+ project.type_definition(&buffer, buffer_position, cx)
+ }
+ })
+ })
+ .await
+ .ok()
+ .map(|definition_result| {
+ (
+ definition_result.iter().find_map(|link| {
+ link.origin.as_ref().map(|origin| {
+ let start = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(excerpt_id.clone(), origin.range.start);
+ let end = snapshot
+ .buffer_snapshot
+ .anchor_in_excerpt(excerpt_id.clone(), origin.range.end);
+ RangeInEditor::Text(start..end)
+ })
+ }),
+ definition_result
+ .into_iter()
+ .map(GoToDefinitionLink::Text)
+ .collect(),
+ )
+ })
+ }
+ TriggerPoint::InlayHint(highlight, lsp_location, server_id) => Some((
+ Some(RangeInEditor::Inlay(highlight.clone())),
+ vec![GoToDefinitionLink::InlayHint(
+ lsp_location.clone(),
+ *server_id,
+ )],
+ )),
+ };
+
+ this.update(&mut cx, |this, cx| {
+ // Clear any existing highlights
+ this.clear_highlights::<LinkGoToDefinitionState>(cx);
+ this.link_go_to_definition_state.kind = Some(definition_kind);
+ this.link_go_to_definition_state.symbol_range = result
+ .as_ref()
+ .and_then(|(symbol_range, _)| symbol_range.clone());
+
+ if let Some((symbol_range, definitions)) = result {
+ this.link_go_to_definition_state.definitions = definitions.clone();
+
+ let buffer_snapshot = buffer.read(cx).snapshot();
+
+ // Only show highlight if there exists a definition to jump to that doesn't contain
+ // the current location.
+ let any_definition_does_not_contain_current_location =
+ definitions.iter().any(|definition| {
+ match &definition {
+ GoToDefinitionLink::Text(link) => {
+ if link.target.buffer == buffer {
+ let range = &link.target.range;
+ // Expand range by one character as lsp definition ranges include positions adjacent
+ // but not contained by the symbol range
+ let start = buffer_snapshot.clip_offset(
+ range
+ .start
+ .to_offset(&buffer_snapshot)
+ .saturating_sub(1),
+ Bias::Left,
+ );
+ let end = buffer_snapshot.clip_offset(
+ range.end.to_offset(&buffer_snapshot) + 1,
+ Bias::Right,
+ );
+ let offset = buffer_position.to_offset(&buffer_snapshot);
+ !(start <= offset && end >= offset)
+ } else {
+ true
+ }
+ }
+ GoToDefinitionLink::InlayHint(_, _) => true,
+ }
+ });
+
+ if any_definition_does_not_contain_current_location {
+ // Highlight symbol using theme link definition highlight style
+ let style = theme::current(cx).editor.link_definition;
+ let highlight_range =
+ symbol_range.unwrap_or_else(|| match &trigger_point {
+ TriggerPoint::Text(trigger_anchor) => {
+ let snapshot = &snapshot.buffer_snapshot;
+ // If no symbol range returned from language server, use the surrounding word.
+ let (offset_range, _) =
+ snapshot.surrounding_word(*trigger_anchor);
+ RangeInEditor::Text(
+ snapshot.anchor_before(offset_range.start)
+ ..snapshot.anchor_after(offset_range.end),
+ )
+ }
+ TriggerPoint::InlayHint(highlight, _, _) => {
+ RangeInEditor::Inlay(highlight.clone())
+ }
+ });
+
+ match highlight_range {
+ RangeInEditor::Text(text_range) => this
+ .highlight_text::<LinkGoToDefinitionState>(
+ vec![text_range],
+ style,
+ cx,
+ ),
+ RangeInEditor::Inlay(highlight) => this
+ .highlight_inlays::<LinkGoToDefinitionState>(
+ vec![highlight],
+ style,
+ cx,
+ ),
+ }
+ } else {
+ hide_link_definition(this, cx);
+ }
+ }
+ })?;
+
+ Ok::<_, anyhow::Error>(())
+ }
+ .log_err()
+ });
+
+ editor.link_go_to_definition_state.task = Some(task);
+}
+
+pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
+ if editor.link_go_to_definition_state.symbol_range.is_some()
+ || !editor.link_go_to_definition_state.definitions.is_empty()
+ {
+ editor.link_go_to_definition_state.symbol_range.take();
+ editor.link_go_to_definition_state.definitions.clear();
+ cx.notify();
+ }
+
+ editor.link_go_to_definition_state.task = None;
+
+ editor.clear_highlights::<LinkGoToDefinitionState>(cx);
+}
+
+pub fn go_to_fetched_definition(
+ editor: &mut Editor,
+ point: PointForPosition,
+ split: bool,
+ cx: &mut ViewContext<Editor>,
+) {
+ go_to_fetched_definition_of_kind(LinkDefinitionKind::Symbol, editor, point, split, cx);
+}
+
+pub fn go_to_fetched_type_definition(
+ editor: &mut Editor,
+ point: PointForPosition,
+ split: bool,
+ cx: &mut ViewContext<Editor>,
+) {
+ go_to_fetched_definition_of_kind(LinkDefinitionKind::Type, editor, point, split, cx);
+}
+
+fn go_to_fetched_definition_of_kind(
+ kind: LinkDefinitionKind,
+ editor: &mut Editor,
+ point: PointForPosition,
+ split: bool,
+ cx: &mut ViewContext<Editor>,
+) {
+ let cached_definitions = editor.link_go_to_definition_state.definitions.clone();
+ hide_link_definition(editor, cx);
+ let cached_definitions_kind = editor.link_go_to_definition_state.kind;
+
+ let is_correct_kind = cached_definitions_kind == Some(kind);
+ if !cached_definitions.is_empty() && is_correct_kind {
+ if !editor.focused {
+ cx.focus_self();
+ }
+
+ editor.navigate_to_definitions(cached_definitions, split, cx);
+ } else {
+ editor.select(
+ SelectPhase::Begin {
+ position: point.next_valid,
+ add: false,
+ click_count: 1,
+ },
+ cx,
+ );
+
+ if point.as_valid().is_some() {
+ match kind {
+ LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx),
+ LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx),
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::ToDisplayPoint,
+ editor_tests::init_test,
+ inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels},
+ test::editor_lsp_test_context::EditorLspTestContext,
+ };
+ use futures::StreamExt;
+ use gpui::{
+ platform::{self, Modifiers, ModifiersChangedEvent},
+ View,
+ };
+ use indoc::indoc;
+ use language::language_settings::InlayHintSettings;
+ use lsp::request::{GotoDefinition, GotoTypeDefinition};
+ use util::assert_set_eq;
+
+ #[gpui::test]
+ async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ type_definition_provider: Some(lsp::TypeDefinitionProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ struct A;
+ let vหariable = A;
+ "});
+
+ // Basic hold cmd+shift, expect highlight in region if response contains type definition
+ let hover_point = cx.display_point(indoc! {"
+ struct A;
+ let vหariable = A;
+ "});
+ let symbol_range = cx.lsp_range(indoc! {"
+ struct A;
+ let ยซvariableยป = A;
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ struct ยซAยป;
+ let variable = A;
+ "});
+
+ let mut requests =
+ cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ // Press cmd+shift to trigger highlight
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ true,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.foreground().run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ struct A;
+ let ยซvariableยป = A;
+ "});
+
+ // Unpress shift causes highlight to go away (normal goto-definition is not valid here)
+ cx.update_editor(|editor, cx| {
+ editor.modifiers_changed(
+ &platform::ModifiersChangedEvent {
+ modifiers: Modifiers {
+ cmd: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ cx,
+ );
+ });
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ struct A;
+ let variable = A;
+ "});
+
+ // Cmd+shift click without existing definition requests and jumps
+ let hover_point = cx.display_point(indoc! {"
+ struct A;
+ let vหariable = A;
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ struct ยซAยป;
+ let variable = A;
+ "});
+
+ let mut requests =
+ cx.handle_request::<GotoTypeDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoTypeDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: None,
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx);
+ });
+ requests.next().await;
+ cx.foreground().run_until_parked();
+
+ cx.assert_editor_state(indoc! {"
+ struct ยซAหยป;
+ let variable = A;
+ "});
+ }
+
+ #[gpui::test]
+ async fn test_link_go_to_definition(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ fn หtest() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Basic hold cmd, expect highlight in region if response contains definition
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_wหork(); }
+ fn do_work() { test(); }
+ "});
+ let symbol_range = cx.lsp_range(indoc! {"
+ fn test() { ยซdo_workยป(); }
+ fn do_work() { test(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn ยซdo_workยป() { test(); }
+ "});
+
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.foreground().run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { ยซdo_workยป(); }
+ fn do_work() { test(); }
+ "});
+
+ // Unpress cmd causes highlight to go away
+ cx.update_editor(|editor, cx| {
+ editor.modifiers_changed(&Default::default(), cx);
+ });
+
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Response without source range still highlights word
+ cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None);
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ // No origin range
+ origin_selection_range: None,
+ target_uri: url.clone(),
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.foreground().run_until_parked();
+
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { ยซdo_workยป(); }
+ fn do_work() { test(); }
+ "});
+
+ // Moving mouse to location with no response dismisses highlight
+ let hover_point = cx.display_point(indoc! {"
+ fหn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+ let mut requests = cx
+ .lsp
+ .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+ // No definitions returned
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+ });
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.foreground().run_until_parked();
+
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Move mouse without cmd and then pressing cmd triggers highlight
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { teหst(); }
+ "});
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ false,
+ false,
+ cx,
+ );
+ });
+ cx.foreground().run_until_parked();
+
+ // Assert no link highlights
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ let symbol_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { ยซtestยป(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn ยซtestยป() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: Some(symbol_range),
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+ cx.update_editor(|editor, cx| {
+ editor.modifiers_changed(
+ &ModifiersChangedEvent {
+ modifiers: Modifiers {
+ cmd: true,
+ ..Default::default()
+ },
+ },
+ cx,
+ );
+ });
+ requests.next().await;
+ cx.foreground().run_until_parked();
+
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { ยซtestยป(); }
+ "});
+
+ // Deactivating the window dismisses the highlight
+ cx.update_workspace(|workspace, cx| {
+ workspace.on_window_activation_changed(false, cx);
+ });
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Moving the mouse restores the highlights.
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.foreground().run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { ยซtestยป(); }
+ "});
+
+ // Moving again within the same symbol range doesn't re-request
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { tesหt(); }
+ "});
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.foreground().run_until_parked();
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { ยซtestยป(); }
+ "});
+
+ // Cmd click with existing definition doesn't re-request and dismisses highlight
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+ });
+ // Assert selection moved to to definition
+ cx.lsp
+ .handle_request::<GotoDefinition, _, _>(move |_, _| async move {
+ // Empty definition response to make sure we aren't hitting the lsp and using
+ // the cached location instead
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![])))
+ });
+ cx.foreground().run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ fn ยซtestหยป() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Assert no link highlights after jump
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+
+ // Cmd click without existing definition requests and jumps
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_wหork(); }
+ fn do_work() { test(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn ยซdo_workยป() { test(); }
+ "});
+
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: None,
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx);
+ });
+ requests.next().await;
+ cx.foreground().run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ fn test() { do_work(); }
+ fn ยซdo_workหยป() { test(); }
+ "});
+
+ // 1. We have a pending selection, mouse point is over a symbol that we have a response for, hitting cmd and nothing happens
+ // 2. Selection is completed, hovering
+ let hover_point = cx.display_point(indoc! {"
+ fn test() { do_wหork(); }
+ fn do_work() { test(); }
+ "});
+ let target_range = cx.lsp_range(indoc! {"
+ fn test() { do_work(); }
+ fn ยซdo_workยป() { test(); }
+ "});
+ let mut requests = cx.handle_request::<GotoDefinition, _, _>(move |url, _, _| async move {
+ Ok(Some(lsp::GotoDefinitionResponse::Link(vec![
+ lsp::LocationLink {
+ origin_selection_range: None,
+ target_uri: url,
+ target_range,
+ target_selection_range: target_range,
+ },
+ ])))
+ });
+
+ // create a pending selection
+ let selection_range = cx.ranges(indoc! {"
+ fn ยซtest() { do_wยปork(); }
+ fn do_work() { test(); }
+ "})[0]
+ .clone();
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let anchor_range = snapshot.anchor_before(selection_range.start)
+ ..snapshot.anchor_after(selection_range.end);
+ editor.change_selections(Some(crate::Autoscroll::fit()), cx, |s| {
+ s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
+ });
+ });
+ cx.update_editor(|editor, cx| {
+ update_go_to_definition_link(
+ editor,
+ Some(GoToDefinitionTrigger::Text(hover_point)),
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.foreground().run_until_parked();
+ assert!(requests.try_next().is_err());
+ cx.assert_editor_text_highlights::<LinkGoToDefinitionState>(indoc! {"
+ fn test() { do_work(); }
+ fn do_work() { test(); }
+ "});
+ cx.foreground().run_until_parked();
+ }
+
+ #[gpui::test]
+ async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |settings| {
+ settings.defaults.inlay_hints = Some(InlayHintSettings {
+ enabled: true,
+ show_type_hints: true,
+ show_parameter_hints: true,
+ show_other_hints: true,
+ })
+ });
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ inlay_hint_provider: Some(lsp::OneOf::Left(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+ cx.set_state(indoc! {"
+ struct TestStruct;
+
+ fn main() {
+ let variableห = TestStruct;
+ }
+ "});
+ let hint_start_offset = cx.ranges(indoc! {"
+ struct TestStruct;
+
+ fn main() {
+ let variableห = TestStruct;
+ }
+ "})[0]
+ .start;
+ let hint_position = cx.to_lsp(hint_start_offset);
+ let target_range = cx.lsp_range(indoc! {"
+ struct ยซTestStructยป;
+
+ fn main() {
+ let variable = TestStruct;
+ }
+ "});
+
+ let expected_uri = cx.buffer_lsp_url.clone();
+ let hint_label = ": TestStruct";
+ cx.lsp
+ .handle_request::<lsp::request::InlayHintRequest, _, _>(move |params, _| {
+ let expected_uri = expected_uri.clone();
+ async move {
+ assert_eq!(params.text_document.uri, expected_uri);
+ Ok(Some(vec![lsp::InlayHint {
+ position: hint_position,
+ label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart {
+ value: hint_label.to_string(),
+ location: Some(lsp::Location {
+ uri: params.text_document.uri,
+ range: target_range,
+ }),
+ ..Default::default()
+ }]),
+ kind: Some(lsp::InlayHintKind::TYPE),
+ text_edits: None,
+ tooltip: None,
+ padding_left: Some(false),
+ padding_right: Some(false),
+ data: None,
+ }]))
+ }
+ })
+ .next()
+ .await;
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| {
+ let expected_layers = vec![hint_label.to_string()];
+ assert_eq!(expected_layers, cached_hint_labels(editor));
+ assert_eq!(expected_layers, visible_hint_labels(editor, cx));
+ });
+
+ let inlay_range = cx
+ .ranges(indoc! {"
+ struct TestStruct;
+
+ fn main() {
+ let variableยซ ยป= TestStruct;
+ }
+ "})
+ .get(0)
+ .cloned()
+ .unwrap();
+ let hint_hover_position = cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let previous_valid = inlay_range.start.to_display_point(&snapshot);
+ let next_valid = inlay_range.end.to_display_point(&snapshot);
+ assert_eq!(previous_valid.row(), next_valid.row());
+ assert!(previous_valid.column() < next_valid.column());
+ let exact_unclipped = DisplayPoint::new(
+ previous_valid.row(),
+ previous_valid.column() + (hint_label.len() / 2) as u32,
+ );
+ PointForPosition {
+ previous_valid,
+ next_valid,
+ exact_unclipped,
+ column_overshoot_after_line_end: 0,
+ }
+ });
+ // Press cmd to trigger highlight
+ cx.update_editor(|editor, cx| {
+ update_inlay_link_and_hover_points(
+ &editor.snapshot(cx),
+ hint_hover_position,
+ editor,
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let actual_highlights = snapshot
+ .inlay_highlights::<LinkGoToDefinitionState>()
+ .into_iter()
+ .flat_map(|highlights| highlights.values().map(|(_, highlight)| highlight))
+ .collect::<Vec<_>>();
+
+ let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx));
+ let expected_highlight = InlayHighlight {
+ inlay: InlayId::Hint(0),
+ inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
+ range: 0..hint_label.len(),
+ };
+ assert_set_eq!(actual_highlights, vec![&expected_highlight]);
+ });
+
+ // Unpress cmd causes highlight to go away
+ cx.update_editor(|editor, cx| {
+ editor.modifiers_changed(
+ &platform::ModifiersChangedEvent {
+ modifiers: Modifiers {
+ cmd: false,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ cx,
+ );
+ });
+ // Assert no link highlights
+ cx.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ let actual_ranges = snapshot
+ .text_highlight_ranges::<LinkGoToDefinitionState>()
+ .map(|ranges| ranges.as_ref().clone().1)
+ .unwrap_or_default();
+
+ assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}");
+ });
+
+ // Cmd+click without existing definition requests and jumps
+ cx.update_editor(|editor, cx| {
+ editor.modifiers_changed(
+ &platform::ModifiersChangedEvent {
+ modifiers: Modifiers {
+ cmd: true,
+ ..Default::default()
+ },
+ ..Default::default()
+ },
+ cx,
+ );
+ update_inlay_link_and_hover_points(
+ &editor.snapshot(cx),
+ hint_hover_position,
+ editor,
+ true,
+ false,
+ cx,
+ );
+ });
+ cx.foreground().run_until_parked();
+ cx.update_editor(|editor, cx| {
+ go_to_fetched_type_definition(editor, hint_hover_position, false, cx);
+ });
+ cx.foreground().run_until_parked();
+ cx.assert_editor_state(indoc! {"
+ struct ยซTestStructหยป;
+
+ fn main() {
+ let variable = TestStruct;
+ }
+ "});
+ }
+}
@@ -0,0 +1,96 @@
+use crate::{
+ DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
+ Rename, RevealInFinder, SelectMode, ToggleCodeActions,
+};
+use context_menu::ContextMenuItem;
+use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
+
+pub fn deploy_context_menu(
+ editor: &mut Editor,
+ position: Vector2F,
+ point: DisplayPoint,
+ cx: &mut ViewContext<Editor>,
+) {
+ if !editor.focused {
+ cx.focus_self();
+ }
+
+ // Don't show context menu for inline editors
+ if editor.mode() != EditorMode::Full {
+ return;
+ }
+
+ // Don't show the context menu if there isn't a project associated with this editor
+ if editor.project.is_none() {
+ return;
+ }
+
+ // Move the cursor to the clicked location so that dispatched actions make sense
+ editor.change_selections(None, cx, |s| {
+ s.clear_disjoint();
+ s.set_pending_display_range(point..point, SelectMode::Character);
+ });
+
+ editor.mouse_context_menu.update(cx, |menu, cx| {
+ menu.show(
+ position,
+ AnchorCorner::TopLeft,
+ vec![
+ ContextMenuItem::action("Rename Symbol", Rename),
+ ContextMenuItem::action("Go to Definition", GoToDefinition),
+ ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
+ ContextMenuItem::action("Find All References", FindAllReferences),
+ ContextMenuItem::action(
+ "Code Actions",
+ ToggleCodeActions {
+ deployed_from_indicator: false,
+ },
+ ),
+ ContextMenuItem::Separator,
+ ContextMenuItem::action("Reveal in Finder", RevealInFinder),
+ ],
+ cx,
+ );
+ });
+ cx.notify();
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
+ use indoc::indoc;
+
+ #[gpui::test]
+ async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ let mut cx = EditorLspTestContext::new_rust(
+ lsp::ServerCapabilities {
+ hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
+ ..Default::default()
+ },
+ cx,
+ )
+ .await;
+
+ cx.set_state(indoc! {"
+ fn teหst() {
+ do_work();
+ }
+ "});
+ let point = cx.display_point(indoc! {"
+ fn test() {
+ do_wหork();
+ }
+ "});
+ cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
+
+ cx.assert_editor_state(indoc! {"
+ fn test() {
+ do_wหork();
+ }
+ "});
+ cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
+ }
+}
@@ -0,0 +1,927 @@
+use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
+use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
+use gpui::{FontCache, TextLayoutCache};
+use language::Point;
+use std::{ops::Range, sync::Arc};
+
+#[derive(Debug, PartialEq)]
+pub enum FindRange {
+ SingleLine,
+ MultiLine,
+}
+
+/// TextLayoutDetails encompasses everything we need to move vertically
+/// taking into account variable width characters.
+pub struct TextLayoutDetails {
+ pub font_cache: Arc<FontCache>,
+ pub text_layout_cache: Arc<TextLayoutCache>,
+ pub editor_style: EditorStyle,
+}
+
+pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+ if point.column() > 0 {
+ *point.column_mut() -= 1;
+ } else if point.row() > 0 {
+ *point.row_mut() -= 1;
+ *point.column_mut() = map.line_len(point.row());
+ }
+ map.clip_point(point, Bias::Left)
+}
+
+pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+ if point.column() > 0 {
+ *point.column_mut() -= 1;
+ }
+ map.clip_point(point, Bias::Left)
+}
+
+pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+ let max_column = map.line_len(point.row());
+ if point.column() < max_column {
+ *point.column_mut() += 1;
+ } else if point.row() < map.max_point().row() {
+ *point.row_mut() += 1;
+ *point.column_mut() = 0;
+ }
+ map.clip_point(point, Bias::Right)
+}
+
+pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
+ *point.column_mut() += 1;
+ map.clip_point(point, Bias::Right)
+}
+
+pub fn up(
+ map: &DisplaySnapshot,
+ start: DisplayPoint,
+ goal: SelectionGoal,
+ preserve_column_at_start: bool,
+ text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+ up_by_rows(
+ map,
+ start,
+ 1,
+ goal,
+ preserve_column_at_start,
+ text_layout_details,
+ )
+}
+
+pub fn down(
+ map: &DisplaySnapshot,
+ start: DisplayPoint,
+ goal: SelectionGoal,
+ preserve_column_at_end: bool,
+ text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+ down_by_rows(
+ map,
+ start,
+ 1,
+ goal,
+ preserve_column_at_end,
+ text_layout_details,
+ )
+}
+
+pub fn up_by_rows(
+ map: &DisplaySnapshot,
+ start: DisplayPoint,
+ row_count: u32,
+ goal: SelectionGoal,
+ preserve_column_at_start: bool,
+ text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+ let mut goal_x = match goal {
+ SelectionGoal::HorizontalPosition(x) => x,
+ SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+ SelectionGoal::HorizontalRange { end, .. } => end,
+ _ => map.x_for_point(start, text_layout_details),
+ };
+
+ let prev_row = start.row().saturating_sub(row_count);
+ let mut point = map.clip_point(
+ DisplayPoint::new(prev_row, map.line_len(prev_row)),
+ Bias::Left,
+ );
+ if point.row() < start.row() {
+ *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+ } else if preserve_column_at_start {
+ return (start, goal);
+ } else {
+ point = DisplayPoint::new(0, 0);
+ goal_x = 0.0;
+ }
+
+ let mut clipped_point = map.clip_point(point, Bias::Left);
+ if clipped_point.row() < point.row() {
+ clipped_point = map.clip_point(point, Bias::Right);
+ }
+ (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
+}
+
+pub fn down_by_rows(
+ map: &DisplaySnapshot,
+ start: DisplayPoint,
+ row_count: u32,
+ goal: SelectionGoal,
+ preserve_column_at_end: bool,
+ text_layout_details: &TextLayoutDetails,
+) -> (DisplayPoint, SelectionGoal) {
+ let mut goal_x = match goal {
+ SelectionGoal::HorizontalPosition(x) => x,
+ SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
+ SelectionGoal::HorizontalRange { end, .. } => end,
+ _ => map.x_for_point(start, text_layout_details),
+ };
+
+ let new_row = start.row() + row_count;
+ let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
+ if point.row() > start.row() {
+ *point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
+ } else if preserve_column_at_end {
+ return (start, goal);
+ } else {
+ point = map.max_point();
+ goal_x = map.x_for_point(point, text_layout_details)
+ }
+
+ let mut clipped_point = map.clip_point(point, Bias::Right);
+ if clipped_point.row() > point.row() {
+ clipped_point = map.clip_point(point, Bias::Left);
+ }
+ (clipped_point, SelectionGoal::HorizontalPosition(goal_x))
+}
+
+pub fn line_beginning(
+ map: &DisplaySnapshot,
+ display_point: DisplayPoint,
+ stop_at_soft_boundaries: bool,
+) -> DisplayPoint {
+ let point = display_point.to_point(map);
+ let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
+ let line_start = map.prev_line_boundary(point).1;
+
+ if stop_at_soft_boundaries && display_point != soft_line_start {
+ soft_line_start
+ } else {
+ line_start
+ }
+}
+
+pub fn indented_line_beginning(
+ map: &DisplaySnapshot,
+ display_point: DisplayPoint,
+ stop_at_soft_boundaries: bool,
+) -> DisplayPoint {
+ let point = display_point.to_point(map);
+ let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
+ let indent_start = Point::new(
+ point.row,
+ map.buffer_snapshot.indent_size_for_line(point.row).len,
+ )
+ .to_display_point(map);
+ let line_start = map.prev_line_boundary(point).1;
+
+ if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
+ {
+ soft_line_start
+ } else if stop_at_soft_boundaries && display_point != indent_start {
+ indent_start
+ } else {
+ line_start
+ }
+}
+
+pub fn line_end(
+ map: &DisplaySnapshot,
+ display_point: DisplayPoint,
+ stop_at_soft_boundaries: bool,
+) -> DisplayPoint {
+ let soft_line_end = map.clip_point(
+ DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
+ Bias::Left,
+ );
+ if stop_at_soft_boundaries && display_point != soft_line_end {
+ soft_line_end
+ } else {
+ map.next_line_boundary(display_point.to_point(map)).1
+ }
+}
+
+pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+ find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+ (char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
+ || left == '\n'
+ })
+}
+
+pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+ find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
+ let is_word_start =
+ char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
+ let is_subword_start =
+ left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
+ is_word_start || is_subword_start || left == '\n'
+ })
+}
+
+pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+ find_boundary(map, point, FindRange::MultiLine, |left, right| {
+ (char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
+ || right == '\n'
+ })
+}
+
+pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
+ let raw_point = point.to_point(map);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
+
+ find_boundary(map, point, FindRange::MultiLine, |left, right| {
+ let is_word_end =
+ (char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
+ let is_subword_end =
+ left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
+ is_word_end || is_subword_end || right == '\n'
+ })
+}
+
+pub fn start_of_paragraph(
+ map: &DisplaySnapshot,
+ display_point: DisplayPoint,
+ mut count: usize,
+) -> DisplayPoint {
+ let point = display_point.to_point(map);
+ if point.row == 0 {
+ return DisplayPoint::zero();
+ }
+
+ let mut found_non_blank_line = false;
+ for row in (0..point.row + 1).rev() {
+ let blank = map.buffer_snapshot.is_line_blank(row);
+ if found_non_blank_line && blank {
+ if count <= 1 {
+ return Point::new(row, 0).to_display_point(map);
+ }
+ count -= 1;
+ found_non_blank_line = false;
+ }
+
+ found_non_blank_line |= !blank;
+ }
+
+ DisplayPoint::zero()
+}
+
+pub fn end_of_paragraph(
+ map: &DisplaySnapshot,
+ display_point: DisplayPoint,
+ mut count: usize,
+) -> DisplayPoint {
+ let point = display_point.to_point(map);
+ if point.row == map.max_buffer_row() {
+ return map.max_point();
+ }
+
+ let mut found_non_blank_line = false;
+ for row in point.row..map.max_buffer_row() + 1 {
+ let blank = map.buffer_snapshot.is_line_blank(row);
+ if found_non_blank_line && blank {
+ if count <= 1 {
+ return Point::new(row, 0).to_display_point(map);
+ }
+ count -= 1;
+ found_non_blank_line = false;
+ }
+
+ found_non_blank_line |= !blank;
+ }
+
+ map.max_point()
+}
+
+/// Scans for a boundary preceding the given start point `from` until a boundary is found,
+/// indicated by the given predicate returning true.
+/// The predicate is called with the character to the left and right of the candidate boundary location.
+/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
+pub fn find_preceding_boundary(
+ map: &DisplaySnapshot,
+ from: DisplayPoint,
+ find_range: FindRange,
+ mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+ let mut prev_ch = None;
+ let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
+
+ for ch in map.buffer_snapshot.reversed_chars_at(offset) {
+ if find_range == FindRange::SingleLine && ch == '\n' {
+ break;
+ }
+ if let Some(prev_ch) = prev_ch {
+ if is_boundary(ch, prev_ch) {
+ break;
+ }
+ }
+
+ offset -= ch.len_utf8();
+ prev_ch = Some(ch);
+ }
+
+ map.clip_point(offset.to_display_point(map), Bias::Left)
+}
+
+/// Scans for a boundary following the given start point until a boundary is found, indicated by the
+/// given predicate returning true. The predicate is called with the character to the left and right
+/// of the candidate boundary location, and will be called with `\n` characters indicating the start
+/// or end of a line.
+pub fn find_boundary(
+ map: &DisplaySnapshot,
+ from: DisplayPoint,
+ find_range: FindRange,
+ mut is_boundary: impl FnMut(char, char) -> bool,
+) -> DisplayPoint {
+ let mut offset = from.to_offset(&map, Bias::Right);
+ let mut prev_ch = None;
+
+ for ch in map.buffer_snapshot.chars_at(offset) {
+ if find_range == FindRange::SingleLine && ch == '\n' {
+ break;
+ }
+ if let Some(prev_ch) = prev_ch {
+ if is_boundary(prev_ch, ch) {
+ break;
+ }
+ }
+
+ offset += ch.len_utf8();
+ prev_ch = Some(ch);
+ }
+ map.clip_point(offset.to_display_point(map), Bias::Right)
+}
+
+pub fn chars_after(
+ map: &DisplaySnapshot,
+ mut offset: usize,
+) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
+ map.buffer_snapshot.chars_at(offset).map(move |ch| {
+ let before = offset;
+ offset = offset + ch.len_utf8();
+ (ch, before..offset)
+ })
+}
+
+pub fn chars_before(
+ map: &DisplaySnapshot,
+ mut offset: usize,
+) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
+ map.buffer_snapshot
+ .reversed_chars_at(offset)
+ .map(move |ch| {
+ let after = offset;
+ offset = offset - ch.len_utf8();
+ (ch, offset..after)
+ })
+}
+
+pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
+ let raw_point = point.to_point(map);
+ let scope = map.buffer_snapshot.language_scope_at(raw_point);
+ let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
+ let text = &map.buffer_snapshot;
+ let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
+ let prev_char_kind = text
+ .reversed_chars_at(ix)
+ .next()
+ .map(|c| char_kind(&scope, c));
+ prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
+}
+
+pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
+ let position = map
+ .clip_point(position, Bias::Left)
+ .to_offset(map, Bias::Left);
+ let (range, _) = map.buffer_snapshot.surrounding_word(position);
+ let start = range
+ .start
+ .to_point(&map.buffer_snapshot)
+ .to_display_point(map);
+ let end = range
+ .end
+ .to_point(&map.buffer_snapshot)
+ .to_display_point(map);
+ start..end
+}
+
+pub fn split_display_range_by_lines(
+ map: &DisplaySnapshot,
+ range: Range<DisplayPoint>,
+) -> Vec<Range<DisplayPoint>> {
+ let mut result = Vec::new();
+
+ let mut start = range.start;
+ // Loop over all the covered rows until the one containing the range end
+ for row in range.start.row()..range.end.row() {
+ let row_end_column = map.line_len(row);
+ let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
+ if start != end {
+ result.push(start..end);
+ }
+ start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
+ }
+
+ // Add the final range from the start of the last end to the original range end.
+ result.push(start..range.end);
+
+ result
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::{
+ display_map::Inlay,
+ test::{editor_test_context::EditorTestContext, marked_display_snapshot},
+ Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
+ };
+ use project::Project;
+ use settings::SettingsStore;
+ use util::post_inc;
+
+ #[gpui::test]
+ fn test_previous_word_start(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ previous_word_start(&snapshot, display_points[1]),
+ display_points[0]
+ );
+ }
+
+ assert("\nห หlorem", cx);
+ assert("ห\nห lorem", cx);
+ assert(" หloremห", cx);
+ assert("ห หlorem", cx);
+ assert(" หlorหem", cx);
+ assert("\nlorem\nห หipsum", cx);
+ assert("\n\nห\nห", cx);
+ assert(" หlorem หipsum", cx);
+ assert("loremห-หipsum", cx);
+ assert("loremห-#$@หipsum", cx);
+ assert("หlorem_หipsum", cx);
+ assert(" หdefฮณห", cx);
+ assert(" หbcฮห", cx);
+ assert(" abหโโหcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_previous_subword_start(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ previous_subword_start(&snapshot, display_points[1]),
+ display_points[0]
+ );
+ }
+
+ // Subword boundaries are respected
+ assert("lorem_หipหsum", cx);
+ assert("lorem_หipsumห", cx);
+ assert("หlorem_หipsum", cx);
+ assert("lorem_หipsum_หdolor", cx);
+ assert("loremหIpหsum", cx);
+ assert("loremหIpsumห", cx);
+
+ // Word boundaries are still respected
+ assert("\nห หlorem", cx);
+ assert(" หloremห", cx);
+ assert(" หlorหem", cx);
+ assert("\nlorem\nห หipsum", cx);
+ assert("\n\nห\nห", cx);
+ assert(" หlorem หipsum", cx);
+ assert("loremห-หipsum", cx);
+ assert("loremห-#$@หipsum", cx);
+ assert(" หdefฮณห", cx);
+ assert(" bcหฮห", cx);
+ assert(" หbcฮดห", cx);
+ assert(" abหโโหcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(
+ marked_text: &str,
+ cx: &mut gpui::AppContext,
+ is_boundary: impl FnMut(char, char) -> bool,
+ ) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ find_preceding_boundary(
+ &snapshot,
+ display_points[1],
+ FindRange::MultiLine,
+ is_boundary
+ ),
+ display_points[0]
+ );
+ }
+
+ assert("abcหdef\ngh\nijหk", cx, |left, right| {
+ left == 'c' && right == 'd'
+ });
+ assert("abcdef\nหgh\nijหk", cx, |left, right| {
+ left == '\n' && right == 'g'
+ });
+ let mut line_count = 0;
+ assert("abcdef\nหgh\nijหk", cx, |left, _| {
+ if left == '\n' {
+ line_count += 1;
+ line_count == 2
+ } else {
+ false
+ }
+ });
+ }
+
+ #[gpui::test]
+ fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ let input_text = "abcdefghijklmnopqrstuvwxys";
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+ let buffer = MultiBuffer::build_simple(input_text, cx);
+ let buffer_snapshot = buffer.read(cx).snapshot(cx);
+ let display_map =
+ cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+
+ // add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
+ let mut id = 0;
+ let inlays = (0..buffer_snapshot.len())
+ .map(|offset| {
+ [
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Left),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Suggestion(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Right),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Left),
+ text: format!("test").into(),
+ },
+ Inlay {
+ id: InlayId::Hint(post_inc(&mut id)),
+ position: buffer_snapshot.anchor_at(offset, Bias::Right),
+ text: format!("test").into(),
+ },
+ ]
+ })
+ .flatten()
+ .collect();
+ let snapshot = display_map.update(cx, |map, cx| {
+ map.splice_inlays(Vec::new(), inlays, cx);
+ map.snapshot(cx)
+ });
+
+ assert_eq!(
+ find_preceding_boundary(
+ &snapshot,
+ buffer_snapshot.len().to_display_point(&snapshot),
+ FindRange::MultiLine,
+ |left, _| left == 'e',
+ ),
+ snapshot
+ .buffer_snapshot
+ .offset_to_point(5)
+ .to_display_point(&snapshot),
+ "Should not stop at inlays when looking for boundaries"
+ );
+ }
+
+ #[gpui::test]
+ fn test_next_word_end(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ next_word_end(&snapshot, display_points[0]),
+ display_points[1]
+ );
+ }
+
+ assert("\nห loremห", cx);
+ assert(" หloremห", cx);
+ assert(" lorหemห", cx);
+ assert(" loremห ห\nipsum\n", cx);
+ assert("\nห\nห\n\n", cx);
+ assert("loremห ipsumห ", cx);
+ assert("loremห-หipsum", cx);
+ assert("loremห#$@-หipsum", cx);
+ assert("loremห_ipsumห", cx);
+ assert(" หbcฮห", cx);
+ assert(" abหโโหcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_next_subword_end(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ next_subword_end(&snapshot, display_points[0]),
+ display_points[1]
+ );
+ }
+
+ // Subword boundaries are respected
+ assert("loหremห_ipsum", cx);
+ assert("หloremห_ipsum", cx);
+ assert("loremห_ipsumห", cx);
+ assert("loremห_ipsumห_dolor", cx);
+ assert("loหremหIpsum", cx);
+ assert("loremหIpsumหDolor", cx);
+
+ // Word boundaries are still respected
+ assert("\nห loremห", cx);
+ assert(" หloremห", cx);
+ assert(" lorหemห", cx);
+ assert(" loremห ห\nipsum\n", cx);
+ assert("\nห\nห\n\n", cx);
+ assert("loremห ipsumห ", cx);
+ assert("loremห-หipsum", cx);
+ assert("loremห#$@-หipsum", cx);
+ assert("loremห_ipsumห", cx);
+ assert(" หbcหฮ", cx);
+ assert(" abหโโหcd", cx);
+ }
+
+ #[gpui::test]
+ fn test_find_boundary(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(
+ marked_text: &str,
+ cx: &mut gpui::AppContext,
+ is_boundary: impl FnMut(char, char) -> bool,
+ ) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ find_boundary(
+ &snapshot,
+ display_points[0],
+ FindRange::MultiLine,
+ is_boundary
+ ),
+ display_points[1]
+ );
+ }
+
+ assert("abcหdef\ngh\nijหk", cx, |left, right| {
+ left == 'j' && right == 'k'
+ });
+ assert("abหcdef\ngh\nหijk", cx, |left, right| {
+ left == '\n' && right == 'i'
+ });
+ let mut line_count = 0;
+ assert("abcหdef\ngh\nหijk", cx, |left, _| {
+ if left == '\n' {
+ line_count += 1;
+ line_count == 2
+ } else {
+ false
+ }
+ });
+ }
+
+ #[gpui::test]
+ fn test_surrounding_word(cx: &mut gpui::AppContext) {
+ init_test(cx);
+
+ fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
+ let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
+ assert_eq!(
+ surrounding_word(&snapshot, display_points[1]),
+ display_points[0]..display_points[2],
+ "{}",
+ marked_text.to_string()
+ );
+ }
+
+ assert("หหloremห ipsum", cx);
+ assert("หloหremห ipsum", cx);
+ assert("หloremหห ipsum", cx);
+ assert("loremห ห หipsum", cx);
+ assert("lorem\nหหห\nipsum", cx);
+ assert("lorem\nหหipsumห", cx);
+ assert("loremห,หห ipsum", cx);
+ assert("หloremหห, ipsum", cx);
+ }
+
+ #[gpui::test]
+ async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
+ cx.update(|cx| {
+ init_test(cx);
+ });
+
+ let mut cx = EditorTestContext::new(cx).await;
+ let editor = cx.editor.clone();
+ let window = cx.window.clone();
+ cx.update_window(window, |cx| {
+ let text_layout_details =
+ editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
+
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+
+ let buffer =
+ cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
+ let multibuffer = cx.add_model(|cx| {
+ let mut multibuffer = MultiBuffer::new(0);
+ multibuffer.push_excerpts(
+ buffer.clone(),
+ [
+ ExcerptRange {
+ context: Point::new(0, 0)..Point::new(1, 4),
+ primary: None,
+ },
+ ExcerptRange {
+ context: Point::new(2, 0)..Point::new(3, 2),
+ primary: None,
+ },
+ ],
+ cx,
+ );
+ multibuffer
+ });
+ let display_map =
+ cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
+ let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
+
+ let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
+
+ // Can't move up into the first excerpt's header
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 2),
+ SelectionGoal::HorizontalPosition(col_2_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 0),
+ SelectionGoal::HorizontalPosition(0.0)
+ ),
+ );
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(2, 0),
+ SelectionGoal::None,
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 0),
+ SelectionGoal::HorizontalPosition(0.0)
+ ),
+ );
+
+ let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
+
+ // Move up and down within first excerpt
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_4_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(2, 3),
+ SelectionGoal::HorizontalPosition(col_4_x)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(2, 3),
+ SelectionGoal::HorizontalPosition(col_4_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_4_x)
+ ),
+ );
+
+ let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
+
+ // Move up and down across second excerpt's header
+ assert_eq!(
+ up(
+ &snapshot,
+ DisplayPoint::new(6, 5),
+ SelectionGoal::HorizontalPosition(col_5_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_5_x)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(3, 4),
+ SelectionGoal::HorizontalPosition(col_5_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(6, 5),
+ SelectionGoal::HorizontalPosition(col_5_x)
+ ),
+ );
+
+ let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
+
+ // Can't move down off the end
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 0),
+ SelectionGoal::HorizontalPosition(0.0),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x)
+ ),
+ );
+ assert_eq!(
+ down(
+ &snapshot,
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x),
+ false,
+ &text_layout_details
+ ),
+ (
+ DisplayPoint::new(7, 2),
+ SelectionGoal::HorizontalPosition(max_point_x)
+ ),
+ );
+ });
+ }
+
+ fn init_test(cx: &mut gpui::AppContext) {
+ cx.set_global(SettingsStore::test(cx));
+ theme::init((), cx);
+ language::init(cx);
+ crate::init(cx);
+ Project::init_settings(cx);
+ }
+}
@@ -0,0 +1,83 @@
+use std::path::PathBuf;
+
+use db::sqlez_macros::sql;
+use db::{define_connection, query};
+
+use workspace::{ItemId, WorkspaceDb, WorkspaceId};
+
+define_connection!(
+ // Current schema shape using pseudo-rust syntax:
+ // editors(
+ // item_id: usize,
+ // workspace_id: usize,
+ // path: PathBuf,
+ // scroll_top_row: usize,
+ // scroll_vertical_offset: f32,
+ // scroll_horizontal_offset: f32,
+ // )
+ pub static ref DB: EditorDb<WorkspaceDb> =
+ &[sql! (
+ CREATE TABLE editors(
+ item_id INTEGER NOT NULL,
+ workspace_id INTEGER NOT NULL,
+ path BLOB NOT NULL,
+ PRIMARY KEY(item_id, workspace_id),
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ON UPDATE CASCADE
+ ) STRICT;
+ ),
+ sql! (
+ ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
+ ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
+ ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
+ )];
+);
+
+impl EditorDb {
+ query! {
+ pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
+ SELECT path FROM editors
+ WHERE item_id = ? AND workspace_id = ?
+ }
+ }
+
+ query! {
+ pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
+ INSERT INTO editors
+ (item_id, workspace_id, path)
+ VALUES
+ (?1, ?2, ?3)
+ ON CONFLICT DO UPDATE SET
+ item_id = ?1,
+ workspace_id = ?2,
+ path = ?3
+ }
+ }
+
+ // Returns the scroll top row, and offset
+ query! {
+ pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
+ SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
+ FROM editors
+ WHERE item_id = ? AND workspace_id = ?
+ }
+ }
+
+ query! {
+ pub async fn save_scroll_position(
+ item_id: ItemId,
+ workspace_id: WorkspaceId,
+ top_row: u32,
+ vertical_offset: f32,
+ horizontal_offset: f32
+ ) -> Result<()> {
+ UPDATE OR IGNORE editors
+ SET
+ scroll_top_row = ?3,
+ scroll_horizontal_offset = ?4,
+ scroll_vertical_offset = ?5
+ WHERE item_id = ?1 AND workspace_id = ?2
+ }
+ }
+}
@@ -0,0 +1,436 @@
+pub mod actions;
+pub mod autoscroll;
+pub mod scroll_amount;
+
+use std::{
+ cmp::Ordering,
+ time::{Duration, Instant},
+};
+
+use gpui::{
+ geometry::vector::{vec2f, Vector2F},
+ AppContext, Axis, Task, ViewContext,
+};
+use language::{Bias, Point};
+use util::ResultExt;
+use workspace::WorkspaceId;
+
+use crate::{
+ display_map::{DisplaySnapshot, ToDisplayPoint},
+ hover_popover::hide_hover,
+ persistence::DB,
+ Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
+ ToPoint,
+};
+
+use self::{
+ autoscroll::{Autoscroll, AutoscrollStrategy},
+ scroll_amount::ScrollAmount,
+};
+
+pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
+pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
+const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
+
+#[derive(Default)]
+pub struct ScrollbarAutoHide(pub bool);
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct ScrollAnchor {
+ pub offset: Vector2F,
+ pub anchor: Anchor,
+}
+
+impl ScrollAnchor {
+ fn new() -> Self {
+ Self {
+ offset: Vector2F::zero(),
+ anchor: Anchor::min(),
+ }
+ }
+
+ pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
+ let mut scroll_position = self.offset;
+ if self.anchor != Anchor::min() {
+ let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
+ scroll_position.set_y(scroll_top + scroll_position.y());
+ } else {
+ scroll_position.set_y(0.);
+ }
+ scroll_position
+ }
+
+ pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
+ self.anchor.to_point(buffer).row
+ }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct OngoingScroll {
+ last_event: Instant,
+ axis: Option<Axis>,
+}
+
+impl OngoingScroll {
+ fn new() -> Self {
+ Self {
+ last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
+ axis: None,
+ }
+ }
+
+ pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
+ const UNLOCK_PERCENT: f32 = 1.9;
+ const UNLOCK_LOWER_BOUND: f32 = 6.;
+ let mut axis = self.axis;
+
+ let x = delta.x().abs();
+ let y = delta.y().abs();
+ let duration = Instant::now().duration_since(self.last_event);
+ if duration > SCROLL_EVENT_SEPARATION {
+ //New ongoing scroll will start, determine axis
+ axis = if x <= y {
+ Some(Axis::Vertical)
+ } else {
+ Some(Axis::Horizontal)
+ };
+ } else if x.max(y) >= UNLOCK_LOWER_BOUND {
+ //Check if the current ongoing will need to unlock
+ match axis {
+ Some(Axis::Vertical) => {
+ if x > y && x >= y * UNLOCK_PERCENT {
+ axis = None;
+ }
+ }
+
+ Some(Axis::Horizontal) => {
+ if y > x && y >= x * UNLOCK_PERCENT {
+ axis = None;
+ }
+ }
+
+ None => {}
+ }
+ }
+
+ match axis {
+ Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
+ Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
+ None => {}
+ }
+
+ axis
+ }
+}
+
+pub struct ScrollManager {
+ vertical_scroll_margin: f32,
+ anchor: ScrollAnchor,
+ ongoing: OngoingScroll,
+ autoscroll_request: Option<(Autoscroll, bool)>,
+ last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
+ show_scrollbars: bool,
+ hide_scrollbar_task: Option<Task<()>>,
+ visible_line_count: Option<f32>,
+}
+
+impl ScrollManager {
+ pub fn new() -> Self {
+ ScrollManager {
+ vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
+ anchor: ScrollAnchor::new(),
+ ongoing: OngoingScroll::new(),
+ autoscroll_request: None,
+ show_scrollbars: true,
+ hide_scrollbar_task: None,
+ last_autoscroll: None,
+ visible_line_count: None,
+ }
+ }
+
+ pub fn clone_state(&mut self, other: &Self) {
+ self.anchor = other.anchor;
+ self.ongoing = other.ongoing;
+ }
+
+ pub fn anchor(&self) -> ScrollAnchor {
+ self.anchor
+ }
+
+ pub fn ongoing_scroll(&self) -> OngoingScroll {
+ self.ongoing
+ }
+
+ pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
+ self.ongoing.last_event = Instant::now();
+ self.ongoing.axis = axis;
+ }
+
+ pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
+ self.anchor.scroll_position(snapshot)
+ }
+
+ fn set_scroll_position(
+ &mut self,
+ scroll_position: Vector2F,
+ map: &DisplaySnapshot,
+ local: bool,
+ autoscroll: bool,
+ workspace_id: Option<i64>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let (new_anchor, top_row) = if scroll_position.y() <= 0. {
+ (
+ ScrollAnchor {
+ anchor: Anchor::min(),
+ offset: scroll_position.max(vec2f(0., 0.)),
+ },
+ 0,
+ )
+ } else {
+ let scroll_top_buffer_point =
+ DisplayPoint::new(scroll_position.y() as u32, 0).to_point(&map);
+ let top_anchor = map
+ .buffer_snapshot
+ .anchor_at(scroll_top_buffer_point, Bias::Right);
+
+ (
+ ScrollAnchor {
+ anchor: top_anchor,
+ offset: vec2f(
+ scroll_position.x(),
+ scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
+ ),
+ },
+ scroll_top_buffer_point.row,
+ )
+ };
+
+ self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
+ }
+
+ fn set_anchor(
+ &mut self,
+ anchor: ScrollAnchor,
+ top_row: u32,
+ local: bool,
+ autoscroll: bool,
+ workspace_id: Option<i64>,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ self.anchor = anchor;
+ cx.emit(Event::ScrollPositionChanged { local, autoscroll });
+ self.show_scrollbar(cx);
+ self.autoscroll_request.take();
+ if let Some(workspace_id) = workspace_id {
+ let item_id = cx.view_id();
+
+ cx.background()
+ .spawn(async move {
+ DB.save_scroll_position(
+ item_id,
+ workspace_id,
+ top_row,
+ anchor.offset.x(),
+ anchor.offset.y(),
+ )
+ .await
+ .log_err()
+ })
+ .detach()
+ }
+ cx.notify();
+ }
+
+ pub fn show_scrollbar(&mut self, cx: &mut ViewContext<Editor>) {
+ if !self.show_scrollbars {
+ self.show_scrollbars = true;
+ cx.notify();
+ }
+
+ if cx.default_global::<ScrollbarAutoHide>().0 {
+ self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move {
+ cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await;
+ editor
+ .update(&mut cx, |editor, cx| {
+ editor.scroll_manager.show_scrollbars = false;
+ cx.notify();
+ })
+ .log_err();
+ }));
+ } else {
+ self.hide_scrollbar_task = None;
+ }
+ }
+
+ pub fn scrollbars_visible(&self) -> bool {
+ self.show_scrollbars
+ }
+
+ pub fn has_autoscroll_request(&self) -> bool {
+ self.autoscroll_request.is_some()
+ }
+
+ pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
+ if max < self.anchor.offset.x() {
+ self.anchor.offset.set_x(max);
+ true
+ } else {
+ false
+ }
+ }
+}
+
+impl Editor {
+ pub fn vertical_scroll_margin(&mut self) -> usize {
+ self.scroll_manager.vertical_scroll_margin as usize
+ }
+
+ pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
+ self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
+ cx.notify();
+ }
+
+ pub fn visible_line_count(&self) -> Option<f32> {
+ self.scroll_manager.visible_line_count
+ }
+
+ pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
+ let opened_first_time = self.scroll_manager.visible_line_count.is_none();
+ self.scroll_manager.visible_line_count = Some(lines);
+ if opened_first_time {
+ cx.spawn(|editor, mut cx| async move {
+ editor
+ .update(&mut cx, |editor, cx| {
+ editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
+ })
+ .ok()
+ })
+ .detach()
+ }
+ }
+
+ pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
+ self.set_scroll_position_internal(scroll_position, true, false, cx);
+ }
+
+ pub(crate) fn set_scroll_position_internal(
+ &mut self,
+ scroll_position: Vector2F,
+ local: bool,
+ autoscroll: bool,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+ hide_hover(self, cx);
+ let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
+ self.scroll_manager.set_scroll_position(
+ scroll_position,
+ &map,
+ local,
+ autoscroll,
+ workspace_id,
+ cx,
+ );
+
+ self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+ }
+
+ pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ self.scroll_manager.anchor.scroll_position(&display_map)
+ }
+
+ pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
+ hide_hover(self, cx);
+ let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
+ let top_row = scroll_anchor
+ .anchor
+ .to_point(&self.buffer().read(cx).snapshot(cx))
+ .row;
+ self.scroll_manager
+ .set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
+ }
+
+ pub(crate) fn set_scroll_anchor_remote(
+ &mut self,
+ scroll_anchor: ScrollAnchor,
+ cx: &mut ViewContext<Self>,
+ ) {
+ hide_hover(self, cx);
+ let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
+ let top_row = scroll_anchor
+ .anchor
+ .to_point(&self.buffer().read(cx).snapshot(cx))
+ .row;
+ self.scroll_manager
+ .set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
+ }
+
+ pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return;
+ }
+
+ if self.take_rename(true, cx).is_some() {
+ return;
+ }
+
+ let cur_position = self.scroll_position(cx);
+ let new_pos = cur_position + vec2f(0., amount.lines(self));
+ self.set_scroll_position(new_pos, cx);
+ }
+
+ /// Returns an ordering. The newest selection is:
+ /// Ordering::Equal => on screen
+ /// Ordering::Less => above the screen
+ /// Ordering::Greater => below the screen
+ pub fn newest_selection_on_screen(&self, cx: &mut AppContext) -> Ordering {
+ let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let newest_head = self
+ .selections
+ .newest_anchor()
+ .head()
+ .to_display_point(&snapshot);
+ let screen_top = self
+ .scroll_manager
+ .anchor
+ .anchor
+ .to_display_point(&snapshot);
+
+ if screen_top > newest_head {
+ return Ordering::Less;
+ }
+
+ if let Some(visible_lines) = self.visible_line_count() {
+ if newest_head.row() < screen_top.row() + visible_lines as u32 {
+ return Ordering::Equal;
+ }
+ }
+
+ Ordering::Greater
+ }
+
+ pub fn read_scroll_position_from_db(
+ &mut self,
+ item_id: usize,
+ workspace_id: WorkspaceId,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let scroll_position = DB.get_scroll_position(item_id, workspace_id);
+ if let Ok(Some((top_row, x, y))) = scroll_position {
+ let top_anchor = self
+ .buffer()
+ .read(cx)
+ .snapshot(cx)
+ .anchor_at(Point::new(top_row as u32, 0), Bias::Left);
+ let scroll_anchor = ScrollAnchor {
+ offset: Vector2F::new(x, y),
+ anchor: top_anchor,
+ };
+ self.set_scroll_anchor(scroll_anchor, cx);
+ }
+ }
+}
@@ -0,0 +1,152 @@
+use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext};
+use language::Bias;
+
+use crate::{Editor, EditorMode};
+
+use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor};
+
+actions!(
+ editor,
+ [
+ LineDown,
+ LineUp,
+ HalfPageDown,
+ HalfPageUp,
+ PageDown,
+ PageUp,
+ NextScreen,
+ ScrollCursorTop,
+ ScrollCursorCenter,
+ ScrollCursorBottom,
+ ]
+);
+
+pub fn init(cx: &mut AppContext) {
+ cx.add_action(Editor::next_screen);
+ cx.add_action(Editor::scroll_cursor_top);
+ cx.add_action(Editor::scroll_cursor_center);
+ cx.add_action(Editor::scroll_cursor_bottom);
+ cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
+ this.scroll_screen(&ScrollAmount::Line(1.), cx)
+ });
+ cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
+ this.scroll_screen(&ScrollAmount::Line(-1.), cx)
+ });
+ cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
+ this.scroll_screen(&ScrollAmount::Page(0.5), cx)
+ });
+ cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
+ this.scroll_screen(&ScrollAmount::Page(-0.5), cx)
+ });
+ cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
+ this.scroll_screen(&ScrollAmount::Page(1.), cx)
+ });
+ cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
+ this.scroll_screen(&ScrollAmount::Page(-1.), cx)
+ });
+}
+
+impl Editor {
+ pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) -> Option<()> {
+ if self.take_rename(true, cx).is_some() {
+ return None;
+ }
+
+ if self.mouse_context_menu.read(cx).visible() {
+ return None;
+ }
+
+ if matches!(self.mode, EditorMode::SingleLine) {
+ cx.propagate_action();
+ return None;
+ }
+ self.request_autoscroll(Autoscroll::Next, cx);
+ Some(())
+ }
+
+ pub fn scroll(
+ &mut self,
+ scroll_position: Vector2F,
+ axis: Option<Axis>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.scroll_manager.update_ongoing_scroll(axis);
+ self.set_scroll_position(scroll_position, cx);
+ }
+
+ fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
+ let snapshot = editor.snapshot(cx).display_snapshot;
+ let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
+
+ let mut new_screen_top = editor.selections.newest_display(cx).head();
+ *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows);
+ *new_screen_top.column_mut() = 0;
+ let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+ let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+ editor.set_scroll_anchor(
+ ScrollAnchor {
+ anchor: new_anchor,
+ offset: Default::default(),
+ },
+ cx,
+ )
+ }
+
+ fn scroll_cursor_center(
+ editor: &mut Editor,
+ _: &ScrollCursorCenter,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let snapshot = editor.snapshot(cx).display_snapshot;
+ let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+ visible_rows as u32
+ } else {
+ return;
+ };
+
+ let mut new_screen_top = editor.selections.newest_display(cx).head();
+ *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2);
+ *new_screen_top.column_mut() = 0;
+ let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+ let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+ editor.set_scroll_anchor(
+ ScrollAnchor {
+ anchor: new_anchor,
+ offset: Default::default(),
+ },
+ cx,
+ )
+ }
+
+ fn scroll_cursor_bottom(
+ editor: &mut Editor,
+ _: &ScrollCursorBottom,
+ cx: &mut ViewContext<Editor>,
+ ) {
+ let snapshot = editor.snapshot(cx).display_snapshot;
+ let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
+ let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+ visible_rows as u32
+ } else {
+ return;
+ };
+
+ let mut new_screen_top = editor.selections.newest_display(cx).head();
+ *new_screen_top.row_mut() = new_screen_top
+ .row()
+ .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows));
+ *new_screen_top.column_mut() = 0;
+ let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+ let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+ editor.set_scroll_anchor(
+ ScrollAnchor {
+ anchor: new_anchor,
+ offset: Default::default(),
+ },
+ cx,
+ )
+ }
+}
@@ -0,0 +1,258 @@
+use std::cmp;
+
+use gpui::ViewContext;
+use language::Point;
+
+use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
+
+#[derive(PartialEq, Eq)]
+pub enum Autoscroll {
+ Next,
+ Strategy(AutoscrollStrategy),
+}
+
+impl Autoscroll {
+ pub fn fit() -> Self {
+ Self::Strategy(AutoscrollStrategy::Fit)
+ }
+
+ pub fn newest() -> Self {
+ Self::Strategy(AutoscrollStrategy::Newest)
+ }
+
+ pub fn center() -> Self {
+ Self::Strategy(AutoscrollStrategy::Center)
+ }
+}
+
+#[derive(PartialEq, Eq, Default)]
+pub enum AutoscrollStrategy {
+ Fit,
+ Newest,
+ #[default]
+ Center,
+ Top,
+ Bottom,
+}
+
+impl AutoscrollStrategy {
+ fn next(&self) -> Self {
+ match self {
+ AutoscrollStrategy::Center => AutoscrollStrategy::Top,
+ AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
+ _ => AutoscrollStrategy::Center,
+ }
+ }
+}
+
+impl Editor {
+ pub fn autoscroll_vertically(
+ &mut self,
+ viewport_height: f32,
+ line_height: f32,
+ cx: &mut ViewContext<Editor>,
+ ) -> bool {
+ let visible_lines = viewport_height / line_height;
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
+ let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+ (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
+ } else {
+ display_map.max_point().row() as f32
+ };
+ if scroll_position.y() > max_scroll_top {
+ scroll_position.set_y(max_scroll_top);
+ self.set_scroll_position(scroll_position, cx);
+ }
+
+ let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
+ return false;
+ };
+
+ let mut target_top;
+ let mut target_bottom;
+ if let Some(highlighted_rows) = &self.highlighted_rows {
+ target_top = highlighted_rows.start as f32;
+ target_bottom = target_top + 1.;
+ } else {
+ let selections = self.selections.all::<Point>(cx);
+ target_top = selections
+ .first()
+ .unwrap()
+ .head()
+ .to_display_point(&display_map)
+ .row() as f32;
+ target_bottom = selections
+ .last()
+ .unwrap()
+ .head()
+ .to_display_point(&display_map)
+ .row() as f32
+ + 1.0;
+
+ // If the selections can't all fit on screen, scroll to the newest.
+ if autoscroll == Autoscroll::newest()
+ || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines
+ {
+ let newest_selection_top = selections
+ .iter()
+ .max_by_key(|s| s.id)
+ .unwrap()
+ .head()
+ .to_display_point(&display_map)
+ .row() as f32;
+ target_top = newest_selection_top;
+ target_bottom = newest_selection_top + 1.;
+ }
+ }
+
+ let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+ 0.
+ } else {
+ ((visible_lines - (target_bottom - target_top)) / 2.0).floor()
+ };
+
+ let strategy = match autoscroll {
+ Autoscroll::Strategy(strategy) => strategy,
+ Autoscroll::Next => {
+ let last_autoscroll = &self.scroll_manager.last_autoscroll;
+ if let Some(last_autoscroll) = last_autoscroll {
+ if self.scroll_manager.anchor.offset == last_autoscroll.0
+ && target_top == last_autoscroll.1
+ && target_bottom == last_autoscroll.2
+ {
+ last_autoscroll.3.next()
+ } else {
+ AutoscrollStrategy::default()
+ }
+ } else {
+ AutoscrollStrategy::default()
+ }
+ }
+ };
+
+ match strategy {
+ AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
+ let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
+ let target_top = (target_top - margin).max(0.0);
+ let target_bottom = target_bottom + margin;
+ let start_row = scroll_position.y();
+ let end_row = start_row + visible_lines;
+
+ let needs_scroll_up = target_top < start_row;
+ let needs_scroll_down = target_bottom >= end_row;
+
+ if needs_scroll_up && !needs_scroll_down {
+ scroll_position.set_y(target_top);
+ self.set_scroll_position_internal(scroll_position, local, true, cx);
+ }
+ if !needs_scroll_up && needs_scroll_down {
+ scroll_position.set_y(target_bottom - visible_lines);
+ self.set_scroll_position_internal(scroll_position, local, true, cx);
+ }
+ }
+ AutoscrollStrategy::Center => {
+ scroll_position.set_y((target_top - margin).max(0.0));
+ self.set_scroll_position_internal(scroll_position, local, true, cx);
+ }
+ AutoscrollStrategy::Top => {
+ scroll_position.set_y((target_top).max(0.0));
+ self.set_scroll_position_internal(scroll_position, local, true, cx);
+ }
+ AutoscrollStrategy::Bottom => {
+ scroll_position.set_y((target_bottom - visible_lines).max(0.0));
+ self.set_scroll_position_internal(scroll_position, local, true, cx);
+ }
+ }
+
+ self.scroll_manager.last_autoscroll = Some((
+ self.scroll_manager.anchor.offset,
+ target_top,
+ target_bottom,
+ strategy,
+ ));
+
+ true
+ }
+
+ pub fn autoscroll_horizontally(
+ &mut self,
+ start_row: u32,
+ viewport_width: f32,
+ scroll_width: f32,
+ max_glyph_width: f32,
+ layouts: &[LineWithInvisibles],
+ cx: &mut ViewContext<Self>,
+ ) -> bool {
+ let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+ let selections = self.selections.all::<Point>(cx);
+
+ let mut target_left;
+ let mut target_right;
+
+ if self.highlighted_rows.is_some() {
+ target_left = 0.0_f32;
+ target_right = 0.0_f32;
+ } else {
+ target_left = std::f32::INFINITY;
+ target_right = 0.0_f32;
+ for selection in selections {
+ let head = selection.head().to_display_point(&display_map);
+ if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
+ let start_column = head.column().saturating_sub(3);
+ let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
+ target_left = target_left.min(
+ layouts[(head.row() - start_row) as usize]
+ .line
+ .x_for_index(start_column as usize),
+ );
+ target_right = target_right.max(
+ layouts[(head.row() - start_row) as usize]
+ .line
+ .x_for_index(end_column as usize)
+ + max_glyph_width,
+ );
+ }
+ }
+ }
+
+ target_right = target_right.min(scroll_width);
+
+ if target_right - target_left > viewport_width {
+ return false;
+ }
+
+ let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width;
+ let scroll_right = scroll_left + viewport_width;
+
+ if target_left < scroll_left {
+ self.scroll_manager
+ .anchor
+ .offset
+ .set_x(target_left / max_glyph_width);
+ true
+ } else if target_right > scroll_right {
+ self.scroll_manager
+ .anchor
+ .offset
+ .set_x((target_right - viewport_width) / max_glyph_width);
+ true
+ } else {
+ false
+ }
+ }
+
+ pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
+ self.scroll_manager.autoscroll_request = Some((autoscroll, true));
+ cx.notify();
+ }
+
+ pub(crate) fn request_autoscroll_remotely(
+ &mut self,
+ autoscroll: Autoscroll,
+ cx: &mut ViewContext<Self>,
+ ) {
+ self.scroll_manager.autoscroll_request = Some((autoscroll, false));
+ cx.notify();
+ }
+}
@@ -0,0 +1,28 @@
+use crate::Editor;
+use serde::Deserialize;
+
+#[derive(Clone, PartialEq, Deserialize)]
+pub enum ScrollAmount {
+ // Scroll N lines (positive is towards the end of the document)
+ Line(f32),
+ // Scroll N pages (positive is towards the end of the document)
+ Page(f32),
+}
+
+impl ScrollAmount {
+ pub fn lines(&self, editor: &mut Editor) -> f32 {
+ match self {
+ Self::Line(count) => *count,
+ Self::Page(count) => editor
+ .visible_line_count()
+ .map(|mut l| {
+ // for full pages subtract one to leave an anchor line
+ if count.abs() == 1.0 {
+ l -= 1.0
+ }
+ (l * count).trunc()
+ })
+ .unwrap_or(0.),
+ }
+ }
+}
@@ -0,0 +1,886 @@
+use std::{
+ cell::Ref,
+ iter, mem,
+ ops::{Deref, DerefMut, Range, Sub},
+ sync::Arc,
+};
+
+use collections::HashMap;
+use gpui::{AppContext, ModelHandle};
+use itertools::Itertools;
+use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
+use util::post_inc;
+
+use crate::{
+ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+ movement::TextLayoutDetails,
+ Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
+};
+
+#[derive(Debug, Clone)]
+pub struct PendingSelection {
+ pub selection: Selection<Anchor>,
+ pub mode: SelectMode,
+}
+
+#[derive(Debug, Clone)]
+pub struct SelectionsCollection {
+ display_map: ModelHandle<DisplayMap>,
+ buffer: ModelHandle<MultiBuffer>,
+ pub next_selection_id: usize,
+ pub line_mode: bool,
+ disjoint: Arc<[Selection<Anchor>]>,
+ pending: Option<PendingSelection>,
+}
+
+impl SelectionsCollection {
+ pub fn new(display_map: ModelHandle<DisplayMap>, buffer: ModelHandle<MultiBuffer>) -> Self {
+ Self {
+ display_map,
+ buffer,
+ next_selection_id: 1,
+ line_mode: false,
+ disjoint: Arc::from([]),
+ pending: Some(PendingSelection {
+ selection: Selection {
+ id: 0,
+ start: Anchor::min(),
+ end: Anchor::min(),
+ reversed: false,
+ goal: SelectionGoal::None,
+ },
+ mode: SelectMode::Character,
+ }),
+ }
+ }
+
+ pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
+ self.display_map.update(cx, |map, cx| map.snapshot(cx))
+ }
+
+ fn buffer<'a>(&self, cx: &'a AppContext) -> Ref<'a, MultiBufferSnapshot> {
+ self.buffer.read(cx).read(cx)
+ }
+
+ pub fn clone_state(&mut self, other: &SelectionsCollection) {
+ self.next_selection_id = other.next_selection_id;
+ self.line_mode = other.line_mode;
+ self.disjoint = other.disjoint.clone();
+ self.pending = other.pending.clone();
+ }
+
+ pub fn count(&self) -> usize {
+ let mut count = self.disjoint.len();
+ if self.pending.is_some() {
+ count += 1;
+ }
+ count
+ }
+
+ /// The non-pending, non-overlapping selections. There could still be a pending
+ /// selection that overlaps these if the mouse is being dragged, etc. Returned as
+ /// selections over Anchors.
+ pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
+ self.disjoint.clone()
+ }
+
+ pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
+ self.pending
+ .as_ref()
+ .map(|pending| pending.selection.clone())
+ }
+
+ pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
+ &self,
+ cx: &AppContext,
+ ) -> Option<Selection<D>> {
+ self.pending_anchor()
+ .as_ref()
+ .map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
+ }
+
+ pub fn pending_mode(&self) -> Option<SelectMode> {
+ self.pending.as_ref().map(|pending| pending.mode.clone())
+ }
+
+ pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
+ where
+ D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+ {
+ let disjoint_anchors = &self.disjoint;
+ let mut disjoint =
+ resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
+
+ let mut pending_opt = self.pending::<D>(cx);
+
+ iter::from_fn(move || {
+ if let Some(pending) = pending_opt.as_mut() {
+ while let Some(next_selection) = disjoint.peek() {
+ if pending.start <= next_selection.end && pending.end >= next_selection.start {
+ let next_selection = disjoint.next().unwrap();
+ if next_selection.start < pending.start {
+ pending.start = next_selection.start;
+ }
+ if next_selection.end > pending.end {
+ pending.end = next_selection.end;
+ }
+ } else if next_selection.end < pending.start {
+ return disjoint.next();
+ } else {
+ break;
+ }
+ }
+
+ pending_opt.take()
+ } else {
+ disjoint.next()
+ }
+ })
+ .collect()
+ }
+
+ /// Returns all of the selections, adjusted to take into account the selection line_mode
+ pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec<Selection<Point>> {
+ let mut selections = self.all::<Point>(cx);
+ if self.line_mode {
+ let map = self.display_map(cx);
+ for selection in &mut selections {
+ let new_range = map.expand_to_line(selection.range());
+ selection.start = new_range.start;
+ selection.end = new_range.end;
+ }
+ }
+ selections
+ }
+
+ pub fn all_adjusted_display(
+ &self,
+ cx: &mut AppContext,
+ ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
+ if self.line_mode {
+ let selections = self.all::<Point>(cx);
+ let map = self.display_map(cx);
+ let result = selections
+ .into_iter()
+ .map(|mut selection| {
+ let new_range = map.expand_to_line(selection.range());
+ selection.start = new_range.start;
+ selection.end = new_range.end;
+ selection.map(|point| point.to_display_point(&map))
+ })
+ .collect();
+ (map, result)
+ } else {
+ self.all_display(cx)
+ }
+ }
+
+ pub fn disjoint_in_range<'a, D>(
+ &self,
+ range: Range<Anchor>,
+ cx: &AppContext,
+ ) -> Vec<Selection<D>>
+ where
+ D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+ {
+ let buffer = self.buffer(cx);
+ let start_ix = match self
+ .disjoint
+ .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
+ {
+ Ok(ix) | Err(ix) => ix,
+ };
+ let end_ix = match self
+ .disjoint
+ .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
+ {
+ Ok(ix) => ix + 1,
+ Err(ix) => ix,
+ };
+ resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
+ }
+
+ pub fn all_display(
+ &self,
+ cx: &mut AppContext,
+ ) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
+ let display_map = self.display_map(cx);
+ let selections = self
+ .all::<Point>(cx)
+ .into_iter()
+ .map(|selection| selection.map(|point| point.to_display_point(&display_map)))
+ .collect();
+ (display_map, selections)
+ }
+
+ pub fn newest_anchor(&self) -> &Selection<Anchor> {
+ self.pending
+ .as_ref()
+ .map(|s| &s.selection)
+ .or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
+ .unwrap()
+ }
+
+ pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
+ &self,
+ cx: &AppContext,
+ ) -> Selection<D> {
+ resolve(self.newest_anchor(), &self.buffer(cx))
+ }
+
+ pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
+ let display_map = self.display_map(cx);
+ let selection = self
+ .newest_anchor()
+ .map(|point| point.to_display_point(&display_map));
+ selection
+ }
+
+ pub fn oldest_anchor(&self) -> &Selection<Anchor> {
+ self.disjoint
+ .iter()
+ .min_by_key(|s| s.id)
+ .or_else(|| self.pending.as_ref().map(|p| &p.selection))
+ .unwrap()
+ }
+
+ pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
+ &self,
+ cx: &AppContext,
+ ) -> Selection<D> {
+ resolve(self.oldest_anchor(), &self.buffer(cx))
+ }
+
+ pub fn first_anchor(&self) -> Selection<Anchor> {
+ self.disjoint[0].clone()
+ }
+
+ pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
+ &self,
+ cx: &AppContext,
+ ) -> Selection<D> {
+ self.all(cx).first().unwrap().clone()
+ }
+
+ pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
+ &self,
+ cx: &AppContext,
+ ) -> Selection<D> {
+ self.all(cx).last().unwrap().clone()
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
+ &self,
+ cx: &AppContext,
+ ) -> Vec<Range<D>> {
+ self.all::<D>(cx)
+ .iter()
+ .map(|s| {
+ if s.reversed {
+ s.end.clone()..s.start.clone()
+ } else {
+ s.start.clone()..s.end.clone()
+ }
+ })
+ .collect()
+ }
+
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn display_ranges(&self, cx: &mut AppContext) -> Vec<Range<DisplayPoint>> {
+ let display_map = self.display_map(cx);
+ self.disjoint_anchors()
+ .iter()
+ .chain(self.pending_anchor().as_ref())
+ .map(|s| {
+ if s.reversed {
+ s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
+ } else {
+ s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
+ }
+ })
+ .collect()
+ }
+
+ pub fn build_columnar_selection(
+ &mut self,
+ display_map: &DisplaySnapshot,
+ row: u32,
+ positions: &Range<f32>,
+ reversed: bool,
+ text_layout_details: &TextLayoutDetails,
+ ) -> Option<Selection<Point>> {
+ let is_empty = positions.start == positions.end;
+ let line_len = display_map.line_len(row);
+
+ let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
+
+ let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
+ if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
+ let start = DisplayPoint::new(row, start_col);
+ let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
+ let end = DisplayPoint::new(row, end_col);
+
+ Some(Selection {
+ id: post_inc(&mut self.next_selection_id),
+ start: start.to_point(display_map),
+ end: end.to_point(display_map),
+ reversed,
+ goal: SelectionGoal::HorizontalRange {
+ start: positions.start,
+ end: positions.end,
+ },
+ })
+ } else {
+ None
+ }
+ }
+
+ pub(crate) fn change_with<R>(
+ &mut self,
+ cx: &mut AppContext,
+ change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
+ ) -> (bool, R) {
+ let mut mutable_collection = MutableSelectionsCollection {
+ collection: self,
+ selections_changed: false,
+ cx,
+ };
+
+ let result = change(&mut mutable_collection);
+ assert!(
+ !mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
+ "There must be at least one selection"
+ );
+ (mutable_collection.selections_changed, result)
+ }
+}
+
+pub struct MutableSelectionsCollection<'a> {
+ collection: &'a mut SelectionsCollection,
+ selections_changed: bool,
+ cx: &'a mut AppContext,
+}
+
+impl<'a> MutableSelectionsCollection<'a> {
+ pub fn display_map(&mut self) -> DisplaySnapshot {
+ self.collection.display_map(self.cx)
+ }
+
+ fn buffer(&self) -> Ref<MultiBufferSnapshot> {
+ self.collection.buffer(self.cx)
+ }
+
+ pub fn clear_disjoint(&mut self) {
+ self.collection.disjoint = Arc::from([]);
+ }
+
+ pub fn delete(&mut self, selection_id: usize) {
+ let mut changed = false;
+ self.collection.disjoint = self
+ .disjoint
+ .iter()
+ .filter(|selection| {
+ let found = selection.id == selection_id;
+ changed |= found;
+ !found
+ })
+ .cloned()
+ .collect();
+
+ self.selections_changed |= changed;
+ }
+
+ pub fn clear_pending(&mut self) {
+ if self.collection.pending.is_some() {
+ self.collection.pending = None;
+ self.selections_changed = true;
+ }
+ }
+
+ pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
+ self.collection.pending = Some(PendingSelection {
+ selection: Selection {
+ id: post_inc(&mut self.collection.next_selection_id),
+ start: range.start,
+ end: range.end,
+ reversed: false,
+ goal: SelectionGoal::None,
+ },
+ mode,
+ });
+ self.selections_changed = true;
+ }
+
+ pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
+ let (start, end, reversed) = {
+ let display_map = self.display_map();
+ let buffer = self.buffer();
+ let mut start = range.start;
+ let mut end = range.end;
+ let reversed = if start > end {
+ mem::swap(&mut start, &mut end);
+ true
+ } else {
+ false
+ };
+
+ let end_bias = if end > start { Bias::Left } else { Bias::Right };
+ (
+ buffer.anchor_before(start.to_point(&display_map)),
+ buffer.anchor_at(end.to_point(&display_map), end_bias),
+ reversed,
+ )
+ };
+
+ let new_pending = PendingSelection {
+ selection: Selection {
+ id: post_inc(&mut self.collection.next_selection_id),
+ start,
+ end,
+ reversed,
+ goal: SelectionGoal::None,
+ },
+ mode,
+ };
+
+ self.collection.pending = Some(new_pending);
+ self.selections_changed = true;
+ }
+
+ pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
+ self.collection.pending = Some(PendingSelection { selection, mode });
+ self.selections_changed = true;
+ }
+
+ pub fn try_cancel(&mut self) -> bool {
+ if let Some(pending) = self.collection.pending.take() {
+ if self.disjoint.is_empty() {
+ self.collection.disjoint = Arc::from([pending.selection]);
+ }
+ self.selections_changed = true;
+ return true;
+ }
+
+ let mut oldest = self.oldest_anchor().clone();
+ if self.count() > 1 {
+ self.collection.disjoint = Arc::from([oldest]);
+ self.selections_changed = true;
+ return true;
+ }
+
+ if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() {
+ let head = oldest.head();
+ oldest.start = head.clone();
+ oldest.end = head;
+ self.collection.disjoint = Arc::from([oldest]);
+ self.selections_changed = true;
+ return true;
+ }
+
+ false
+ }
+
+ pub fn insert_range<T>(&mut self, range: Range<T>)
+ where
+ T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
+ {
+ let mut selections = self.all(self.cx);
+ let mut start = range.start.to_offset(&self.buffer());
+ let mut end = range.end.to_offset(&self.buffer());
+ let reversed = if start > end {
+ mem::swap(&mut start, &mut end);
+ true
+ } else {
+ false
+ };
+ selections.push(Selection {
+ id: post_inc(&mut self.collection.next_selection_id),
+ start,
+ end,
+ reversed,
+ goal: SelectionGoal::None,
+ });
+ self.select(selections);
+ }
+
+ pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
+ where
+ T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
+ {
+ let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+ selections.sort_unstable_by_key(|s| s.start);
+ // Merge overlapping selections.
+ let mut i = 1;
+ while i < selections.len() {
+ if selections[i - 1].end >= selections[i].start {
+ let removed = selections.remove(i);
+ if removed.start < selections[i - 1].start {
+ selections[i - 1].start = removed.start;
+ }
+ if removed.end > selections[i - 1].end {
+ selections[i - 1].end = removed.end;
+ }
+ } else {
+ i += 1;
+ }
+ }
+
+ self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
+ let end_bias = if selection.end > selection.start {
+ Bias::Left
+ } else {
+ Bias::Right
+ };
+ Selection {
+ id: selection.id,
+ start: buffer.anchor_after(selection.start),
+ end: buffer.anchor_at(selection.end, end_bias),
+ reversed: selection.reversed,
+ goal: selection.goal,
+ }
+ }));
+
+ self.collection.pending = None;
+ self.selections_changed = true;
+ }
+
+ pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
+ let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+ let resolved_selections =
+ resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
+ self.select(resolved_selections);
+ }
+
+ pub fn select_ranges<I, T>(&mut self, ranges: I)
+ where
+ I: IntoIterator<Item = Range<T>>,
+ T: ToOffset,
+ {
+ let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+ let ranges = ranges
+ .into_iter()
+ .map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
+ self.select_offset_ranges(ranges);
+ }
+
+ fn select_offset_ranges<I>(&mut self, ranges: I)
+ where
+ I: IntoIterator<Item = Range<usize>>,
+ {
+ let selections = ranges
+ .into_iter()
+ .map(|range| {
+ let mut start = range.start;
+ let mut end = range.end;
+ let reversed = if start > end {
+ mem::swap(&mut start, &mut end);
+ true
+ } else {
+ false
+ };
+ Selection {
+ id: post_inc(&mut self.collection.next_selection_id),
+ start,
+ end,
+ reversed,
+ goal: SelectionGoal::None,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ self.select(selections)
+ }
+
+ pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
+ let buffer = self.buffer.read(self.cx).snapshot(self.cx);
+ let selections = ranges
+ .into_iter()
+ .map(|range| {
+ let mut start = range.start;
+ let mut end = range.end;
+ let reversed = if start.cmp(&end, &buffer).is_gt() {
+ mem::swap(&mut start, &mut end);
+ true
+ } else {
+ false
+ };
+ Selection {
+ id: post_inc(&mut self.collection.next_selection_id),
+ start,
+ end,
+ reversed,
+ goal: SelectionGoal::None,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ self.select_anchors(selections)
+ }
+
+ pub fn new_selection_id(&mut self) -> usize {
+ post_inc(&mut self.next_selection_id)
+ }
+
+ pub fn select_display_ranges<T>(&mut self, ranges: T)
+ where
+ T: IntoIterator<Item = Range<DisplayPoint>>,
+ {
+ let display_map = self.display_map();
+ let selections = ranges
+ .into_iter()
+ .map(|range| {
+ let mut start = range.start;
+ let mut end = range.end;
+ let reversed = if start > end {
+ mem::swap(&mut start, &mut end);
+ true
+ } else {
+ false
+ };
+ Selection {
+ id: post_inc(&mut self.collection.next_selection_id),
+ start: start.to_point(&display_map),
+ end: end.to_point(&display_map),
+ reversed,
+ goal: SelectionGoal::None,
+ }
+ })
+ .collect();
+ self.select(selections);
+ }
+
+ pub fn move_with(
+ &mut self,
+ mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
+ ) {
+ let mut changed = false;
+ let display_map = self.display_map();
+ let selections = self
+ .all::<Point>(self.cx)
+ .into_iter()
+ .map(|selection| {
+ let mut moved_selection =
+ selection.map(|point| point.to_display_point(&display_map));
+ move_selection(&display_map, &mut moved_selection);
+ let moved_selection =
+ moved_selection.map(|display_point| display_point.to_point(&display_map));
+ if selection != moved_selection {
+ changed = true;
+ }
+ moved_selection
+ })
+ .collect();
+
+ if changed {
+ self.select(selections)
+ }
+ }
+
+ pub fn move_offsets_with(
+ &mut self,
+ mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
+ ) {
+ let mut changed = false;
+ let snapshot = self.buffer().clone();
+ let selections = self
+ .all::<usize>(self.cx)
+ .into_iter()
+ .map(|selection| {
+ let mut moved_selection = selection.clone();
+ move_selection(&snapshot, &mut moved_selection);
+ if selection != moved_selection {
+ changed = true;
+ }
+ moved_selection
+ })
+ .collect();
+ drop(snapshot);
+
+ if changed {
+ self.select(selections)
+ }
+ }
+
+ pub fn move_heads_with(
+ &mut self,
+ mut update_head: impl FnMut(
+ &DisplaySnapshot,
+ DisplayPoint,
+ SelectionGoal,
+ ) -> (DisplayPoint, SelectionGoal),
+ ) {
+ self.move_with(|map, selection| {
+ let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
+ selection.set_head(new_head, new_goal);
+ });
+ }
+
+ pub fn move_cursors_with(
+ &mut self,
+ mut update_cursor_position: impl FnMut(
+ &DisplaySnapshot,
+ DisplayPoint,
+ SelectionGoal,
+ ) -> (DisplayPoint, SelectionGoal),
+ ) {
+ self.move_with(|map, selection| {
+ let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
+ selection.collapse_to(cursor, new_goal)
+ });
+ }
+
+ pub fn maybe_move_cursors_with(
+ &mut self,
+ mut update_cursor_position: impl FnMut(
+ &DisplaySnapshot,
+ DisplayPoint,
+ SelectionGoal,
+ ) -> Option<(DisplayPoint, SelectionGoal)>,
+ ) {
+ self.move_cursors_with(|map, point, goal| {
+ update_cursor_position(map, point, goal).unwrap_or((point, goal))
+ })
+ }
+
+ pub fn replace_cursors_with(
+ &mut self,
+ mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
+ ) {
+ let display_map = self.display_map();
+ let new_selections = find_replacement_cursors(&display_map)
+ .into_iter()
+ .map(|cursor| {
+ let cursor_point = cursor.to_point(&display_map);
+ Selection {
+ id: post_inc(&mut self.collection.next_selection_id),
+ start: cursor_point,
+ end: cursor_point,
+ reversed: false,
+ goal: SelectionGoal::None,
+ }
+ })
+ .collect();
+ self.select(new_selections);
+ }
+
+ /// Compute new ranges for any selections that were located in excerpts that have
+ /// since been removed.
+ ///
+ /// Returns a `HashMap` indicating which selections whose former head position
+ /// was no longer present. The keys of the map are selection ids. The values are
+ /// the id of the new excerpt where the head of the selection has been moved.
+ pub fn refresh(&mut self) -> HashMap<usize, ExcerptId> {
+ let mut pending = self.collection.pending.take();
+ let mut selections_with_lost_position = HashMap::default();
+
+ let anchors_with_status = {
+ let buffer = self.buffer();
+ let disjoint_anchors = self
+ .disjoint
+ .iter()
+ .flat_map(|selection| [&selection.start, &selection.end]);
+ buffer.refresh_anchors(disjoint_anchors)
+ };
+ let adjusted_disjoint: Vec<_> = anchors_with_status
+ .chunks(2)
+ .map(|selection_anchors| {
+ let (anchor_ix, start, kept_start) = selection_anchors[0].clone();
+ let (_, end, kept_end) = selection_anchors[1].clone();
+ let selection = &self.disjoint[anchor_ix / 2];
+ let kept_head = if selection.reversed {
+ kept_start
+ } else {
+ kept_end
+ };
+ if !kept_head {
+ selections_with_lost_position.insert(selection.id, selection.head().excerpt_id);
+ }
+
+ Selection {
+ id: selection.id,
+ start,
+ end,
+ reversed: selection.reversed,
+ goal: selection.goal,
+ }
+ })
+ .collect();
+
+ if !adjusted_disjoint.is_empty() {
+ let resolved_selections =
+ resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
+ self.select::<usize>(resolved_selections);
+ }
+
+ if let Some(pending) = pending.as_mut() {
+ let buffer = self.buffer();
+ let anchors =
+ buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]);
+ let (_, start, kept_start) = anchors[0].clone();
+ let (_, end, kept_end) = anchors[1].clone();
+ let kept_head = if pending.selection.reversed {
+ kept_start
+ } else {
+ kept_end
+ };
+ if !kept_head {
+ selections_with_lost_position
+ .insert(pending.selection.id, pending.selection.head().excerpt_id);
+ }
+
+ pending.selection.start = start;
+ pending.selection.end = end;
+ }
+ self.collection.pending = pending;
+ self.selections_changed = true;
+
+ selections_with_lost_position
+ }
+}
+
+impl<'a> Deref for MutableSelectionsCollection<'a> {
+ type Target = SelectionsCollection;
+ fn deref(&self) -> &Self::Target {
+ self.collection
+ }
+}
+
+impl<'a> DerefMut for MutableSelectionsCollection<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ self.collection
+ }
+}
+
+// Panics if passed selections are not in order
+pub fn resolve_multiple<'a, D, I>(
+ selections: I,
+ snapshot: &MultiBufferSnapshot,
+) -> impl 'a + Iterator<Item = Selection<D>>
+where
+ D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
+ I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
+{
+ let (to_summarize, selections) = selections.into_iter().tee();
+ let mut summaries = snapshot
+ .summaries_for_anchors::<D, _>(
+ to_summarize
+ .flat_map(|s| [&s.start, &s.end])
+ .collect::<Vec<_>>(),
+ )
+ .into_iter();
+ selections.map(move |s| Selection {
+ id: s.id,
+ start: summaries.next().unwrap(),
+ end: summaries.next().unwrap(),
+ reversed: s.reversed,
+ goal: s.goal,
+ })
+}
+
+fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
+ selection: &Selection<Anchor>,
+ buffer: &MultiBufferSnapshot,
+) -> Selection<D> {
+ selection.map(|p| p.summary::<D>(buffer))
+}
@@ -0,0 +1,83 @@
+pub mod editor_lsp_test_context;
+pub mod editor_test_context;
+
+use crate::{
+ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
+ DisplayPoint, Editor, EditorMode, MultiBuffer,
+};
+
+use gpui::{ModelHandle, ViewContext};
+
+use project::Project;
+use util::test::{marked_text_offsets, marked_text_ranges};
+
+#[cfg(test)]
+#[ctor::ctor]
+fn init_logger() {
+ if std::env::var("RUST_LOG").is_ok() {
+ env_logger::init();
+ }
+}
+
+// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
+pub fn marked_display_snapshot(
+ text: &str,
+ cx: &mut gpui::AppContext,
+) -> (DisplaySnapshot, Vec<DisplayPoint>) {
+ let (unmarked_text, markers) = marked_text_offsets(text);
+
+ let family_id = cx
+ .font_cache()
+ .load_family(&["Helvetica"], &Default::default())
+ .unwrap();
+ let font_id = cx
+ .font_cache()
+ .select_font(family_id, &Default::default())
+ .unwrap();
+ let font_size = 14.0;
+
+ let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
+ let display_map =
+ cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
+ let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
+ let markers = markers
+ .into_iter()
+ .map(|offset| offset.to_display_point(&snapshot))
+ .collect();
+
+ (snapshot, markers)
+}
+
+pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
+ let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
+ assert_eq!(editor.text(cx), unmarked_text);
+ editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
+}
+
+pub fn assert_text_with_selections(
+ editor: &mut Editor,
+ marked_text: &str,
+ cx: &mut ViewContext<Editor>,
+) {
+ let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
+ assert_eq!(editor.text(cx), unmarked_text);
+ assert_eq!(editor.selections.ranges(cx), text_ranges);
+}
+
+// RA thinks this is dead code even though it is used in a whole lot of tests
+#[allow(dead_code)]
+#[cfg(any(test, feature = "test-support"))]
+pub(crate) fn build_editor(
+ buffer: ModelHandle<MultiBuffer>,
+ cx: &mut ViewContext<Editor>,
+) -> Editor {
+ Editor::new(EditorMode::Full, buffer, None, None, cx)
+}
+
+pub(crate) fn build_editor_with_project(
+ project: ModelHandle<Project>,
+ buffer: ModelHandle<MultiBuffer>,
+ cx: &mut ViewContext<Editor>,
+) -> Editor {
+ Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
+}
@@ -0,0 +1,297 @@
+use std::{
+ borrow::Cow,
+ ops::{Deref, DerefMut, Range},
+ sync::Arc,
+};
+
+use anyhow::Result;
+
+use crate::{Editor, ToPoint};
+use collections::HashSet;
+use futures::Future;
+use gpui::{json, ViewContext, ViewHandle};
+use indoc::indoc;
+use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
+use lsp::{notification, request};
+use multi_buffer::ToPointUtf16;
+use project::Project;
+use smol::stream::StreamExt;
+use workspace::{AppState, Workspace, WorkspaceHandle};
+
+use super::editor_test_context::EditorTestContext;
+
+pub struct EditorLspTestContext<'a> {
+ pub cx: EditorTestContext<'a>,
+ pub lsp: lsp::FakeLanguageServer,
+ pub workspace: ViewHandle<Workspace>,
+ pub buffer_lsp_url: lsp::Url,
+}
+
+impl<'a> EditorLspTestContext<'a> {
+ pub async fn new(
+ mut language: Language,
+ capabilities: lsp::ServerCapabilities,
+ cx: &'a mut gpui::TestAppContext,
+ ) -> EditorLspTestContext<'a> {
+ use json::json;
+
+ let app_state = cx.update(AppState::test);
+
+ cx.update(|cx| {
+ language::init(cx);
+ crate::init(cx);
+ workspace::init(app_state.clone(), cx);
+ Project::init_settings(cx);
+ });
+
+ let file_name = format!(
+ "file.{}",
+ language
+ .path_suffixes()
+ .first()
+ .expect("language must have a path suffix for EditorLspTestContext")
+ );
+
+ let mut fake_servers = language
+ .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
+ capabilities,
+ ..Default::default()
+ }))
+ .await;
+
+ let project = Project::test(app_state.fs.clone(), [], cx).await;
+ project.update(cx, |project, _| project.languages().add(Arc::new(language)));
+
+ app_state
+ .fs
+ .as_fake()
+ .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
+ .await;
+
+ let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
+ let workspace = window.root(cx);
+ project
+ .update(cx, |project, cx| {
+ project.find_or_create_local_worktree("/root", true, cx)
+ })
+ .await
+ .unwrap();
+ cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
+ .await;
+
+ let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
+ let item = workspace
+ .update(cx, |workspace, cx| {
+ workspace.open_path(file, None, true, cx)
+ })
+ .await
+ .expect("Could not open test file");
+
+ let editor = cx.update(|cx| {
+ item.act_as::<Editor>(cx)
+ .expect("Opened test file wasn't an editor")
+ });
+ editor.update(cx, |_, cx| cx.focus_self());
+
+ let lsp = fake_servers.next().await.unwrap();
+
+ Self {
+ cx: EditorTestContext {
+ cx,
+ window: window.into(),
+ editor,
+ },
+ lsp,
+ workspace,
+ buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
+ }
+ }
+
+ pub async fn new_rust(
+ capabilities: lsp::ServerCapabilities,
+ cx: &'a mut gpui::TestAppContext,
+ ) -> EditorLspTestContext<'a> {
+ let language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::language()),
+ )
+ .with_queries(LanguageQueries {
+ indents: Some(Cow::from(indoc! {r#"
+ [
+ ((where_clause) _ @end)
+ (field_expression)
+ (call_expression)
+ (assignment_expression)
+ (let_declaration)
+ (let_chain)
+ (await_expression)
+ ] @indent
+
+ (_ "[" "]" @end) @indent
+ (_ "<" ">" @end) @indent
+ (_ "{" "}" @end) @indent
+ (_ "(" ")" @end) @indent"#})),
+ brackets: Some(Cow::from(indoc! {r#"
+ ("(" @open ")" @close)
+ ("[" @open "]" @close)
+ ("{" @open "}" @close)
+ ("<" @open ">" @close)
+ ("\"" @open "\"" @close)
+ (closure_parameters "|" @open "|" @close)"#})),
+ ..Default::default()
+ })
+ .expect("Could not parse queries");
+
+ Self::new(language, capabilities, cx).await
+ }
+
+ pub async fn new_typescript(
+ capabilities: lsp::ServerCapabilities,
+ cx: &'a mut gpui::TestAppContext,
+ ) -> EditorLspTestContext<'a> {
+ let mut word_characters: HashSet<char> = Default::default();
+ word_characters.insert('$');
+ word_characters.insert('#');
+ let language = Language::new(
+ LanguageConfig {
+ name: "Typescript".into(),
+ path_suffixes: vec!["ts".to_string()],
+ brackets: language::BracketPairConfig {
+ pairs: vec![language::BracketPair {
+ start: "{".to_string(),
+ end: "}".to_string(),
+ close: true,
+ newline: true,
+ }],
+ disabled_scopes_by_bracket_ix: Default::default(),
+ },
+ word_characters,
+ ..Default::default()
+ },
+ Some(tree_sitter_typescript::language_typescript()),
+ )
+ .with_queries(LanguageQueries {
+ brackets: Some(Cow::from(indoc! {r#"
+ ("(" @open ")" @close)
+ ("[" @open "]" @close)
+ ("{" @open "}" @close)
+ ("<" @open ">" @close)
+ ("\"" @open "\"" @close)"#})),
+ indents: Some(Cow::from(indoc! {r#"
+ [
+ (call_expression)
+ (assignment_expression)
+ (member_expression)
+ (lexical_declaration)
+ (variable_declaration)
+ (assignment_expression)
+ (if_statement)
+ (for_statement)
+ ] @indent
+
+ (_ "[" "]" @end) @indent
+ (_ "<" ">" @end) @indent
+ (_ "{" "}" @end) @indent
+ (_ "(" ")" @end) @indent
+ "#})),
+ ..Default::default()
+ })
+ .expect("Could not parse queries");
+
+ Self::new(language, capabilities, cx).await
+ }
+
+ // Constructs lsp range using a marked string with '[', ']' range delimiters
+ pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
+ let ranges = self.ranges(marked_text);
+ self.to_lsp_range(ranges[0].clone())
+ }
+
+ pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
+ let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+ let start_point = range.start.to_point(&snapshot.buffer_snapshot);
+ let end_point = range.end.to_point(&snapshot.buffer_snapshot);
+
+ self.editor(|editor, cx| {
+ let buffer = editor.buffer().read(cx);
+ let start = point_to_lsp(
+ buffer
+ .point_to_buffer_offset(start_point, cx)
+ .unwrap()
+ .1
+ .to_point_utf16(&buffer.read(cx)),
+ );
+ let end = point_to_lsp(
+ buffer
+ .point_to_buffer_offset(end_point, cx)
+ .unwrap()
+ .1
+ .to_point_utf16(&buffer.read(cx)),
+ );
+
+ lsp::Range { start, end }
+ })
+ }
+
+ pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
+ let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+ let point = offset.to_point(&snapshot.buffer_snapshot);
+
+ self.editor(|editor, cx| {
+ let buffer = editor.buffer().read(cx);
+ point_to_lsp(
+ buffer
+ .point_to_buffer_offset(point, cx)
+ .unwrap()
+ .1
+ .to_point_utf16(&buffer.read(cx)),
+ )
+ })
+ }
+
+ pub fn update_workspace<F, T>(&mut self, update: F) -> T
+ where
+ F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
+ {
+ self.workspace.update(self.cx.cx, update)
+ }
+
+ pub fn handle_request<T, F, Fut>(
+ &self,
+ mut handler: F,
+ ) -> futures::channel::mpsc::UnboundedReceiver<()>
+ where
+ T: 'static + request::Request,
+ T::Params: 'static + Send,
+ F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
+ Fut: 'static + Send + Future<Output = Result<T::Result>>,
+ {
+ let url = self.buffer_lsp_url.clone();
+ self.lsp.handle_request::<T, _, _>(move |params, cx| {
+ let url = url.clone();
+ handler(url, params, cx)
+ })
+ }
+
+ pub fn notify<T: notification::Notification>(&self, params: T::Params) {
+ self.lsp.notify::<T>(params);
+ }
+}
+
+impl<'a> Deref for EditorLspTestContext<'a> {
+ type Target = EditorTestContext<'a>;
+
+ fn deref(&self) -> &Self::Target {
+ &self.cx
+ }
+}
+
+impl<'a> DerefMut for EditorLspTestContext<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.cx
+ }
+}
@@ -0,0 +1,332 @@
+use crate::{
+ display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
+};
+use futures::Future;
+use gpui::{
+ executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle,
+ ModelContext, ViewContext, ViewHandle,
+};
+use indoc::indoc;
+use language::{Buffer, BufferSnapshot};
+use project::{FakeFs, Project};
+use std::{
+ any::TypeId,
+ ops::{Deref, DerefMut, Range},
+};
+use util::{
+ assert_set_eq,
+ test::{generate_marked_text, marked_text_ranges},
+};
+
+use super::build_editor_with_project;
+
+pub struct EditorTestContext<'a> {
+ pub cx: &'a mut gpui::TestAppContext,
+ pub window: AnyWindowHandle,
+ pub editor: ViewHandle<Editor>,
+}
+
+impl<'a> EditorTestContext<'a> {
+ pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
+ let fs = FakeFs::new(cx.background());
+ // fs.insert_file("/file", "".to_owned()).await;
+ fs.insert_tree(
+ "/root",
+ gpui::serde_json::json!({
+ "file": "",
+ }),
+ )
+ .await;
+ let project = Project::test(fs, ["/root".as_ref()], cx).await;
+ let buffer = project
+ .update(cx, |project, cx| {
+ project.open_local_buffer("/root/file", cx)
+ })
+ .await
+ .unwrap();
+ let window = cx.add_window(|cx| {
+ cx.focus_self();
+ build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
+ });
+ let editor = window.root(cx);
+ Self {
+ cx,
+ window: window.into(),
+ editor,
+ }
+ }
+
+ pub fn condition(
+ &self,
+ predicate: impl FnMut(&Editor, &AppContext) -> bool,
+ ) -> impl Future<Output = ()> {
+ self.editor.condition(self.cx, predicate)
+ }
+
+ pub fn editor<F, T>(&self, read: F) -> T
+ where
+ F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
+ {
+ self.editor.read_with(self.cx, read)
+ }
+
+ pub fn update_editor<F, T>(&mut self, update: F) -> T
+ where
+ F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
+ {
+ self.editor.update(self.cx, update)
+ }
+
+ pub fn multibuffer<F, T>(&self, read: F) -> T
+ where
+ F: FnOnce(&MultiBuffer, &AppContext) -> T,
+ {
+ self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
+ }
+
+ pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
+ where
+ F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
+ {
+ self.update_editor(|editor, cx| editor.buffer().update(cx, update))
+ }
+
+ pub fn buffer_text(&self) -> String {
+ self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
+ }
+
+ pub fn buffer<F, T>(&self, read: F) -> T
+ where
+ F: FnOnce(&Buffer, &AppContext) -> T,
+ {
+ self.multibuffer(|multibuffer, cx| {
+ let buffer = multibuffer.as_singleton().unwrap().read(cx);
+ read(buffer, cx)
+ })
+ }
+
+ pub fn update_buffer<F, T>(&mut self, update: F) -> T
+ where
+ F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
+ {
+ self.update_multibuffer(|multibuffer, cx| {
+ let buffer = multibuffer.as_singleton().unwrap();
+ buffer.update(cx, update)
+ })
+ }
+
+ pub fn buffer_snapshot(&self) -> BufferSnapshot {
+ self.buffer(|buffer, _| buffer.snapshot())
+ }
+
+ pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
+ let keystroke_under_test_handle =
+ self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
+ let keystroke = Keystroke::parse(keystroke_text).unwrap();
+
+ self.cx.dispatch_keystroke(self.window, keystroke, false);
+
+ keystroke_under_test_handle
+ }
+
+ pub fn simulate_keystrokes<const COUNT: usize>(
+ &mut self,
+ keystroke_texts: [&str; COUNT],
+ ) -> ContextHandle {
+ let keystrokes_under_test_handle =
+ self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
+ for keystroke_text in keystroke_texts.into_iter() {
+ self.simulate_keystroke(keystroke_text);
+ }
+ // it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
+ // before returning.
+ // NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
+ // quickly races with async actions.
+ if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
+ executor.run_until_parked();
+ } else {
+ unreachable!();
+ }
+
+ keystrokes_under_test_handle
+ }
+
+ pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
+ let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
+ assert_eq!(self.buffer_text(), unmarked_text);
+ ranges
+ }
+
+ pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
+ let ranges = self.ranges(marked_text);
+ let snapshot = self
+ .editor
+ .update(self.cx, |editor, cx| editor.snapshot(cx));
+ ranges[0].start.to_display_point(&snapshot)
+ }
+
+ // Returns anchors for the current buffer using `ยซ` and `ยป`
+ pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
+ let ranges = self.ranges(marked_text);
+ let snapshot = self.buffer_snapshot();
+ snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
+ }
+
+ pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
+ let diff_base = diff_base.map(String::from);
+ self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
+ }
+
+ /// Change the editor's text and selections using a string containing
+ /// embedded range markers that represent the ranges and directions of
+ /// each selection.
+ ///
+ /// Returns a context handle so that assertion failures can print what
+ /// editor state was needed to cause the failure.
+ ///
+ /// See the `util::test::marked_text_ranges` function for more information.
+ pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
+ let state_context = self.add_assertion_context(format!(
+ "Initial Editor State: \"{}\"",
+ marked_text.escape_debug().to_string()
+ ));
+ let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
+ self.editor.update(self.cx, |editor, cx| {
+ editor.set_text(unmarked_text, cx);
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(selection_ranges)
+ })
+ });
+ state_context
+ }
+
+ /// Only change the editor's selections
+ pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
+ let state_context = self.add_assertion_context(format!(
+ "Initial Editor State: \"{}\"",
+ marked_text.escape_debug().to_string()
+ ));
+ let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
+ self.editor.update(self.cx, |editor, cx| {
+ assert_eq!(editor.text(cx), unmarked_text);
+ editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
+ s.select_ranges(selection_ranges)
+ })
+ });
+ state_context
+ }
+
+ /// Make an assertion about the editor's text and the ranges and directions
+ /// of its selections using a string containing embedded range markers.
+ ///
+ /// See the `util::test::marked_text_ranges` function for more information.
+ #[track_caller]
+ pub fn assert_editor_state(&mut self, marked_text: &str) {
+ let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
+ let buffer_text = self.buffer_text();
+
+ if buffer_text != unmarked_text {
+ panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
+ }
+
+ self.assert_selections(expected_selections, marked_text.to_string())
+ }
+
+ pub fn editor_state(&mut self) -> String {
+ generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
+ }
+
+ #[track_caller]
+ pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
+ let expected_ranges = self.ranges(marked_text);
+ let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
+ let snapshot = editor.snapshot(cx);
+ editor
+ .background_highlights
+ .get(&TypeId::of::<Tag>())
+ .map(|h| h.1.clone())
+ .unwrap_or_default()
+ .into_iter()
+ .map(|range| range.to_offset(&snapshot.buffer_snapshot))
+ .collect()
+ });
+ assert_set_eq!(actual_ranges, expected_ranges);
+ }
+
+ #[track_caller]
+ pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
+ let expected_ranges = self.ranges(marked_text);
+ let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
+ let actual_ranges: Vec<Range<usize>> = snapshot
+ .text_highlight_ranges::<Tag>()
+ .map(|ranges| ranges.as_ref().clone().1)
+ .unwrap_or_default()
+ .into_iter()
+ .map(|range| range.to_offset(&snapshot.buffer_snapshot))
+ .collect();
+ assert_set_eq!(actual_ranges, expected_ranges);
+ }
+
+ #[track_caller]
+ pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
+ let expected_marked_text =
+ generate_marked_text(&self.buffer_text(), &expected_selections, true);
+ self.assert_selections(expected_selections, expected_marked_text)
+ }
+
+ fn editor_selections(&self) -> Vec<Range<usize>> {
+ self.editor
+ .read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
+ .into_iter()
+ .map(|s| {
+ if s.reversed {
+ s.end..s.start
+ } else {
+ s.start..s.end
+ }
+ })
+ .collect::<Vec<_>>()
+ }
+
+ #[track_caller]
+ fn assert_selections(
+ &mut self,
+ expected_selections: Vec<Range<usize>>,
+ expected_marked_text: String,
+ ) {
+ let actual_selections = self.editor_selections();
+ let actual_marked_text =
+ generate_marked_text(&self.buffer_text(), &actual_selections, true);
+ if expected_selections != actual_selections {
+ panic!(
+ indoc! {"
+
+ {}Editor has unexpected selections.
+
+ Expected selections:
+ {}
+
+ Actual selections:
+ {}
+ "},
+ self.assertion_context(),
+ expected_marked_text,
+ actual_marked_text,
+ );
+ }
+ }
+}
+
+impl<'a> Deref for EditorTestContext<'a> {
+ type Target = gpui::TestAppContext;
+
+ fn deref(&self) -> &Self::Target {
+ self.cx
+ }
+}
+
+impl<'a> DerefMut for EditorTestContext<'a> {
+ fn deref_mut(&mut self) -> &mut Self::Target {
+ &mut self.cx
+ }
+}