Merge branch 'main' into multi-server-completions-tailwind

This commit is contained in:
Julia
2023-08-30 22:58:37 -04:00
427 changed files with 43121 additions and 12859 deletions
+8 -5
View File
@@ -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();
}
+79 -18
View File
@@ -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));
+4 -4
View File
@@ -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,
+4 -4
View File
@@ -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,
+124 -76
View File
@@ -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);
+4 -4
View File
@@ -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,
+4 -4
View File
@@ -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,
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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)]
+96 -19
View File
@@ -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
View File
@@ -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]
+455 -12
View File
@@ -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
+20 -9
View File
@@ -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
+10 -9
View File
@@ -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(
+205 -80
View File
@@ -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| {
+5 -4
View File
@@ -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 {
+40 -31
View File
@@ -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,
));
+17 -3
View File
@@ -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);