Merge branch 'main' into multi-server-completions-tailwind
This commit is contained in:
@@ -37,10 +37,7 @@ impl BlinkManager {
|
||||
}
|
||||
|
||||
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
|
||||
if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
self.show_cursor(cx);
|
||||
|
||||
let epoch = self.next_blink_epoch();
|
||||
let interval = self.blink_interval;
|
||||
@@ -82,7 +79,13 @@ impl BlinkManager {
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
} else if !self.visible {
|
||||
} else {
|
||||
self.show_cursor(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
|
||||
if !self.visible {
|
||||
self.visible = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ mod inlay_map;
|
||||
mod tab_map;
|
||||
mod wrap_map;
|
||||
|
||||
use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint};
|
||||
use crate::{
|
||||
link_go_to_definition::{DocumentRange, InlayRange},
|
||||
Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint,
|
||||
};
|
||||
pub use block_map::{BlockMap, BlockPoint};
|
||||
use collections::{HashMap, HashSet};
|
||||
use fold_map::FoldMap;
|
||||
@@ -27,7 +30,8 @@ pub use block_map::{
|
||||
BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock,
|
||||
};
|
||||
|
||||
pub use self::inlay_map::Inlay;
|
||||
pub use self::fold_map::FoldPoint;
|
||||
pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub enum FoldStatus {
|
||||
@@ -39,7 +43,7 @@ pub trait ToDisplayPoint {
|
||||
fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint;
|
||||
}
|
||||
|
||||
type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<Range<Anchor>>)>>;
|
||||
type TextHighlights = TreeMap<Option<TypeId>, Arc<(HighlightStyle, Vec<DocumentRange>)>>;
|
||||
|
||||
pub struct DisplayMap {
|
||||
buffer: ModelHandle<MultiBuffer>,
|
||||
@@ -211,11 +215,28 @@ impl DisplayMap {
|
||||
ranges: Vec<Range<Anchor>>,
|
||||
style: HighlightStyle,
|
||||
) {
|
||||
self.text_highlights
|
||||
.insert(Some(type_id), Arc::new((style, ranges)));
|
||||
self.text_highlights.insert(
|
||||
Some(type_id),
|
||||
Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range<Anchor>])> {
|
||||
pub fn highlight_inlays(
|
||||
&mut self,
|
||||
type_id: TypeId,
|
||||
ranges: Vec<InlayRange>,
|
||||
style: HighlightStyle,
|
||||
) {
|
||||
self.text_highlights.insert(
|
||||
Some(type_id),
|
||||
Arc::new((
|
||||
style,
|
||||
ranges.into_iter().map(DocumentRange::Inlay).collect(),
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> {
|
||||
let highlights = self.text_highlights.get(&Some(type_id))?;
|
||||
Some((highlights.0, &highlights.1))
|
||||
}
|
||||
@@ -223,7 +244,7 @@ impl DisplayMap {
|
||||
pub fn clear_text_highlights(
|
||||
&mut self,
|
||||
type_id: TypeId,
|
||||
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
|
||||
) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
|
||||
self.text_highlights.remove(&Some(type_id))
|
||||
}
|
||||
|
||||
@@ -290,7 +311,7 @@ impl DisplayMap {
|
||||
|
||||
pub struct DisplaySnapshot {
|
||||
pub buffer_snapshot: MultiBufferSnapshot,
|
||||
fold_snapshot: fold_map::FoldSnapshot,
|
||||
pub fold_snapshot: fold_map::FoldSnapshot,
|
||||
inlay_snapshot: inlay_map::InlaySnapshot,
|
||||
tab_snapshot: tab_map::TabSnapshot,
|
||||
wrap_snapshot: wrap_map::WrapSnapshot,
|
||||
@@ -387,12 +408,49 @@ impl DisplaySnapshot {
|
||||
}
|
||||
|
||||
fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point {
|
||||
self.inlay_snapshot
|
||||
.to_buffer_point(self.display_point_to_inlay_point(point, bias))
|
||||
}
|
||||
|
||||
pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset {
|
||||
self.inlay_snapshot
|
||||
.to_offset(self.display_point_to_inlay_point(point, bias))
|
||||
}
|
||||
|
||||
pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset {
|
||||
self.inlay_snapshot
|
||||
.to_inlay_offset(anchor.to_offset(&self.buffer_snapshot))
|
||||
}
|
||||
|
||||
pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint {
|
||||
let inlay_point = self.inlay_snapshot.to_point(offset);
|
||||
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_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;
|
||||
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
|
||||
self.inlay_snapshot.to_buffer_point(inlay_point)
|
||||
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 {
|
||||
@@ -428,15 +486,15 @@ impl DisplaySnapshot {
|
||||
&self,
|
||||
display_rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> DisplayChunks<'_> {
|
||||
self.block_snapshot.chunks(
|
||||
display_rows,
|
||||
language_aware,
|
||||
Some(&self.text_highlights),
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -757,7 +815,7 @@ impl DisplaySnapshot {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn highlight_ranges<Tag: ?Sized + 'static>(
|
||||
&self,
|
||||
) -> Option<Arc<(HighlightStyle, Vec<Range<Anchor>>)>> {
|
||||
) -> Option<Arc<(HighlightStyle, Vec<DocumentRange>)>> {
|
||||
let type_id = TypeId::of::<Tag>();
|
||||
self.text_highlights.get(&Some(type_id)).cloned()
|
||||
}
|
||||
@@ -1319,7 +1377,8 @@ pub mod tests {
|
||||
|
||||
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, text, cx).with_language(language, 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));
|
||||
|
||||
@@ -1408,7 +1467,8 @@ pub mod tests {
|
||||
|
||||
cx.update(|cx| init_test(cx, |_| {}));
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, 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));
|
||||
|
||||
@@ -1480,7 +1540,8 @@ pub mod tests {
|
||||
|
||||
let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false);
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, 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));
|
||||
|
||||
@@ -589,8 +589,8 @@ impl BlockSnapshot {
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> BlockChunks<'a> {
|
||||
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
|
||||
let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>();
|
||||
@@ -623,8 +623,8 @@ impl BlockSnapshot {
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
),
|
||||
input_chunk: Default::default(),
|
||||
transforms: cursor,
|
||||
|
||||
@@ -652,8 +652,8 @@ impl FoldSnapshot {
|
||||
range: Range<FoldOffset>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> FoldChunks<'a> {
|
||||
let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>();
|
||||
|
||||
@@ -675,8 +675,8 @@ impl FoldSnapshot {
|
||||
inlay_start..inlay_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
),
|
||||
inlay_chunk: None,
|
||||
inlay_offset: inlay_start,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::{
|
||||
link_go_to_definition::DocumentRange,
|
||||
multi_buffer::{MultiBufferChunks, MultiBufferRows},
|
||||
Anchor, InlayId, MultiBufferSnapshot, ToOffset,
|
||||
};
|
||||
@@ -183,7 +184,7 @@ pub struct InlayBufferRows<'a> {
|
||||
max_buffer_row: u32,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
struct HighlightEndpoint {
|
||||
offset: InlayOffset,
|
||||
is_start: bool,
|
||||
@@ -210,6 +211,7 @@ pub struct InlayChunks<'a> {
|
||||
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,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
@@ -297,13 +299,31 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
- 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_highlight_endpoint.0 - self.output_offset.0),
|
||||
);
|
||||
*inlay_chunk = remainder;
|
||||
if inlay_chunk.is_empty() {
|
||||
self.inlay_chunk = None;
|
||||
}
|
||||
|
||||
let chunk = inlay_chunks.next().unwrap();
|
||||
self.output_offset.0 += chunk.len();
|
||||
let highlight_style = match inlay.id {
|
||||
let mut highlight_style = match inlay.id {
|
||||
InlayId::Suggestion(_) => self.suggestion_highlight_style,
|
||||
InlayId::Hint(_) => self.hint_highlight_style,
|
||||
};
|
||||
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,
|
||||
@@ -973,8 +993,8 @@ impl InlaySnapshot {
|
||||
range: Range<InlayOffset>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> InlayChunks<'a> {
|
||||
let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>();
|
||||
cursor.seek(&range.start, Bias::Right, &());
|
||||
@@ -983,52 +1003,56 @@ impl InlaySnapshot {
|
||||
if let Some(text_highlights) = text_highlights {
|
||||
if !text_highlights.is_empty() {
|
||||
while cursor.start().0 < range.end {
|
||||
if true {
|
||||
let transform_start = self.buffer.anchor_after(
|
||||
self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
|
||||
);
|
||||
let transform_start = self.buffer.anchor_after(
|
||||
self.to_buffer_offset(cmp::max(range.start, cursor.start().0)),
|
||||
);
|
||||
let transform_start =
|
||||
self.to_inlay_offset(transform_start.to_offset(&self.buffer));
|
||||
|
||||
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,
|
||||
)))
|
||||
};
|
||||
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,
|
||||
)))
|
||||
};
|
||||
let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer));
|
||||
|
||||
for (tag, highlights) in text_highlights.iter() {
|
||||
let style = highlights.0;
|
||||
let ranges = &highlights.1;
|
||||
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,
|
||||
});
|
||||
let start_ix = match ranges.binary_search_by(|probe| {
|
||||
let cmp = self
|
||||
.document_to_inlay_range(probe)
|
||||
.end
|
||||
.cmp(&transform_start);
|
||||
if cmp.is_gt() {
|
||||
cmp::Ordering::Greater
|
||||
} else {
|
||||
cmp::Ordering::Less
|
||||
}
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
let range = self.document_to_inlay_range(range);
|
||||
if range.start.cmp(&transform_end).is_ge() {
|
||||
break;
|
||||
}
|
||||
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: range.start,
|
||||
is_start: true,
|
||||
tag: *tag,
|
||||
style,
|
||||
});
|
||||
highlight_endpoints.push(HighlightEndpoint {
|
||||
offset: range.end,
|
||||
is_start: false,
|
||||
tag: *tag,
|
||||
style,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1046,17 +1070,30 @@ impl InlaySnapshot {
|
||||
transforms: cursor,
|
||||
buffer_chunks,
|
||||
inlay_chunks: None,
|
||||
inlay_chunk: None,
|
||||
buffer_chunk: None,
|
||||
output_offset: range.start,
|
||||
max_output_offset: range.end,
|
||||
hint_highlight_style: hint_highlights,
|
||||
suggestion_highlight_style: suggestion_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
highlight_endpoints: highlight_endpoints.into_iter().peekable(),
|
||||
active_highlights: Default::default(),
|
||||
snapshot: self,
|
||||
}
|
||||
}
|
||||
|
||||
fn document_to_inlay_range(&self, range: &DocumentRange) -> Range<InlayOffset> {
|
||||
match range {
|
||||
DocumentRange::Text(text_range) => {
|
||||
self.to_inlay_offset(text_range.start.to_offset(&self.buffer))
|
||||
..self.to_inlay_offset(text_range.end.to_offset(&self.buffer))
|
||||
}
|
||||
DocumentRange::Inlay(inlay_range) => {
|
||||
inlay_range.highlight_start..inlay_range.highlight_end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn text(&self) -> String {
|
||||
self.chunks(Default::default()..self.len(), false, None, None, None)
|
||||
@@ -1107,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree<Transform>, summary: TextSummary) {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{InlayId, MultiBuffer};
|
||||
use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer};
|
||||
use gpui::AppContext;
|
||||
use project::{InlayHint, InlayHintLabel};
|
||||
use project::{InlayHint, InlayHintLabel, ResolveState};
|
||||
use rand::prelude::*;
|
||||
use settings::SettingsStore;
|
||||
use std::{cmp::Reverse, env, sync::Arc};
|
||||
use sum_tree::TreeMap;
|
||||
use text::Patch;
|
||||
use util::post_inc;
|
||||
|
||||
@@ -1125,12 +1161,12 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String("a".to_string()),
|
||||
buffer_id: 0,
|
||||
position: text::Anchor::default(),
|
||||
padding_left: false,
|
||||
padding_right: false,
|
||||
tooltip: None,
|
||||
kind: None,
|
||||
resolve_state: ResolveState::Resolved,
|
||||
},
|
||||
)
|
||||
.text
|
||||
@@ -1145,12 +1181,12 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String("a".to_string()),
|
||||
buffer_id: 0,
|
||||
position: text::Anchor::default(),
|
||||
padding_left: true,
|
||||
padding_right: true,
|
||||
tooltip: None,
|
||||
kind: None,
|
||||
resolve_state: ResolveState::Resolved,
|
||||
},
|
||||
)
|
||||
.text
|
||||
@@ -1165,12 +1201,12 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String(" a ".to_string()),
|
||||
buffer_id: 0,
|
||||
position: text::Anchor::default(),
|
||||
padding_left: false,
|
||||
padding_right: false,
|
||||
tooltip: None,
|
||||
kind: None,
|
||||
resolve_state: ResolveState::Resolved,
|
||||
},
|
||||
)
|
||||
.text
|
||||
@@ -1185,12 +1221,12 @@ mod tests {
|
||||
Anchor::min(),
|
||||
&InlayHint {
|
||||
label: InlayHintLabel::String(" a ".to_string()),
|
||||
buffer_id: 0,
|
||||
position: text::Anchor::default(),
|
||||
padding_left: true,
|
||||
padding_right: true,
|
||||
tooltip: None,
|
||||
kind: None,
|
||||
resolve_state: ResolveState::Resolved,
|
||||
},
|
||||
)
|
||||
.text
|
||||
@@ -1542,26 +1578,6 @@ mod tests {
|
||||
let mut buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let mut next_inlay_id = 0;
|
||||
log::info!("buffer text: {:?}", buffer_snapshot.text());
|
||||
|
||||
let mut highlights = TreeMap::default();
|
||||
let highlight_count = rng.gen_range(0_usize..10);
|
||||
let mut highlight_ranges = (0..highlight_count)
|
||||
.map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
|
||||
.collect::<Vec<_>>();
|
||||
highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
|
||||
log::info!("highlighting ranges {:?}", highlight_ranges);
|
||||
let highlight_ranges = highlight_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
highlights.insert(
|
||||
Some(TypeId::of::<()>()),
|
||||
Arc::new((HighlightStyle::default(), highlight_ranges)),
|
||||
);
|
||||
|
||||
let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
|
||||
for _ in 0..operations {
|
||||
let mut inlay_edits = Patch::default();
|
||||
@@ -1624,6 +1640,38 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
let mut highlights = TextHighlights::default();
|
||||
let highlight_count = rng.gen_range(0_usize..10);
|
||||
let mut highlight_ranges = (0..highlight_count)
|
||||
.map(|_| buffer_snapshot.random_byte_range(0, &mut rng))
|
||||
.collect::<Vec<_>>();
|
||||
highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end)));
|
||||
log::info!("highlighting ranges {:?}", highlight_ranges);
|
||||
let highlight_ranges = if rng.gen_bool(0.5) {
|
||||
highlight_ranges
|
||||
.into_iter()
|
||||
.map(|range| InlayRange {
|
||||
inlay_position: buffer_snapshot.anchor_before(range.start),
|
||||
highlight_start: inlay_snapshot.to_inlay_offset(range.start),
|
||||
highlight_end: inlay_snapshot.to_inlay_offset(range.end),
|
||||
})
|
||||
.map(DocumentRange::Inlay)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
highlight_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
buffer_snapshot.anchor_before(range.start)
|
||||
..buffer_snapshot.anchor_after(range.end)
|
||||
})
|
||||
.map(DocumentRange::Text)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
highlights.insert(
|
||||
Some(TypeId::of::<()>()),
|
||||
Arc::new((HighlightStyle::default(), highlight_ranges)),
|
||||
);
|
||||
|
||||
for _ in 0..5 {
|
||||
let mut end = rng.gen_range(0..=inlay_snapshot.len().0);
|
||||
end = expected_text.clip_offset(end, Bias::Right);
|
||||
|
||||
@@ -224,8 +224,8 @@ impl TabSnapshot {
|
||||
range: Range<TabPoint>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> TabChunks<'a> {
|
||||
let (input_start, expanded_char_column, to_next_stop) =
|
||||
self.to_fold_point(range.start, Bias::Left);
|
||||
@@ -246,8 +246,8 @@ impl TabSnapshot {
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
),
|
||||
input_column,
|
||||
column: expanded_char_column,
|
||||
|
||||
@@ -576,8 +576,8 @@ impl WrapSnapshot {
|
||||
rows: Range<u32>,
|
||||
language_aware: bool,
|
||||
text_highlights: Option<&'a TextHighlights>,
|
||||
hint_highlights: Option<HighlightStyle>,
|
||||
suggestion_highlights: Option<HighlightStyle>,
|
||||
hint_highlight_style: Option<HighlightStyle>,
|
||||
suggestion_highlight_style: Option<HighlightStyle>,
|
||||
) -> WrapChunks<'a> {
|
||||
let output_start = WrapPoint::new(rows.start, 0);
|
||||
let output_end = WrapPoint::new(rows.end, 0);
|
||||
@@ -595,8 +595,8 @@ impl WrapSnapshot {
|
||||
input_start..input_end,
|
||||
language_aware,
|
||||
text_highlights,
|
||||
hint_highlights,
|
||||
suggestion_highlights,
|
||||
hint_highlight_style,
|
||||
suggestion_highlight_style,
|
||||
),
|
||||
input_chunk: Default::default(),
|
||||
output_position: output_start,
|
||||
|
||||
+559
-252
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ pub struct EditorSettings {
|
||||
pub show_completions_on_input: bool,
|
||||
pub use_on_type_format: bool,
|
||||
pub scrollbar: Scrollbar,
|
||||
pub relative_line_numbers: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
@@ -34,6 +35,7 @@ pub struct EditorSettingsContent {
|
||||
pub show_completions_on_input: 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)]
|
||||
|
||||
@@ -43,7 +43,7 @@ fn test_edit_events(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let mut buffer = language::Buffer::new(0, "123456", cx);
|
||||
let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456");
|
||||
buffer.set_group_interval(Duration::from_secs(1));
|
||||
buffer
|
||||
});
|
||||
@@ -175,7 +175,7 @@ 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, "123456", cx));
|
||||
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
|
||||
@@ -248,7 +248,7 @@ fn test_ime_composition(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer = cx.add_model(|cx| {
|
||||
let mut buffer = language::Buffer::new(0, "abcde", 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
|
||||
@@ -1435,6 +1435,74 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[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, |_| {});
|
||||
@@ -2214,10 +2282,12 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
|
||||
None,
|
||||
));
|
||||
|
||||
let toml_buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx));
|
||||
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, "const c: usize = 3;\n", cx).with_language(rust_language, 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);
|
||||
@@ -3687,7 +3757,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
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))
|
||||
@@ -3850,7 +3921,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let text = "fn a() {}";
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
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
|
||||
@@ -4413,7 +4485,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) {
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
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))
|
||||
@@ -4561,7 +4634,8 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
|
||||
"#
|
||||
.unindent();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
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
|
||||
@@ -5238,6 +5312,7 @@ async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
@@ -5766,7 +5841,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
|
||||
fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), 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(
|
||||
@@ -5850,7 +5925,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
|
||||
primary: None,
|
||||
}
|
||||
});
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, initial_text, cx));
|
||||
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);
|
||||
@@ -5908,7 +5983,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
|
||||
fn test_refresh_selections(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), 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);
|
||||
@@ -5995,7 +6070,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) {
|
||||
fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), 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);
|
||||
@@ -6092,7 +6167,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) {
|
||||
"{{} }\n", //
|
||||
);
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx));
|
||||
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))
|
||||
@@ -6384,7 +6460,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
.update(|cx| {
|
||||
Editor::from_state_proto(
|
||||
pane.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
id: 0,
|
||||
@@ -6479,7 +6555,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
|
||||
.update(|cx| {
|
||||
Editor::from_state_proto(
|
||||
pane.clone(),
|
||||
project.clone(),
|
||||
workspace.clone(),
|
||||
ViewId {
|
||||
creator: Default::default(),
|
||||
id: 0,
|
||||
@@ -7092,8 +7168,8 @@ async fn test_copilot_multibuffer(
|
||||
let (copilot, copilot_lsp) = Copilot::fake(cx);
|
||||
cx.update(|cx| cx.set_global(copilot));
|
||||
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx));
|
||||
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(
|
||||
@@ -7529,6 +7605,7 @@ async fn test_completions_with_additional_edits(cx: &mut gpui::TestAppContext) {
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string()]),
|
||||
resolve_provider: Some(true),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
|
||||
+261
-90
@@ -13,6 +13,7 @@ use crate::{
|
||||
},
|
||||
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,
|
||||
};
|
||||
@@ -62,6 +63,7 @@ struct SelectionLayout {
|
||||
head: DisplayPoint,
|
||||
cursor_shape: CursorShape,
|
||||
is_newest: bool,
|
||||
is_local: bool,
|
||||
range: Range<DisplayPoint>,
|
||||
active_rows: Range<u32>,
|
||||
}
|
||||
@@ -73,6 +75,7 @@ impl SelectionLayout {
|
||||
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));
|
||||
@@ -109,6 +112,7 @@ impl SelectionLayout {
|
||||
head,
|
||||
cursor_shape,
|
||||
is_newest,
|
||||
is_local,
|
||||
range,
|
||||
active_rows,
|
||||
}
|
||||
@@ -284,13 +288,13 @@ impl EditorElement {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (position, target_position) = position_map.point_for_position(text_bounds, position);
|
||||
|
||||
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: target_position.column(),
|
||||
goal_column: point_for_position.exact_unclipped.column(),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -326,9 +330,13 @@ impl EditorElement {
|
||||
if !text_bounds.contains_point(position) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let (point, _) = position_map.point_for_position(text_bounds, position);
|
||||
mouse_context_menu::deploy_context_menu(editor, position, point, cx);
|
||||
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
|
||||
}
|
||||
|
||||
@@ -350,17 +358,15 @@ impl EditorElement {
|
||||
}
|
||||
|
||||
if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
|
||||
if point == target_point {
|
||||
if shift {
|
||||
go_to_fetched_type_definition(editor, point, alt, cx);
|
||||
} else {
|
||||
go_to_fetched_definition(editor, point, alt, cx);
|
||||
}
|
||||
|
||||
return true;
|
||||
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
|
||||
@@ -380,17 +386,20 @@ impl EditorElement {
|
||||
// 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) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
if point == target_point {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
position_map
|
||||
.point_for_position(text_bounds, position)
|
||||
.as_valid()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
update_go_to_definition_link(editor, point, cmd, shift, cx);
|
||||
update_go_to_definition_link(
|
||||
editor,
|
||||
point.map(GoToDefinitionTrigger::Text),
|
||||
cmd,
|
||||
shift,
|
||||
cx,
|
||||
);
|
||||
|
||||
if editor.has_pending_selection() {
|
||||
let mut scroll_delta = Vector2F::zero();
|
||||
@@ -419,13 +428,12 @@ impl EditorElement {
|
||||
))
|
||||
}
|
||||
|
||||
let (position, target_position) =
|
||||
position_map.point_for_position(text_bounds, position);
|
||||
let point_for_position = position_map.point_for_position(text_bounds, position);
|
||||
|
||||
editor.select(
|
||||
SelectPhase::Update {
|
||||
position,
|
||||
goal_column: target_position.column(),
|
||||
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),
|
||||
},
|
||||
@@ -452,10 +460,34 @@ impl EditorElement {
|
||||
) -> 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 = position_to_display_point(position, text_bounds, position_map);
|
||||
|
||||
update_go_to_definition_link(editor, point, cmd, shift, cx);
|
||||
hover_at(editor, point, cx);
|
||||
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
|
||||
}
|
||||
@@ -605,7 +637,7 @@ impl EditorElement {
|
||||
visible_bounds: RectF,
|
||||
layout: &mut LayoutState,
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut PaintContext<Editor>,
|
||||
) {
|
||||
let line_height = layout.position_map.line_height;
|
||||
|
||||
@@ -760,10 +792,9 @@ impl EditorElement {
|
||||
visible_bounds: RectF,
|
||||
layout: &mut LayoutState,
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut PaintContext<Editor>,
|
||||
) {
|
||||
let style = &self.style;
|
||||
let local_replica_id = editor.replica_id(cx);
|
||||
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;
|
||||
@@ -852,15 +883,13 @@ impl EditorElement {
|
||||
|
||||
for (replica_id, selections) in &layout.selections {
|
||||
let replica_id = *replica_id;
|
||||
let selection_style = style.replica_selection_style(replica_id);
|
||||
let selection_style = if let Some(replica_id) = replica_id {
|
||||
style.replica_selection_style(replica_id)
|
||||
} else {
|
||||
&style.absent_selection
|
||||
};
|
||||
|
||||
for selection in selections {
|
||||
if !selection.range.is_empty()
|
||||
&& (replica_id == local_replica_id
|
||||
|| Some(replica_id) == editor.leader_replica_id)
|
||||
{
|
||||
invisible_display_ranges.push(selection.range.clone());
|
||||
}
|
||||
self.paint_highlighted_range(
|
||||
scene,
|
||||
selection.range.clone(),
|
||||
@@ -874,7 +903,10 @@ impl EditorElement {
|
||||
bounds,
|
||||
);
|
||||
|
||||
if editor.show_local_cursors(cx) || replica_id != local_replica_id {
|
||||
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
|
||||
@@ -906,7 +938,7 @@ impl EditorElement {
|
||||
&text,
|
||||
cursor_row_layout.font_size(),
|
||||
&[(
|
||||
text.len(),
|
||||
text.chars().count(),
|
||||
RunStyle {
|
||||
font_id,
|
||||
color: style.background,
|
||||
@@ -1337,7 +1369,7 @@ impl EditorElement {
|
||||
visible_bounds: RectF,
|
||||
layout: &mut LayoutState,
|
||||
editor: &mut Editor,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
cx: &mut PaintContext<Editor>,
|
||||
) {
|
||||
let scroll_position = layout.position_map.snapshot.scroll_position();
|
||||
let scroll_left = scroll_position.x() * layout.position_map.em_width;
|
||||
@@ -1405,10 +1437,61 @@ impl EditorElement {
|
||||
.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>,
|
||||
@@ -1421,6 +1504,15 @@ impl EditorElement {
|
||||
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)
|
||||
@@ -1435,7 +1527,11 @@ impl EditorElement {
|
||||
if let Some(buffer_row) = row {
|
||||
if include_line_numbers {
|
||||
line_number.clear();
|
||||
write!(&mut line_number, "{}", buffer_row + 1).unwrap();
|
||||
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,
|
||||
@@ -2079,14 +2175,11 @@ impl Element<Editor> for EditorElement {
|
||||
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
|
||||
.min(constraint.max_along(Axis::Vertical))
|
||||
.max(constraint.min_along(Axis::Vertical)),
|
||||
)
|
||||
size.set_y(line_height.max(constraint.min_along(Axis::Vertical)))
|
||||
} else if size.y().is_infinite() {
|
||||
size.set_y(scroll_height);
|
||||
}
|
||||
@@ -2124,7 +2217,7 @@ impl Element<Editor> for EditorElement {
|
||||
.anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right))
|
||||
};
|
||||
|
||||
let mut selections: Vec<(ReplicaId, Vec<SelectionLayout>)> = Vec::new();
|
||||
let mut selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)> = Vec::new();
|
||||
let mut active_rows = BTreeMap::new();
|
||||
let mut fold_ranges = Vec::new();
|
||||
let is_singleton = editor.is_singleton(cx);
|
||||
@@ -2155,8 +2248,14 @@ impl Element<Editor> for EditorElement {
|
||||
.buffer_snapshot
|
||||
.remote_selections_in_range(&(start_anchor..end_anchor))
|
||||
{
|
||||
let replica_id = if let Some(mapping) = &editor.replica_id_mapping {
|
||||
mapping.get(&replica_id).copied()
|
||||
} else {
|
||||
Some(replica_id)
|
||||
};
|
||||
|
||||
// The local selections match the leader's selections.
|
||||
if Some(replica_id) == editor.leader_replica_id {
|
||||
if replica_id.is_some() && replica_id == editor.leader_replica_id {
|
||||
continue;
|
||||
}
|
||||
remote_selections
|
||||
@@ -2168,6 +2267,7 @@ impl Element<Editor> for EditorElement {
|
||||
cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
false,
|
||||
false,
|
||||
));
|
||||
}
|
||||
selections.extend(remote_selections);
|
||||
@@ -2191,6 +2291,7 @@ impl Element<Editor> for EditorElement {
|
||||
editor.cursor_shape,
|
||||
&snapshot.display_snapshot,
|
||||
is_newest,
|
||||
true,
|
||||
);
|
||||
if is_newest {
|
||||
newest_selection_head = Some(layout.head);
|
||||
@@ -2206,11 +2307,18 @@ impl Element<Editor> for EditorElement {
|
||||
}
|
||||
|
||||
// Render the local selections in the leader's color when following.
|
||||
let local_replica_id = editor
|
||||
.leader_replica_id
|
||||
.unwrap_or_else(|| editor.replica_id(cx));
|
||||
let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id {
|
||||
leader_replica_id
|
||||
} else {
|
||||
let replica_id = editor.replica_id(cx);
|
||||
if let Some(mapping) = &editor.replica_id_mapping {
|
||||
mapping.get(&replica_id).copied().unwrap_or(replica_id)
|
||||
} else {
|
||||
replica_id
|
||||
}
|
||||
};
|
||||
|
||||
selections.push((local_replica_id, layouts));
|
||||
selections.push((Some(local_replica_id), layouts));
|
||||
}
|
||||
|
||||
let scrollbar_settings = &settings::get::<EditorSettings>(cx).scrollbar;
|
||||
@@ -2244,9 +2352,23 @@ impl Element<Editor> for EditorElement {
|
||||
})
|
||||
.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,
|
||||
@@ -2591,7 +2713,7 @@ pub struct LayoutState {
|
||||
blocks: Vec<BlockLayout>,
|
||||
highlighted_ranges: Vec<(Range<DisplayPoint>, Color)>,
|
||||
fold_ranges: Vec<(BufferRow, Range<DisplayPoint>, Color)>,
|
||||
selections: Vec<(ReplicaId, Vec<SelectionLayout>)>,
|
||||
selections: Vec<(Option<ReplicaId>, Vec<SelectionLayout>)>,
|
||||
scrollbar_row_range: Range<f32>,
|
||||
show_scrollbars: bool,
|
||||
is_singleton: bool,
|
||||
@@ -2614,22 +2736,42 @@ struct PositionMap {
|
||||
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 {
|
||||
/// Returns two display points:
|
||||
/// 1. The nearest *valid* position in the editor
|
||||
/// 2. An unclipped, potentially *invalid* position that maps directly to
|
||||
/// the given pixel position.
|
||||
fn point_for_position(
|
||||
&self,
|
||||
text_bounds: RectF,
|
||||
position: Vector2F,
|
||||
) -> (DisplayPoint, DisplayPoint) {
|
||||
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) = if let Some(line) = self
|
||||
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)
|
||||
@@ -2643,11 +2785,18 @@ impl PositionMap {
|
||||
(0, x)
|
||||
};
|
||||
|
||||
let mut target_point = DisplayPoint::new(row, column);
|
||||
let point = self.snapshot.clip_point(target_point, Bias::Left);
|
||||
*target_point.column_mut() += (x_overshoot / self.em_advance) as u32;
|
||||
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);
|
||||
|
||||
(point, target_point)
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2901,23 +3050,6 @@ impl HighlightedRange {
|
||||
}
|
||||
}
|
||||
|
||||
fn position_to_display_point(
|
||||
position: Vector2F,
|
||||
text_bounds: RectF,
|
||||
position_map: &PositionMap,
|
||||
) -> Option<DisplayPoint> {
|
||||
if text_bounds.contains_point(position) {
|
||||
let (point, target_point) = position_map.point_for_position(text_bounds, position);
|
||||
if point == target_point {
|
||||
Some(point)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn range_to_bounds(
|
||||
range: &Range<DisplayPoint>,
|
||||
content_origin: Vector2F,
|
||||
@@ -2995,7 +3127,6 @@ mod tests {
|
||||
#[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);
|
||||
@@ -3007,10 +3138,50 @@ mod tests {
|
||||
let layouts = editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.snapshot(cx);
|
||||
element
|
||||
.layout_line_numbers(0..6, &Default::default(), false, &snapshot, cx)
|
||||
.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]
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use crate::{
|
||||
display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings,
|
||||
EditorSnapshot, EditorStyle, RangeToAnchorExt,
|
||||
display_map::{InlayOffset, ToDisplayPoint},
|
||||
link_go_to_definition::{DocumentRange, InlayRange},
|
||||
Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle,
|
||||
ExcerptId, RangeToAnchorExt,
|
||||
};
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
@@ -11,7 +13,7 @@ use gpui::{
|
||||
AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext,
|
||||
};
|
||||
use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry};
|
||||
use project::{HoverBlock, HoverBlockKind, Project};
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use util::TryFutureExt;
|
||||
|
||||
@@ -46,6 +48,106 @@ pub fn hover_at(editor: &mut Editor, point: Option<DisplayPoint>, cx: &mut ViewC
|
||||
}
|
||||
}
|
||||
|
||||
pub struct InlayHover {
|
||||
pub excerpt: ExcerptId,
|
||||
pub triggered_from: InlayOffset,
|
||||
pub range: InlayRange,
|
||||
pub tooltip: HoverBlock,
|
||||
}
|
||||
|
||||
pub fn find_hovered_hint_part(
|
||||
label_parts: Vec<InlayHintLabelPart>,
|
||||
hint_range: Range<InlayOffset>,
|
||||
hovered_offset: InlayOffset,
|
||||
) -> Option<(InlayHintLabelPart, Range<InlayOffset>)> {
|
||||
if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end {
|
||||
let mut hovered_character = (hovered_offset - hint_range.start).0;
|
||||
let mut part_start = hint_range.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 DocumentRange::Inlay(range) = symbol_range {
|
||||
if (range.highlight_start..range.highlight_end)
|
||||
.contains(&inlay_hover.triggered_from)
|
||||
{
|
||||
// Hover triggered from same location as last time. Don't show again.
|
||||
return;
|
||||
}
|
||||
}
|
||||
hide_hover(editor, cx);
|
||||
}
|
||||
|
||||
let snapshot = editor.snapshot(cx);
|
||||
// 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 inlay_hover.triggered_from
|
||||
== snapshot
|
||||
.display_snapshot
|
||||
.anchor_to_inlay_offset(triggered_from)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 hover_popover = InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: DocumentRange::Inlay(inlay_hover.range),
|
||||
blocks: vec![inlay_hover.tooltip],
|
||||
language: None,
|
||||
rendered_content: None,
|
||||
};
|
||||
|
||||
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.
|
||||
@@ -110,8 +212,13 @@ fn show_hover(
|
||||
if !ignore_timeout {
|
||||
if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover {
|
||||
if symbol_range
|
||||
.to_offset(&snapshot.buffer_snapshot)
|
||||
.contains(&multibuffer_offset)
|
||||
.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;
|
||||
@@ -219,7 +326,7 @@ fn show_hover(
|
||||
|
||||
Some(InfoPopover {
|
||||
project: project.clone(),
|
||||
symbol_range: range,
|
||||
symbol_range: DocumentRange::Text(range),
|
||||
blocks: hover_result.contents,
|
||||
language: hover_result.language,
|
||||
rendered_content: None,
|
||||
@@ -227,10 +334,13 @@ fn show_hover(
|
||||
});
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(hover_popover) = hover_popover.as_ref() {
|
||||
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![hover_popover.symbol_range.clone()],
|
||||
vec![symbol_range],
|
||||
|theme| theme.editor.hover_popover.highlight,
|
||||
cx,
|
||||
);
|
||||
@@ -497,7 +607,10 @@ impl HoverState {
|
||||
.or_else(|| {
|
||||
self.info_popover
|
||||
.as_ref()
|
||||
.map(|info_popover| &info_popover.symbol_range.start)
|
||||
.map(|info_popover| match &info_popover.symbol_range {
|
||||
DocumentRange::Text(range) => &range.start,
|
||||
DocumentRange::Inlay(range) => &range.inlay_position,
|
||||
})
|
||||
})?;
|
||||
let point = anchor.to_display_point(&snapshot.display_snapshot);
|
||||
|
||||
@@ -522,7 +635,7 @@ impl HoverState {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InfoPopover {
|
||||
pub project: ModelHandle<Project>,
|
||||
pub symbol_range: Range<Anchor>,
|
||||
symbol_range: DocumentRange,
|
||||
pub blocks: Vec<HoverBlock>,
|
||||
language: Option<Arc<Language>>,
|
||||
rendered_content: Option<RenderedInfo>,
|
||||
@@ -692,10 +805,17 @@ impl DiagnosticPopover {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
||||
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,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::fonts::Weight;
|
||||
use indoc::indoc;
|
||||
use language::{Diagnostic, DiagnosticSet};
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use smol::stream::StreamExt;
|
||||
@@ -1131,4 +1251,327 @@ mod tests {
|
||||
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 snapshot = editor.snapshot(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));
|
||||
let entire_inlay_start = snapshot.display_point_to_inlay_offset(
|
||||
inlay_range.start.to_display_point(&snapshot),
|
||||
Bias::Left,
|
||||
);
|
||||
|
||||
let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len());
|
||||
assert_eq!(
|
||||
popover.symbol_range,
|
||||
DocumentRange::Inlay(InlayRange {
|
||||
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
|
||||
highlight_start: expected_new_type_label_start,
|
||||
highlight_end: InlayOffset(
|
||||
expected_new_type_label_start.0 + new_type_label.len()
|
||||
),
|
||||
}),
|
||||
"Popover range should match the new type label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover
|
||||
.rendered_content
|
||||
.as_ref()
|
||||
.expect("should have label text for new type hint")
|
||||
.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 snapshot = editor.snapshot(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));
|
||||
let entire_inlay_start = snapshot.display_point_to_inlay_offset(
|
||||
inlay_range.start.to_display_point(&snapshot),
|
||||
Bias::Left,
|
||||
);
|
||||
let expected_struct_label_start =
|
||||
InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len());
|
||||
assert_eq!(
|
||||
popover.symbol_range,
|
||||
DocumentRange::Inlay(InlayRange {
|
||||
inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right),
|
||||
highlight_start: expected_struct_label_start,
|
||||
highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()),
|
||||
}),
|
||||
"Popover range should match the struct label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover
|
||||
.rendered_content
|
||||
.as_ref()
|
||||
.expect("should have label text for struct hint")
|
||||
.text,
|
||||
format!("A tooltip for {struct_label}"),
|
||||
"Rendered markdown element should remove backticks from text"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -49,13 +49,18 @@ impl FollowableItem for Editor {
|
||||
|
||||
fn from_state_proto(
|
||||
pane: ViewHandle<workspace::Pane>,
|
||||
project: ModelHandle<Project>,
|
||||
workspace: ViewHandle<Workspace>,
|
||||
remote_id: ViewId,
|
||||
state: &mut Option<proto::view::Variant>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<Task<Result<ViewHandle<Self>>>> {
|
||||
let Some(proto::view::Variant::Editor(_)) = state else { return None };
|
||||
let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() };
|
||||
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();
|
||||
@@ -340,10 +345,16 @@ async fn update_editor_from_message(
|
||||
|
||||
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 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 Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let adjacent_excerpts = iter::from_fn(|| {
|
||||
let insertion = insertions.peek()?;
|
||||
@@ -561,7 +572,7 @@ impl Item for Editor {
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_content<T: View>(
|
||||
fn tab_content<T: 'static>(
|
||||
&self,
|
||||
detail: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
@@ -614,7 +625,7 @@ impl Item for Editor {
|
||||
|
||||
fn workspace_deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
hide_link_definition(self, cx);
|
||||
self.link_go_to_definition_state.last_mouse_location = None;
|
||||
self.link_go_to_definition_state.last_trigger_point = None;
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
@@ -753,7 +764,7 @@ impl Item for Editor {
|
||||
Some(Box::new(handle.clone()))
|
||||
}
|
||||
|
||||
fn pixel_position_of_cursor(&self) -> Option<Vector2F> {
|
||||
fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Vector2F> {
|
||||
self.pixel_position_of_newest_cursor
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -61,10 +61,10 @@ pub fn up_by_rows(
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_start: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
let mut goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => map.column_to_chars(start.row(), start.column()),
|
||||
};
|
||||
|
||||
let prev_row = start.row().saturating_sub(row_count);
|
||||
@@ -95,10 +95,10 @@ pub fn down_by_rows(
|
||||
goal: SelectionGoal,
|
||||
preserve_column_at_end: bool,
|
||||
) -> (DisplayPoint, SelectionGoal) {
|
||||
let mut goal_column = if let SelectionGoal::Column(column) = goal {
|
||||
column
|
||||
} else {
|
||||
map.column_to_chars(start.row(), start.column())
|
||||
let mut goal_column = match goal {
|
||||
SelectionGoal::Column(column) => column,
|
||||
SelectionGoal::ColumnRange { end, .. } => end,
|
||||
_ => map.column_to_chars(start.row(), start.column()),
|
||||
};
|
||||
|
||||
let new_row = start.row() + row_count;
|
||||
@@ -756,7 +756,8 @@ mod tests {
|
||||
.select_font(family_id, &Default::default())
|
||||
.unwrap();
|
||||
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx));
|
||||
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(
|
||||
|
||||
@@ -6,7 +6,7 @@ use clock::ReplicaId;
|
||||
use collections::{BTreeMap, Bound, HashMap, HashSet};
|
||||
use futures::{channel::mpsc, SinkExt};
|
||||
use git::diff::DiffHunk;
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle};
|
||||
pub use language::Completion;
|
||||
use language::{
|
||||
char_kind,
|
||||
@@ -67,7 +67,9 @@ pub enum Event {
|
||||
ExcerptsEdited {
|
||||
ids: Vec<ExcerptId>,
|
||||
},
|
||||
Edited,
|
||||
Edited {
|
||||
sigleton_buffer_edited: bool,
|
||||
},
|
||||
Reloaded,
|
||||
DiffBaseChanged,
|
||||
LanguageChanged,
|
||||
@@ -615,6 +617,42 @@ impl MultiBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn merge_transactions(
|
||||
&mut self,
|
||||
transaction: TransactionId,
|
||||
destination: TransactionId,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, _| {
|
||||
buffer.merge_transactions(transaction, destination)
|
||||
});
|
||||
} else {
|
||||
if let Some(transaction) = self.history.forget(transaction) {
|
||||
if let Some(destination) = self.history.transaction_mut(destination) {
|
||||
for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions {
|
||||
if let Some(destination_buffer_transaction_id) =
|
||||
destination.buffer_transactions.get(&buffer_id)
|
||||
{
|
||||
if let Some(state) = self.buffers.borrow().get(&buffer_id) {
|
||||
state.buffer.update(cx, |buffer, _| {
|
||||
buffer.merge_transactions(
|
||||
buffer_transaction_id,
|
||||
*destination_buffer_transaction_id,
|
||||
)
|
||||
});
|
||||
}
|
||||
} else {
|
||||
destination
|
||||
.buffer_transactions
|
||||
.insert(buffer_id, buffer_transaction_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize_last_transaction(&mut self, cx: &mut ModelContext<Self>) {
|
||||
self.history.finalize_last_transaction();
|
||||
for BufferState { buffer, .. } in self.buffers.borrow().values() {
|
||||
@@ -786,61 +824,75 @@ impl MultiBuffer {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut ModelContext<Self>) {
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx));
|
||||
} else if let Some(transaction) = self.history.remove_from_undo(transaction_id) {
|
||||
for (buffer_id, transaction_id) in &transaction.buffer_transactions {
|
||||
if let Some(BufferState { buffer, .. }) = self.buffers.borrow().get(buffer_id) {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.undo_transaction(*transaction_id, cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stream_excerpts_with_context_lines(
|
||||
&mut self,
|
||||
excerpts: Vec<(ModelHandle<Buffer>, Vec<Range<text::Anchor>>)>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
ranges: Vec<Range<text::Anchor>>,
|
||||
context_line_count: u32,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> (Task<()>, mpsc::Receiver<Range<Anchor>>) {
|
||||
) -> mpsc::Receiver<Range<Anchor>> {
|
||||
let (mut tx, rx) = mpsc::channel(256);
|
||||
let task = cx.spawn(|this, mut cx| async move {
|
||||
for (buffer, ranges) in excerpts {
|
||||
let (buffer_id, buffer_snapshot) =
|
||||
buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let (buffer_id, buffer_snapshot) =
|
||||
buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot()));
|
||||
|
||||
let mut excerpt_ranges = Vec::new();
|
||||
let mut range_counts = Vec::new();
|
||||
cx.background()
|
||||
.scoped(|scope| {
|
||||
scope.spawn(async {
|
||||
let (ranges, counts) =
|
||||
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
|
||||
excerpt_ranges = ranges;
|
||||
range_counts = counts;
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut ranges = ranges.into_iter();
|
||||
let mut range_counts = range_counts.into_iter();
|
||||
for excerpt_ranges in excerpt_ranges.chunks(100) {
|
||||
let excerpt_ids = this.update(&mut cx, |this, cx| {
|
||||
this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx)
|
||||
let mut excerpt_ranges = Vec::new();
|
||||
let mut range_counts = Vec::new();
|
||||
cx.background()
|
||||
.scoped(|scope| {
|
||||
scope.spawn(async {
|
||||
let (ranges, counts) =
|
||||
build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count);
|
||||
excerpt_ranges = ranges;
|
||||
range_counts = counts;
|
||||
});
|
||||
})
|
||||
.await;
|
||||
|
||||
for (excerpt_id, range_count) in
|
||||
excerpt_ids.into_iter().zip(range_counts.by_ref())
|
||||
{
|
||||
for range in ranges.by_ref().take(range_count) {
|
||||
let start = Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id: excerpt_id.clone(),
|
||||
text_anchor: range.start,
|
||||
};
|
||||
let end = Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id: excerpt_id.clone(),
|
||||
text_anchor: range.end,
|
||||
};
|
||||
if tx.send(start..end).await.is_err() {
|
||||
break;
|
||||
}
|
||||
let mut ranges = ranges.into_iter();
|
||||
let mut range_counts = range_counts.into_iter();
|
||||
for excerpt_ranges in excerpt_ranges.chunks(100) {
|
||||
let excerpt_ids = this.update(&mut cx, |this, cx| {
|
||||
this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx)
|
||||
});
|
||||
|
||||
for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref())
|
||||
{
|
||||
for range in ranges.by_ref().take(range_count) {
|
||||
let start = Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id: excerpt_id.clone(),
|
||||
text_anchor: range.start,
|
||||
};
|
||||
let end = Anchor {
|
||||
buffer_id: Some(buffer_id),
|
||||
excerpt_id: excerpt_id.clone(),
|
||||
text_anchor: range.end,
|
||||
};
|
||||
if tx.send(start..end).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
(task, rx)
|
||||
})
|
||||
.detach();
|
||||
|
||||
rx
|
||||
}
|
||||
|
||||
pub fn push_excerpts<O>(
|
||||
@@ -1022,7 +1074,9 @@ impl MultiBuffer {
|
||||
old: edit_start..edit_start,
|
||||
new: edit_start..edit_end,
|
||||
}]);
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(Event::Edited {
|
||||
sigleton_buffer_edited: false,
|
||||
});
|
||||
cx.emit(Event::ExcerptsAdded {
|
||||
buffer,
|
||||
predecessor: prev_excerpt_id,
|
||||
@@ -1046,7 +1100,9 @@ impl MultiBuffer {
|
||||
old: 0..prev_len,
|
||||
new: 0..0,
|
||||
}]);
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(Event::Edited {
|
||||
sigleton_buffer_edited: false,
|
||||
});
|
||||
cx.emit(Event::ExcerptsRemoved { ids });
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1254,7 +1310,9 @@ impl MultiBuffer {
|
||||
}
|
||||
|
||||
self.subscriptions.publish_mut(edits);
|
||||
cx.emit(Event::Edited);
|
||||
cx.emit(Event::Edited {
|
||||
sigleton_buffer_edited: false,
|
||||
});
|
||||
cx.emit(Event::ExcerptsRemoved { ids });
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1315,7 +1373,9 @@ impl MultiBuffer {
|
||||
cx: &mut ModelContext<Self>,
|
||||
) {
|
||||
cx.emit(match event {
|
||||
language::Event::Edited => Event::Edited,
|
||||
language::Event::Edited => Event::Edited {
|
||||
sigleton_buffer_edited: true,
|
||||
},
|
||||
language::Event::DirtyChanged => Event::DirtyChanged,
|
||||
language::Event::Saved => Event::Saved,
|
||||
language::Event::FileHandleChanged => Event::FileHandleChanged,
|
||||
@@ -1346,10 +1406,7 @@ impl MultiBuffer {
|
||||
.map(|state| state.buffer.clone())
|
||||
}
|
||||
|
||||
pub fn is_completion_trigger<T>(&self, position: T, text: &str, cx: &AppContext) -> bool
|
||||
where
|
||||
T: ToOffset,
|
||||
{
|
||||
pub fn is_completion_trigger(&self, position: Anchor, text: &str, cx: &AppContext) -> bool {
|
||||
let mut chars = text.chars();
|
||||
let char = if let Some(char) = chars.next() {
|
||||
char
|
||||
@@ -1563,7 +1620,7 @@ impl MultiBuffer {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl MultiBuffer {
|
||||
pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> ModelHandle<Self> {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text));
|
||||
cx.add_model(|cx| Self::singleton(buffer, cx))
|
||||
}
|
||||
|
||||
@@ -1573,7 +1630,7 @@ impl MultiBuffer {
|
||||
) -> ModelHandle<Self> {
|
||||
let multi = cx.add_model(|_| Self::new(0));
|
||||
for (text, ranges) in excerpts {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text));
|
||||
let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange {
|
||||
context: range,
|
||||
primary: None,
|
||||
@@ -1665,7 +1722,7 @@ impl MultiBuffer {
|
||||
if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) {
|
||||
let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() {
|
||||
let text = RandomCharIter::new(&mut *rng).take(10).collect::<String>();
|
||||
buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx)));
|
||||
buffers.push(cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)));
|
||||
let buffer = buffers.last().unwrap().read(cx);
|
||||
log::info!(
|
||||
"Creating new buffer {} with text: {:?}",
|
||||
@@ -2309,6 +2366,16 @@ impl MultiBufferSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prev_non_blank_row(&self, mut row: u32) -> Option<u32> {
|
||||
while row > 0 {
|
||||
row -= 1;
|
||||
if !self.is_line_blank(row) {
|
||||
return Some(row);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn line_len(&self, row: u32) -> u32 {
|
||||
if let Some((_, range)) = self.buffer_line_for_row(row) {
|
||||
range.end.column - range.start.column
|
||||
@@ -2759,7 +2826,9 @@ impl MultiBufferSnapshot {
|
||||
// Get the ranges of the innermost pair of brackets.
|
||||
let mut result: Option<(Range<usize>, Range<usize>)> = None;
|
||||
|
||||
let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; };
|
||||
let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
for (open, close) in enclosing_bracket_ranges {
|
||||
let len = close.end - open.start;
|
||||
@@ -3338,6 +3407,35 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
fn forget(&mut self, transaction_id: TransactionId) -> Option<Transaction> {
|
||||
if let Some(ix) = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)
|
||||
{
|
||||
Some(self.undo_stack.remove(ix))
|
||||
} else if let Some(ix) = self
|
||||
.redo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)
|
||||
{
|
||||
Some(self.redo_stack.remove(ix))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> {
|
||||
self.undo_stack
|
||||
.iter_mut()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
.or_else(|| {
|
||||
self.redo_stack
|
||||
.iter_mut()
|
||||
.find(|transaction| transaction.id == transaction_id)
|
||||
})
|
||||
}
|
||||
|
||||
fn pop_undo(&mut self) -> Option<&mut Transaction> {
|
||||
assert_eq!(self.transaction_depth, 0);
|
||||
if let Some(transaction) = self.undo_stack.pop() {
|
||||
@@ -3358,6 +3456,16 @@ impl History {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> {
|
||||
let ix = self
|
||||
.undo_stack
|
||||
.iter()
|
||||
.rposition(|transaction| transaction.id == transaction_id)?;
|
||||
let transaction = self.undo_stack.remove(ix);
|
||||
self.redo_stack.push(transaction);
|
||||
self.redo_stack.last()
|
||||
}
|
||||
|
||||
fn group(&mut self) -> Option<TransactionId> {
|
||||
let mut count = 0;
|
||||
let mut transactions = self.undo_stack.iter();
|
||||
@@ -4013,7 +4121,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_singleton(cx: &mut AppContext) {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a')));
|
||||
let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
@@ -4040,7 +4149,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_remote(cx: &mut AppContext) {
|
||||
let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx));
|
||||
let host_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a"));
|
||||
let guest_buffer = cx.add_model(|cx| {
|
||||
let state = host_buffer.read(cx).to_proto();
|
||||
let ops = cx
|
||||
@@ -4071,15 +4180,17 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx));
|
||||
let buffer_1 =
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a')));
|
||||
let buffer_2 =
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'g')));
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
|
||||
let events = Rc::new(RefCell::new(Vec::<Event>::new()));
|
||||
multibuffer.update(cx, |_, cx| {
|
||||
let events = events.clone();
|
||||
cx.subscribe(&multibuffer, move |_, _, event, _| {
|
||||
if let Event::Edited = event {
|
||||
if let Event::Edited { .. } = event {
|
||||
events.borrow_mut().push(event.clone())
|
||||
}
|
||||
})
|
||||
@@ -4134,7 +4245,17 @@ mod tests {
|
||||
// Adding excerpts emits an edited event.
|
||||
assert_eq!(
|
||||
events.borrow().as_slice(),
|
||||
&[Event::Edited, Event::Edited, Event::Edited]
|
||||
&[
|
||||
Event::Edited {
|
||||
sigleton_buffer_edited: false
|
||||
},
|
||||
Event::Edited {
|
||||
sigleton_buffer_edited: false
|
||||
},
|
||||
Event::Edited {
|
||||
sigleton_buffer_edited: false
|
||||
}
|
||||
]
|
||||
);
|
||||
|
||||
let snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
@@ -4295,8 +4416,10 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_excerpt_events(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx));
|
||||
let buffer_1 =
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'a')));
|
||||
let buffer_2 =
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'm')));
|
||||
|
||||
let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
@@ -4313,7 +4436,7 @@ mod tests {
|
||||
excerpts,
|
||||
} => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx),
|
||||
Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx),
|
||||
Event::Edited => {
|
||||
Event::Edited { .. } => {
|
||||
*follower_edit_event_count.borrow_mut() += 1;
|
||||
}
|
||||
_ => {}
|
||||
@@ -4401,7 +4524,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_push_excerpts_with_context_lines(cx: &mut AppContext) {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a')));
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.push_excerpts_with_context_lines(
|
||||
@@ -4437,9 +4561,10 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx));
|
||||
let buffer =
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a')));
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| {
|
||||
let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| {
|
||||
let snapshot = buffer.read(cx);
|
||||
let ranges = vec![
|
||||
snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)),
|
||||
@@ -4447,12 +4572,10 @@ mod tests {
|
||||
snapshot.anchor_before(Point::new(15, 0))
|
||||
..snapshot.anchor_before(Point::new(15, 0)),
|
||||
];
|
||||
multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx)
|
||||
multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx)
|
||||
});
|
||||
|
||||
let anchor_ranges = anchor_ranges.collect::<Vec<_>>().await;
|
||||
// Ensure task is finished when stream completes.
|
||||
task.await;
|
||||
|
||||
let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx));
|
||||
assert_eq!(
|
||||
@@ -4485,7 +4608,7 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_singleton_multibuffer_anchors(cx: &mut AppContext) {
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
|
||||
let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd"));
|
||||
let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
let old_snapshot = multibuffer.read(cx).snapshot(cx);
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
@@ -4505,8 +4628,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_multibuffer_anchors(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx));
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd"));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "efghi"));
|
||||
let multibuffer = cx.add_model(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(0);
|
||||
multibuffer.push_excerpts(
|
||||
@@ -4563,8 +4686,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) {
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx));
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd"));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "ABCDEFGHIJKLMNOP"));
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
|
||||
// Create an insertion id in buffer 1 that doesn't exist in buffer 2.
|
||||
@@ -4959,7 +5082,9 @@ mod tests {
|
||||
let base_text = util::RandomCharIter::new(&mut rng)
|
||||
.take(10)
|
||||
.collect::<String>();
|
||||
buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx)));
|
||||
buffers.push(
|
||||
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text)),
|
||||
);
|
||||
buffers.last().unwrap()
|
||||
} else {
|
||||
buffers.choose(&mut rng).unwrap()
|
||||
@@ -5300,8 +5425,8 @@ mod tests {
|
||||
fn test_history(cx: &mut AppContext) {
|
||||
cx.set_global(SettingsStore::test(cx));
|
||||
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx));
|
||||
let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "1234"));
|
||||
let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "5678"));
|
||||
let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
|
||||
let group_interval = multibuffer.read(cx).history.group_interval;
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::{
|
||||
display_map::{DisplaySnapshot, ToDisplayPoint},
|
||||
hover_popover::hide_hover,
|
||||
persistence::DB,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayRefreshReason, MultiBufferSnapshot,
|
||||
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
|
||||
ToPoint,
|
||||
};
|
||||
|
||||
@@ -29,6 +29,7 @@ use self::{
|
||||
};
|
||||
|
||||
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)]
|
||||
@@ -136,7 +137,7 @@ pub struct ScrollManager {
|
||||
impl ScrollManager {
|
||||
pub fn new() -> Self {
|
||||
ScrollManager {
|
||||
vertical_scroll_margin: 3.0,
|
||||
vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
|
||||
anchor: ScrollAnchor::new(),
|
||||
ongoing: OngoingScroll::new(),
|
||||
autoscroll_request: None,
|
||||
@@ -301,7 +302,7 @@ impl Editor {
|
||||
cx.spawn(|editor, mut cx| async move {
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
editor.refresh_inlays(InlayRefreshReason::NewLinesShown, cx)
|
||||
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
@@ -333,7 +334,7 @@ impl Editor {
|
||||
cx,
|
||||
);
|
||||
|
||||
self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx);
|
||||
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
|
||||
}
|
||||
|
||||
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
|
||||
|
||||
@@ -65,47 +65,52 @@ impl Editor {
|
||||
self.set_scroll_position(scroll_position, cx);
|
||||
}
|
||||
|
||||
let (autoscroll, local) =
|
||||
if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() {
|
||||
autoscroll
|
||||
} else {
|
||||
return false;
|
||||
};
|
||||
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let first_cursor_top;
|
||||
let last_cursor_bottom;
|
||||
let mut target_top;
|
||||
let mut target_bottom;
|
||||
if let Some(highlighted_rows) = &self.highlighted_rows {
|
||||
first_cursor_top = highlighted_rows.start as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
} else if autoscroll == Autoscroll::newest() {
|
||||
let newest_selection = self.selections.newest::<Point>(cx);
|
||||
first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
|
||||
last_cursor_bottom = first_cursor_top + 1.;
|
||||
target_top = highlighted_rows.start as f32;
|
||||
target_bottom = target_top + 1.;
|
||||
} else {
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
first_cursor_top = selections
|
||||
target_top = selections
|
||||
.first()
|
||||
.unwrap()
|
||||
.head()
|
||||
.to_display_point(&display_map)
|
||||
.row() as f32;
|
||||
last_cursor_bottom = selections
|
||||
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 - (last_cursor_bottom - first_cursor_top)) / 2.0).floor()
|
||||
((visible_lines - (target_bottom - target_top)) / 2.0).floor()
|
||||
};
|
||||
if margin < 0.0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let strategy = match autoscroll {
|
||||
Autoscroll::Strategy(strategy) => strategy,
|
||||
@@ -113,8 +118,8 @@ impl Editor {
|
||||
let last_autoscroll = &self.scroll_manager.last_autoscroll;
|
||||
if let Some(last_autoscroll) = last_autoscroll {
|
||||
if self.scroll_manager.anchor.offset == last_autoscroll.0
|
||||
&& first_cursor_top == last_autoscroll.1
|
||||
&& last_cursor_bottom == last_autoscroll.2
|
||||
&& target_top == last_autoscroll.1
|
||||
&& target_bottom == last_autoscroll.2
|
||||
{
|
||||
last_autoscroll.3.next()
|
||||
} else {
|
||||
@@ -129,37 +134,41 @@ impl Editor {
|
||||
match strategy {
|
||||
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
|
||||
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
|
||||
let target_top = (first_cursor_top - margin).max(0.0);
|
||||
let target_bottom = last_cursor_bottom + 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;
|
||||
|
||||
if target_top < start_row {
|
||||
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);
|
||||
} else if target_bottom >= end_row {
|
||||
}
|
||||
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((first_cursor_top - margin).max(0.0));
|
||||
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((first_cursor_top).max(0.0));
|
||||
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((last_cursor_bottom - visible_lines).max(0.0));
|
||||
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,
|
||||
first_cursor_top,
|
||||
last_cursor_bottom,
|
||||
target_top,
|
||||
target_bottom,
|
||||
strategy,
|
||||
));
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{
|
||||
cell::Ref,
|
||||
cmp, iter, mem,
|
||||
ops::{Deref, Range, Sub},
|
||||
ops::{Deref, DerefMut, Range, Sub},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ impl SelectionsCollection {
|
||||
}
|
||||
}
|
||||
|
||||
fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
|
||||
pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
|
||||
self.display_map.update(cx, |map, cx| map.snapshot(cx))
|
||||
}
|
||||
|
||||
@@ -250,6 +250,10 @@ impl SelectionsCollection {
|
||||
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,
|
||||
@@ -352,7 +356,7 @@ pub struct MutableSelectionsCollection<'a> {
|
||||
}
|
||||
|
||||
impl<'a> MutableSelectionsCollection<'a> {
|
||||
fn display_map(&mut self) -> DisplaySnapshot {
|
||||
pub fn display_map(&mut self) -> DisplaySnapshot {
|
||||
self.collection.display_map(self.cx)
|
||||
}
|
||||
|
||||
@@ -607,6 +611,10 @@ impl<'a> MutableSelectionsCollection<'a> {
|
||||
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>>,
|
||||
@@ -831,6 +839,12 @@ impl<'a> Deref for MutableSelectionsCollection<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::{
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use collections::HashSet;
|
||||
use futures::Future;
|
||||
use gpui::{json, ViewContext, ViewHandle};
|
||||
use indoc::indoc;
|
||||
@@ -154,10 +155,23 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
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()),
|
||||
@@ -169,6 +183,23 @@ impl<'a> EditorLspTestContext<'a> {
|
||||
("{" @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");
|
||||
|
||||
@@ -225,6 +225,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
.map(|h| h.1.clone())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|range| range.as_text_range())
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect()
|
||||
});
|
||||
@@ -240,6 +241,7 @@ impl<'a> EditorTestContext<'a> {
|
||||
.map(|ranges| ranges.as_ref().clone().1)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter_map(|range| range.as_text_range())
|
||||
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
|
||||
.collect();
|
||||
assert_set_eq!(actual_ranges, expected_ranges);
|
||||
|
||||
Reference in New Issue
Block a user