This commit is contained in:
Conrad Irwin 2023-11-02 14:36:42 -06:00
parent 9e5275cc18
commit 09efa017d4
29 changed files with 42607 additions and 0 deletions

92
crates/editor2/Cargo.toml Normal file
View file

@ -0,0 +1,92 @@
[package]
name = "editor"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
path = "src/editor.rs"
doctest = false
[features]
test-support = [
"copilot/test-support",
"text/test-support",
"language/test-support",
"gpui/test-support",
"multi_buffer/test-support",
"project/test-support",
"util/test-support",
"workspace/test-support",
"tree-sitter-rust",
"tree-sitter-typescript"
]
[dependencies]
client = { path = "../client" }
clock = { path = "../clock" }
copilot = { path = "../copilot" }
db = { path = "../db" }
drag_and_drop = { path = "../drag_and_drop" }
collections = { path = "../collections" }
context_menu = { path = "../context_menu" }
fuzzy = { path = "../fuzzy" }
git = { path = "../git" }
gpui = { path = "../gpui" }
language = { path = "../language" }
lsp = { path = "../lsp" }
multi_buffer = { path = "../multi_buffer" }
project = { path = "../project" }
rpc = { path = "../rpc" }
rich_text = { path = "../rich_text" }
settings = { path = "../settings" }
snippet = { path = "../snippet" }
sum_tree = { path = "../sum_tree" }
text = { path = "../text" }
theme = { path = "../theme" }
util = { path = "../util" }
sqlez = { path = "../sqlez" }
workspace = { path = "../workspace" }
aho-corasick = "1.1"
anyhow.workspace = true
convert_case = "0.6.0"
futures.workspace = true
indoc = "1.0.4"
itertools = "0.10"
lazy_static.workspace = true
log.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
postage.workspace = true
rand.workspace = true
schemars.workspace = true
serde.workspace = true
serde_derive.workspace = true
smallvec.workspace = true
smol.workspace = true
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
[dev-dependencies]
copilot = { path = "../copilot", features = ["test-support"] }
text = { path = "../text", features = ["test-support"] }
language = { path = "../language", features = ["test-support"] }
lsp = { path = "../lsp", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }
project = { path = "../project", features = ["test-support"] }
settings = { path = "../settings", features = ["test-support"] }
workspace = { path = "../workspace", features = ["test-support"] }
multi_buffer = { path = "../multi_buffer", features = ["test-support"] }
ctor.workspace = true
env_logger.workspace = true
rand.workspace = true
unindent.workspace = true
tree-sitter.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-html.workspace = true
tree-sitter-typescript.workspace = true

View file

@ -0,0 +1,113 @@
use crate::EditorSettings;
use gpui::{Entity, ModelContext};
use settings::SettingsStore;
use smol::Timer;
use std::time::Duration;
pub struct BlinkManager {
blink_interval: Duration,
blink_epoch: usize,
blinking_paused: bool,
visible: bool,
enabled: bool,
}
impl BlinkManager {
pub fn new(blink_interval: Duration, cx: &mut ModelContext<Self>) -> Self {
// Make sure we blink the cursors if the setting is re-enabled
cx.observe_global::<SettingsStore, _>(move |this, cx| {
this.blink_cursors(this.blink_epoch, cx)
})
.detach();
Self {
blink_interval,
blink_epoch: 0,
blinking_paused: false,
visible: true,
enabled: false,
}
}
fn next_blink_epoch(&mut self) -> usize {
self.blink_epoch += 1;
self.blink_epoch
}
pub fn pause_blinking(&mut self, cx: &mut ModelContext<Self>) {
self.show_cursor(cx);
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(interval).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
}
}
})
.detach();
}
fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if epoch == self.blink_epoch {
self.blinking_paused = false;
self.blink_cursors(epoch, cx);
}
}
fn blink_cursors(&mut self, epoch: usize, cx: &mut ModelContext<Self>) {
if settings::get::<EditorSettings>(cx).cursor_blink {
if epoch == self.blink_epoch && self.enabled && !self.blinking_paused {
self.visible = !self.visible;
cx.notify();
let epoch = self.next_blink_epoch();
let interval = self.blink_interval;
cx.spawn(|this, mut cx| {
let this = this.downgrade();
async move {
Timer::after(interval).await;
if let Some(this) = this.upgrade(&cx) {
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
}
}
})
.detach();
}
} else {
self.show_cursor(cx);
}
}
pub fn show_cursor(&mut self, cx: &mut ModelContext<'_, BlinkManager>) {
if !self.visible {
self.visible = true;
cx.notify();
}
}
pub fn enable(&mut self, cx: &mut ModelContext<Self>) {
self.enabled = true;
// Set cursors as invisible and start blinking: this causes cursors
// to be visible during the next render.
self.visible = false;
self.blink_cursors(self.blink_epoch, cx);
}
pub fn disable(&mut self, _cx: &mut ModelContext<Self>) {
self.enabled = false;
}
pub fn visible(&self) -> bool {
self.visible
}
}
impl Entity for BlinkManager {
type Event = ();
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,765 @@
use super::{
fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot},
Highlights,
};
use crate::MultiBufferSnapshot;
use language::{Chunk, Point};
use std::{cmp, mem, num::NonZeroU32, ops::Range};
use sum_tree::Bias;
const MAX_EXPANSION_COLUMN: u32 = 256;
pub struct TabMap(TabSnapshot);
impl TabMap {
pub fn new(fold_snapshot: FoldSnapshot, tab_size: NonZeroU32) -> (Self, TabSnapshot) {
let snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: MAX_EXPANSION_COLUMN,
version: 0,
};
(Self(snapshot.clone()), snapshot)
}
#[cfg(test)]
pub fn set_max_expansion_column(&mut self, column: u32) -> TabSnapshot {
self.0.max_expansion_column = column;
self.0.clone()
}
pub fn sync(
&mut self,
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
tab_size: NonZeroU32,
) -> (TabSnapshot, Vec<TabEdit>) {
let old_snapshot = &mut self.0;
let mut new_snapshot = TabSnapshot {
fold_snapshot,
tab_size,
max_expansion_column: old_snapshot.max_expansion_column,
version: old_snapshot.version,
};
if old_snapshot.fold_snapshot.version != new_snapshot.fold_snapshot.version {
new_snapshot.version += 1;
}
let mut tab_edits = Vec::with_capacity(fold_edits.len());
if old_snapshot.tab_size == new_snapshot.tab_size {
// Expand each edit to include the next tab on the same line as the edit,
// and any subsequent tabs on that line that moved across the tab expansion
// boundary.
for fold_edit in &mut fold_edits {
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
let old_end_row_successor_offset = cmp::min(
FoldPoint::new(old_end.row() + 1, 0),
old_snapshot.fold_snapshot.max_point(),
)
.to_offset(&old_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
let mut offset_from_edit = 0;
let mut first_tab_offset = None;
let mut last_tab_with_changed_expansion_offset = None;
'outer: for chunk in old_snapshot.fold_snapshot.chunks(
fold_edit.old.end..old_end_row_successor_offset,
false,
Highlights::default(),
) {
for (ix, _) in chunk.text.match_indices('\t') {
let offset_from_edit = offset_from_edit + (ix as u32);
if first_tab_offset.is_none() {
first_tab_offset = Some(offset_from_edit);
}
let old_column = old_end.column() + offset_from_edit;
let new_column = new_end.column() + offset_from_edit;
let was_expanded = old_column < old_snapshot.max_expansion_column;
let is_expanded = new_column < new_snapshot.max_expansion_column;
if was_expanded != is_expanded {
last_tab_with_changed_expansion_offset = Some(offset_from_edit);
} else if !was_expanded && !is_expanded {
break 'outer;
}
}
offset_from_edit += chunk.text.len() as u32;
if old_end.column() + offset_from_edit >= old_snapshot.max_expansion_column
&& new_end.column() + offset_from_edit >= new_snapshot.max_expansion_column
{
break;
}
}
if let Some(offset) = last_tab_with_changed_expansion_offset.or(first_tab_offset) {
fold_edit.old.end.0 += offset as usize + 1;
fold_edit.new.end.0 += offset as usize + 1;
}
}
// Combine any edits that overlap due to the expansion.
let mut ix = 1;
while ix < fold_edits.len() {
let (prev_edits, next_edits) = fold_edits.split_at_mut(ix);
let prev_edit = prev_edits.last_mut().unwrap();
let edit = &next_edits[0];
if prev_edit.old.end >= edit.old.start {
prev_edit.old.end = edit.old.end;
prev_edit.new.end = edit.new.end;
fold_edits.remove(ix);
} else {
ix += 1;
}
}
for fold_edit in fold_edits {
let old_start = fold_edit.old.start.to_point(&old_snapshot.fold_snapshot);
let old_end = fold_edit.old.end.to_point(&old_snapshot.fold_snapshot);
let new_start = fold_edit.new.start.to_point(&new_snapshot.fold_snapshot);
let new_end = fold_edit.new.end.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(TabEdit {
old: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
});
}
} else {
new_snapshot.version += 1;
tab_edits.push(TabEdit {
old: TabPoint::zero()..old_snapshot.max_point(),
new: TabPoint::zero()..new_snapshot.max_point(),
});
}
*old_snapshot = new_snapshot;
(old_snapshot.clone(), tab_edits)
}
}
#[derive(Clone)]
pub struct TabSnapshot {
pub fold_snapshot: FoldSnapshot,
pub tab_size: NonZeroU32,
pub max_expansion_column: u32,
pub version: usize,
}
impl TabSnapshot {
pub fn buffer_snapshot(&self) -> &MultiBufferSnapshot {
&self.fold_snapshot.inlay_snapshot.buffer
}
pub fn line_len(&self, row: u32) -> u32 {
let max_point = self.max_point();
if row < max_point.row() {
self.to_tab_point(FoldPoint::new(row, self.fold_snapshot.line_len(row)))
.0
.column
} else {
max_point.column()
}
}
pub fn text_summary(&self) -> TextSummary {
self.text_summary_for_range(TabPoint::zero()..self.max_point())
}
pub fn text_summary_for_range(&self, range: Range<TabPoint>) -> TextSummary {
let input_start = self.to_fold_point(range.start, Bias::Left).0;
let input_end = self.to_fold_point(range.end, Bias::Right).0;
let input_summary = self
.fold_snapshot
.text_summary_for_range(input_start..input_end);
let mut first_line_chars = 0;
let line_end = if range.start.row() == range.end.row() {
range.end
} else {
self.max_point()
};
for c in self
.chunks(range.start..line_end, false, Highlights::default())
.flat_map(|chunk| chunk.text.chars())
{
if c == '\n' {
break;
}
first_line_chars += 1;
}
let mut last_line_chars = 0;
if range.start.row() == range.end.row() {
last_line_chars = first_line_chars;
} else {
for _ in self
.chunks(
TabPoint::new(range.end.row(), 0)..range.end,
false,
Highlights::default(),
)
.flat_map(|chunk| chunk.text.chars())
{
last_line_chars += 1;
}
}
TextSummary {
lines: range.end.0 - range.start.0,
first_line_chars,
last_line_chars,
longest_row: input_summary.longest_row,
longest_row_chars: input_summary.longest_row_chars,
}
}
pub fn chunks<'a>(
&'a self,
range: Range<TabPoint>,
language_aware: bool,
highlights: Highlights<'a>,
) -> TabChunks<'a> {
let (input_start, expanded_char_column, to_next_stop) =
self.to_fold_point(range.start, Bias::Left);
let input_column = input_start.column();
let input_start = input_start.to_offset(&self.fold_snapshot);
let input_end = self
.to_fold_point(range.end, Bias::Right)
.0
.to_offset(&self.fold_snapshot);
let to_next_stop = if range.start.0 + Point::new(0, to_next_stop) > range.end.0 {
range.end.column() - range.start.column()
} else {
to_next_stop
};
TabChunks {
fold_chunks: self.fold_snapshot.chunks(
input_start..input_end,
language_aware,
highlights,
),
input_column,
column: expanded_char_column,
max_expansion_column: self.max_expansion_column,
output_position: range.start.0,
max_output_position: range.end.0,
tab_size: self.tab_size,
chunk: Chunk {
text: &SPACES[0..(to_next_stop as usize)],
is_tab: true,
..Default::default()
},
inside_leading_tab: to_next_stop > 0,
}
}
pub fn buffer_rows(&self, row: u32) -> fold_map::FoldBufferRows<'_> {
self.fold_snapshot.buffer_rows(row)
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(
TabPoint::zero()..self.max_point(),
false,
Highlights::default(),
)
.map(|chunk| chunk.text)
.collect()
}
pub fn max_point(&self) -> TabPoint {
self.to_tab_point(self.fold_snapshot.max_point())
}
pub fn clip_point(&self, point: TabPoint, bias: Bias) -> TabPoint {
self.to_tab_point(
self.fold_snapshot
.clip_point(self.to_fold_point(point, bias).0, bias),
)
}
pub fn to_tab_point(&self, input: FoldPoint) -> TabPoint {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(input.row(), 0));
let expanded = self.expand_tabs(chars, input.column());
TabPoint::new(input.row(), expanded)
}
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, u32, u32) {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column();
let (collapsed, expanded_char_column, to_next_stop) =
self.collapse_tabs(chars, expanded, bias);
(
FoldPoint::new(output.row(), collapsed as u32),
expanded_char_column,
to_next_stop,
)
}
pub fn make_tab_point(&self, point: Point, bias: Bias) -> TabPoint {
let inlay_point = self.fold_snapshot.inlay_snapshot.to_inlay_point(point);
let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias);
self.to_tab_point(fold_point)
}
pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point {
let fold_point = self.to_fold_point(point, bias).0;
let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot);
self.fold_snapshot
.inlay_snapshot
.to_buffer_point(inlay_point)
}
fn expand_tabs(&self, chars: impl Iterator<Item = char>, column: u32) -> u32 {
let tab_size = self.tab_size.get();
let mut expanded_chars = 0;
let mut expanded_bytes = 0;
let mut collapsed_bytes = 0;
let end_column = column.min(self.max_expansion_column);
for c in chars {
if collapsed_bytes >= end_column {
break;
}
if c == '\t' {
let tab_len = tab_size - expanded_chars % tab_size;
expanded_bytes += tab_len;
expanded_chars += tab_len;
} else {
expanded_bytes += c.len_utf8() as u32;
expanded_chars += 1;
}
collapsed_bytes += c.len_utf8() as u32;
}
expanded_bytes + column.saturating_sub(collapsed_bytes)
}
fn collapse_tabs(
&self,
chars: impl Iterator<Item = char>,
column: u32,
bias: Bias,
) -> (u32, u32, u32) {
let tab_size = self.tab_size.get();
let mut expanded_bytes = 0;
let mut expanded_chars = 0;
let mut collapsed_bytes = 0;
for c in chars {
if expanded_bytes >= column {
break;
}
if collapsed_bytes >= self.max_expansion_column {
break;
}
if c == '\t' {
let tab_len = tab_size - (expanded_chars % tab_size);
expanded_chars += tab_len;
expanded_bytes += tab_len;
if expanded_bytes > column {
expanded_chars -= expanded_bytes - column;
return match bias {
Bias::Left => (collapsed_bytes, expanded_chars, expanded_bytes - column),
Bias::Right => (collapsed_bytes + 1, expanded_chars, 0),
};
}
} else {
expanded_chars += 1;
expanded_bytes += c.len_utf8() as u32;
}
if expanded_bytes > column && matches!(bias, Bias::Left) {
expanded_chars -= 1;
break;
}
collapsed_bytes += c.len_utf8() as u32;
}
(
collapsed_bytes + column.saturating_sub(expanded_bytes),
expanded_chars,
0,
)
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct TabPoint(pub Point);
impl TabPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(Point::new(row, column))
}
pub fn zero() -> Self {
Self::new(0, 0)
}
pub fn row(self) -> u32 {
self.0.row
}
pub fn column(self) -> u32 {
self.0.column
}
}
impl From<Point> for TabPoint {
fn from(point: Point) -> Self {
Self(point)
}
}
pub type TabEdit = text::Edit<TabPoint>;
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
pub lines: Point,
pub first_line_chars: u32,
pub last_line_chars: u32,
pub longest_row: u32,
pub longest_row_chars: u32,
}
impl<'a> From<&'a str> for TextSummary {
fn from(text: &'a str) -> Self {
let sum = text::TextSummary::from(text);
TextSummary {
lines: sum.lines,
first_line_chars: sum.first_line_chars,
last_line_chars: sum.last_line_chars,
longest_row: sum.longest_row,
longest_row_chars: sum.longest_row_chars,
}
}
}
impl<'a> std::ops::AddAssign<&'a Self> for TextSummary {
fn add_assign(&mut self, other: &'a Self) {
let joined_chars = self.last_line_chars + other.first_line_chars;
if joined_chars > self.longest_row_chars {
self.longest_row = self.lines.row;
self.longest_row_chars = joined_chars;
}
if other.longest_row_chars > self.longest_row_chars {
self.longest_row = self.lines.row + other.longest_row;
self.longest_row_chars = other.longest_row_chars;
}
if self.lines.row == 0 {
self.first_line_chars += other.first_line_chars;
}
if other.lines.row == 0 {
self.last_line_chars += other.first_line_chars;
} else {
self.last_line_chars = other.last_line_chars;
}
self.lines += &other.lines;
}
}
// Handles a tab width <= 16
const SPACES: &str = " ";
pub struct TabChunks<'a> {
fold_chunks: FoldChunks<'a>,
chunk: Chunk<'a>,
column: u32,
max_expansion_column: u32,
output_position: Point,
input_column: u32,
max_output_position: Point,
tab_size: NonZeroU32,
inside_leading_tab: bool,
}
impl<'a> Iterator for TabChunks<'a> {
type Item = Chunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.text.is_empty() {
if let Some(chunk) = self.fold_chunks.next() {
self.chunk = chunk;
if self.inside_leading_tab {
self.chunk.text = &self.chunk.text[1..];
self.inside_leading_tab = false;
self.input_column += 1;
}
} else {
return None;
}
}
for (ix, c) in self.chunk.text.char_indices() {
match c {
'\t' => {
if ix > 0 {
let (prefix, suffix) = self.chunk.text.split_at(ix);
self.chunk.text = suffix;
return Some(Chunk {
text: prefix,
..self.chunk
});
} else {
self.chunk.text = &self.chunk.text[1..];
let tab_size = if self.input_column < self.max_expansion_column {
self.tab_size.get() as u32
} else {
1
};
let mut len = tab_size - self.column % tab_size;
let next_output_position = cmp::min(
self.output_position + Point::new(0, len),
self.max_output_position,
);
len = next_output_position.column - self.output_position.column;
self.column += len;
self.input_column += 1;
self.output_position = next_output_position;
return Some(Chunk {
text: &SPACES[..len as usize],
is_tab: true,
..self.chunk
});
}
}
'\n' => {
self.column = 0;
self.input_column = 0;
self.output_position += Point::new(1, 0);
}
_ => {
self.column += 1;
if !self.inside_leading_tab {
self.input_column += c.len_utf8() as u32;
}
self.output_position.column += c.len_utf8() as u32;
}
}
}
Some(mem::take(&mut self.chunk))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
display_map::{fold_map::FoldMap, inlay_map::InlayMap},
MultiBuffer,
};
use rand::{prelude::StdRng, Rng};
#[gpui::test]
fn test_expand_tabs(cx: &mut gpui::AppContext) {
let buffer = MultiBuffer::build_simple("", cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 0), 0);
assert_eq!(tab_snapshot.expand_tabs("\t".chars(), 1), 4);
assert_eq!(tab_snapshot.expand_tabs("\ta".chars(), 2), 5);
}
#[gpui::test]
fn test_long_lines(cx: &mut gpui::AppContext) {
let max_expansion_column = 12;
let input = "A\tBC\tDEF\tG\tHI\tJ\tK\tL\tM";
let output = "A BC DEF G HI J K L M";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), output);
for (ix, c) in input.char_indices() {
assert_eq!(
tab_snapshot
.chunks(
TabPoint::new(0, ix as u32)..tab_snapshot.max_point(),
false,
Highlights::default(),
)
.map(|c| c.text)
.collect::<String>(),
&output[ix..],
"text from index {ix}"
);
if c != '\t' {
let input_point = Point::new(0, ix as u32);
let output_point = Point::new(0, output.find(c).unwrap() as u32);
assert_eq!(
tab_snapshot.to_tab_point(FoldPoint(input_point)),
TabPoint(output_point),
"to_tab_point({input_point:?})"
);
assert_eq!(
tab_snapshot
.to_fold_point(TabPoint(output_point), Bias::Left)
.0,
FoldPoint(input_point),
"to_fold_point({output_point:?})"
);
}
}
}
#[gpui::test]
fn test_long_lines_with_character_spanning_max_expansion_column(cx: &mut gpui::AppContext) {
let max_expansion_column = 8;
let input = "abcdefg⋯hij";
let buffer = MultiBuffer::build_simple(input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, mut tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
tab_snapshot.max_expansion_column = max_expansion_column;
assert_eq!(tab_snapshot.text(), input);
}
#[gpui::test]
fn test_marking_tabs(cx: &mut gpui::AppContext) {
let input = "\t \thello";
let buffer = MultiBuffer::build_simple(&input, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let (_, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
let (_, fold_snapshot) = FoldMap::new(inlay_snapshot);
let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap());
assert_eq!(
chunks(&tab_snapshot, TabPoint::zero()),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
assert_eq!(
chunks(&tab_snapshot, TabPoint::new(0, 2)),
vec![
(" ".to_string(), true),
(" ".to_string(), false),
(" ".to_string(), true),
("hello".to_string(), false),
]
);
fn chunks(snapshot: &TabSnapshot, start: TabPoint) -> Vec<(String, bool)> {
let mut chunks = Vec::new();
let mut was_tab = false;
let mut text = String::new();
for chunk in snapshot.chunks(start..snapshot.max_point(), false, Highlights::default())
{
if chunk.is_tab != was_tab {
if !text.is_empty() {
chunks.push((mem::take(&mut text), was_tab));
}
was_tab = chunk.is_tab;
}
text.push_str(chunk.text);
}
if !text.is_empty() {
chunks.push((text, was_tab));
}
chunks
}
}
#[gpui::test(iterations = 100)]
fn test_random_tabs(cx: &mut gpui::AppContext, mut rng: StdRng) {
let tab_size = NonZeroU32::new(rng.gen_range(1..=4)).unwrap();
let len = rng.gen_range(0..30);
let buffer = if rng.gen() {
let text = util::RandomCharIter::new(&mut rng)
.take(len)
.collect::<String>();
MultiBuffer::build_simple(&text, cx)
} else {
MultiBuffer::build_random(&mut rng, cx)
};
let buffer_snapshot = buffer.read(cx).snapshot(cx);
log::info!("Buffer text: {:?}", buffer_snapshot.text());
let (mut inlay_map, inlay_snapshot) = InlayMap::new(buffer_snapshot.clone());
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut fold_map, _) = FoldMap::new(inlay_snapshot.clone());
fold_map.randomly_mutate(&mut rng);
let (fold_snapshot, _) = fold_map.read(inlay_snapshot, vec![]);
log::info!("FoldMap text: {:?}", fold_snapshot.text());
let (inlay_snapshot, _) = inlay_map.randomly_mutate(&mut 0, &mut rng);
log::info!("InlayMap text: {:?}", inlay_snapshot.text());
let (mut tab_map, _) = TabMap::new(fold_snapshot.clone(), tab_size);
let tabs_snapshot = tab_map.set_max_expansion_column(32);
let text = text::Rope::from(tabs_snapshot.text().as_str());
log::info!(
"TabMap text (tab size: {}): {:?}",
tab_size,
tabs_snapshot.text(),
);
for _ in 0..5 {
let end_row = rng.gen_range(0..=text.max_point().row);
let end_column = rng.gen_range(0..=text.line_len(end_row));
let mut end = TabPoint(text.clip_point(Point::new(end_row, end_column), Bias::Right));
let start_row = rng.gen_range(0..=text.max_point().row);
let start_column = rng.gen_range(0..=text.line_len(start_row));
let mut start =
TabPoint(text.clip_point(Point::new(start_row, start_column), Bias::Left));
if start > end {
mem::swap(&mut start, &mut end);
}
let expected_text = text
.chunks_in_range(text.point_to_offset(start.0)..text.point_to_offset(end.0))
.collect::<String>();
let expected_summary = TextSummary::from(expected_text.as_str());
assert_eq!(
tabs_snapshot
.chunks(start..end, false, Highlights::default())
.map(|c| c.text)
.collect::<String>(),
expected_text,
"chunks({:?}..{:?})",
start,
end
);
let mut actual_summary = tabs_snapshot.text_summary_for_range(start..end);
if tab_size.get() > 1 && inlay_snapshot.text().contains('\t') {
actual_summary.longest_row = expected_summary.longest_row;
actual_summary.longest_row_chars = expected_summary.longest_row_chars;
}
assert_eq!(actual_summary, expected_summary);
}
for row in 0..=text.max_point().row {
assert_eq!(
tabs_snapshot.line_len(row),
text.line_len(row),
"line_len({row})"
);
}
}
}

File diff suppressed because it is too large Load diff

10095
crates/editor2/src/editor.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,62 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Setting;
#[derive(Deserialize)]
pub struct EditorSettings {
pub cursor_blink: bool,
pub hover_popover_enabled: bool,
pub show_completions_on_input: bool,
pub show_completion_documentation: bool,
pub use_on_type_format: bool,
pub scrollbar: Scrollbar,
pub relative_line_numbers: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Scrollbar {
pub show: ShowScrollbar,
pub git_diff: bool,
pub selections: bool,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ShowScrollbar {
Auto,
System,
Always,
Never,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct EditorSettingsContent {
pub cursor_blink: Option<bool>,
pub hover_popover_enabled: Option<bool>,
pub show_completions_on_input: Option<bool>,
pub show_completion_documentation: Option<bool>,
pub use_on_type_format: Option<bool>,
pub scrollbar: Option<ScrollbarContent>,
pub relative_line_numbers: Option<bool>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct ScrollbarContent {
pub show: Option<ShowScrollbar>,
pub git_diff: Option<bool>,
pub selections: Option<bool>,
}
impl Setting for EditorSettings {
const KEY: Option<&'static str> = None;
type FileContent = EditorSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

282
crates/editor2/src/git.rs Normal file
View file

@ -0,0 +1,282 @@
use std::ops::Range;
use git::diff::{DiffHunk, DiffHunkStatus};
use language::Point;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
AnchorRangeExt,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DisplayDiffHunk {
Folded {
display_row: u32,
},
Unfolded {
display_row_range: Range<u32>,
status: DiffHunkStatus,
},
}
impl DisplayDiffHunk {
pub fn start_display_row(&self) -> u32 {
match self {
&DisplayDiffHunk::Folded { display_row } => display_row,
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => display_row_range.start,
}
}
pub fn contains_display_row(&self, display_row: u32) -> bool {
let range = match self {
&DisplayDiffHunk::Folded { display_row } => display_row..=display_row,
DisplayDiffHunk::Unfolded {
display_row_range, ..
} => display_row_range.start..=display_row_range.end,
};
range.contains(&display_row)
}
}
pub fn diff_hunk_to_display(hunk: DiffHunk<u32>, snapshot: &DisplaySnapshot) -> DisplayDiffHunk {
let hunk_start_point = Point::new(hunk.buffer_range.start, 0);
let hunk_start_point_sub = Point::new(hunk.buffer_range.start.saturating_sub(1), 0);
let hunk_end_point_sub = Point::new(
hunk.buffer_range
.end
.saturating_sub(1)
.max(hunk.buffer_range.start),
0,
);
let is_removal = hunk.status() == DiffHunkStatus::Removed;
let folds_start = Point::new(hunk.buffer_range.start.saturating_sub(2), 0);
let folds_end = Point::new(hunk.buffer_range.end + 2, 0);
let folds_range = folds_start..folds_end;
let containing_fold = snapshot.folds_in_range(folds_range).find(|fold_range| {
let fold_point_range = fold_range.to_point(&snapshot.buffer_snapshot);
let fold_point_range = fold_point_range.start..=fold_point_range.end;
let folded_start = fold_point_range.contains(&hunk_start_point);
let folded_end = fold_point_range.contains(&hunk_end_point_sub);
let folded_start_sub = fold_point_range.contains(&hunk_start_point_sub);
(folded_start && folded_end) || (is_removal && folded_start_sub)
});
if let Some(fold) = containing_fold {
let row = fold.start.to_display_point(snapshot).row();
DisplayDiffHunk::Folded { display_row: row }
} else {
let start = hunk_start_point.to_display_point(snapshot).row();
let hunk_end_row = hunk.buffer_range.end.max(hunk.buffer_range.start);
let hunk_end_point = Point::new(hunk_end_row, 0);
let end = hunk_end_point.to_display_point(snapshot).row();
DisplayDiffHunk::Unfolded {
display_row_range: start..end,
status: hunk.status(),
}
}
}
#[cfg(any(test, feature = "test_support"))]
mod tests {
use crate::editor_tests::init_test;
use crate::Point;
use gpui::TestAppContext;
use multi_buffer::{ExcerptRange, MultiBuffer};
use project::{FakeFs, Project};
use unindent::Unindent;
#[gpui::test]
async fn test_diff_hunks_in_range(cx: &mut TestAppContext) {
use git::diff::DiffHunkStatus;
init_test(cx, |_| {});
let fs = FakeFs::new(cx.background());
let project = Project::test(fs, [], cx).await;
// buffer has two modified hunks with two rows each
let buffer_1 = project
.update(cx, |project, cx| {
project.create_buffer(
"
1.zero
1.ONE
1.TWO
1.three
1.FOUR
1.FIVE
1.six
"
.unindent()
.as_str(),
None,
cx,
)
})
.unwrap();
buffer_1.update(cx, |buffer, cx| {
buffer.set_diff_base(
Some(
"
1.zero
1.one
1.two
1.three
1.four
1.five
1.six
"
.unindent(),
),
cx,
);
});
// buffer has a deletion hunk and an insertion hunk
let buffer_2 = project
.update(cx, |project, cx| {
project.create_buffer(
"
2.zero
2.one
2.two
2.three
2.four
2.five
2.six
"
.unindent()
.as_str(),
None,
cx,
)
})
.unwrap();
buffer_2.update(cx, |buffer, cx| {
buffer.set_diff_base(
Some(
"
2.zero
2.one
2.one-and-a-half
2.two
2.three
2.four
2.six
"
.unindent(),
),
cx,
);
});
cx.foreground().run_until_parked();
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts(
buffer_1.clone(),
[
// excerpt ends in the middle of a modified hunk
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt begins in the middle of a modified hunk
ExcerptRange {
context: Point::new(5, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer.push_excerpts(
buffer_2.clone(),
[
// excerpt ends at a deletion
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 5),
primary: Default::default(),
},
// excerpt starts at a deletion
ExcerptRange {
context: Point::new(2, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains a deletion hunk
ExcerptRange {
context: Point::new(1, 0)..Point::new(2, 5),
primary: Default::default(),
},
// excerpt fully contains an insertion hunk
ExcerptRange {
context: Point::new(4, 0)..Point::new(6, 5),
primary: Default::default(),
},
],
cx,
);
multibuffer
});
let snapshot = multibuffer.read_with(cx, |b, cx| b.snapshot(cx));
assert_eq!(
snapshot.text(),
"
1.zero
1.ONE
1.FIVE
1.six
2.zero
2.one
2.two
2.one
2.two
2.four
2.five
2.six"
.unindent()
);
let expected = [
(DiffHunkStatus::Modified, 1..2),
(DiffHunkStatus::Modified, 2..3),
//TODO: Define better when and where removed hunks show up at range extremities
(DiffHunkStatus::Removed, 6..6),
(DiffHunkStatus::Removed, 8..8),
(DiffHunkStatus::Added, 10..11),
];
assert_eq!(
snapshot
.git_diff_hunks_in_range(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
&expected,
);
assert_eq!(
snapshot
.git_diff_hunks_in_range_rev(0..12)
.map(|hunk| (hunk.status(), hunk.buffer_range))
.collect::<Vec<_>>(),
expected
.iter()
.rev()
.cloned()
.collect::<Vec<_>>()
.as_slice(),
);
}
}

View file

@ -0,0 +1,138 @@
use gpui::ViewContext;
use crate::{Editor, RangeToAnchorExt};
enum MatchingBracketHighlight {}
pub fn refresh_matching_bracket_highlights(editor: &mut Editor, cx: &mut ViewContext<Editor>) {
editor.clear_background_highlights::<MatchingBracketHighlight>(cx);
let newest_selection = editor.selections.newest::<usize>(cx);
// Don't highlight brackets if the selection isn't empty
if !newest_selection.is_empty() {
return;
}
let head = newest_selection.head();
let snapshot = editor.snapshot(cx);
if let Some((opening_range, closing_range)) = snapshot
.buffer_snapshot
.innermost_enclosing_bracket_ranges(head..head)
{
editor.highlight_background::<MatchingBracketHighlight>(
vec![
opening_range.to_anchors(&snapshot.buffer_snapshot),
closing_range.to_anchors(&snapshot.buffer_snapshot),
],
|theme| theme.editor.document_highlight_read_background,
cx,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use indoc::indoc;
use language::{BracketPair, BracketPairConfig, Language, LanguageConfig};
#[gpui::test]
async fn test_matching_bracket_highlights(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new(
Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
brackets: BracketPairConfig {
pairs: vec![
BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: false,
newline: true,
},
BracketPair {
start: "(".to_string(),
end: ")".to_string(),
close: false,
newline: true,
},
],
..Default::default()
},
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_brackets_query(indoc! {r#"
("{" @open "}" @close)
("(" @open ")" @close)
"#})
.unwrap(),
Default::default(),
cx,
)
.await;
// positioning cursor inside bracket highlights both
cx.set_state(indoc! {r#"
pub fn test("Test ˇargument") {
another_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test«(»"Test argument"«)» {
another_test(1, 2, 3);
}
"#});
cx.set_state(indoc! {r#"
pub fn test("Test argument") {
another_test(1, ˇ2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test«(»1, 2, 3«)»;
}
"#});
cx.set_state(indoc! {r#"
pub fn test("Test argument") {
anotherˇ_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") «{»
another_test(1, 2, 3);
«}»
"#});
// positioning outside of brackets removes highlight
cx.set_state(indoc! {r#"
pub fˇn test("Test argument") {
another_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test(1, 2, 3);
}
"#});
// non empty selection dismisses highlight
cx.set_state(indoc! {r#"
pub fn test("Te«st argˇ»ument") {
another_test(1, 2, 3);
}
"#});
cx.assert_editor_background_highlights::<MatchingBracketHighlight>(indoc! {r#"
pub fn test("Test argument") {
another_test(1, 2, 3);
}
"#});
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

1327
crates/editor2/src/items.rs Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,96 @@
use crate::{
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToTypeDefinition,
Rename, RevealInFinder, SelectMode, ToggleCodeActions,
};
use context_menu::ContextMenuItem;
use gpui::{elements::AnchorCorner, geometry::vector::Vector2F, ViewContext};
pub fn deploy_context_menu(
editor: &mut Editor,
position: Vector2F,
point: DisplayPoint,
cx: &mut ViewContext<Editor>,
) {
if !editor.focused {
cx.focus_self();
}
// Don't show context menu for inline editors
if editor.mode() != EditorMode::Full {
return;
}
// Don't show the context menu if there isn't a project associated with this editor
if editor.project.is_none() {
return;
}
// Move the cursor to the clicked location so that dispatched actions make sense
editor.change_selections(None, cx, |s| {
s.clear_disjoint();
s.set_pending_display_range(point..point, SelectMode::Character);
});
editor.mouse_context_menu.update(cx, |menu, cx| {
menu.show(
position,
AnchorCorner::TopLeft,
vec![
ContextMenuItem::action("Rename Symbol", Rename),
ContextMenuItem::action("Go to Definition", GoToDefinition),
ContextMenuItem::action("Go to Type Definition", GoToTypeDefinition),
ContextMenuItem::action("Find All References", FindAllReferences),
ContextMenuItem::action(
"Code Actions",
ToggleCodeActions {
deployed_from_indicator: false,
},
),
ContextMenuItem::Separator,
ContextMenuItem::action("Reveal in Finder", RevealInFinder),
],
cx,
);
});
cx.notify();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
use indoc::indoc;
#[gpui::test]
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorLspTestContext::new_rust(
lsp::ServerCapabilities {
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
..Default::default()
},
cx,
)
.await;
cx.set_state(indoc! {"
fn teˇst() {
do_work();
}
"});
let point = cx.display_point(indoc! {"
fn test() {
do_wˇork();
}
"});
cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
cx.assert_editor_state(indoc! {"
fn test() {
do_wˇork();
}
"});
cx.editor(|editor, app| assert!(editor.mouse_context_menu.read(app).visible()));
}
}

View file

@ -0,0 +1,927 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{char_kind, CharKind, EditorStyle, ToOffset, ToPoint};
use gpui::{FontCache, TextLayoutCache};
use language::Point;
use std::{ops::Range, sync::Arc};
#[derive(Debug, PartialEq)]
pub enum FindRange {
SingleLine,
MultiLine,
}
/// TextLayoutDetails encompasses everything we need to move vertically
/// taking into account variable width characters.
pub struct TextLayoutDetails {
pub font_cache: Arc<FontCache>,
pub text_layout_cache: Arc<TextLayoutCache>,
pub editor_style: EditorStyle,
}
pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
} else if point.row() > 0 {
*point.row_mut() -= 1;
*point.column_mut() = map.line_len(point.row());
}
map.clip_point(point, Bias::Left)
}
pub fn saturating_left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
if point.column() > 0 {
*point.column_mut() -= 1;
}
map.clip_point(point, Bias::Left)
}
pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
let max_column = map.line_len(point.row());
if point.column() < max_column {
*point.column_mut() += 1;
} else if point.row() < map.max_point().row() {
*point.row_mut() += 1;
*point.column_mut() = 0;
}
map.clip_point(point, Bias::Right)
}
pub fn saturating_right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint {
*point.column_mut() += 1;
map.clip_point(point, Bias::Right)
}
pub fn up(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_start: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
up_by_rows(
map,
start,
1,
goal,
preserve_column_at_start,
text_layout_details,
)
}
pub fn down(
map: &DisplaySnapshot,
start: DisplayPoint,
goal: SelectionGoal,
preserve_column_at_end: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
down_by_rows(
map,
start,
1,
goal,
preserve_column_at_end,
text_layout_details,
)
}
pub fn up_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_start: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x,
SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
SelectionGoal::HorizontalRange { end, .. } => end,
_ => map.x_for_point(start, text_layout_details),
};
let prev_row = start.row().saturating_sub(row_count);
let mut point = map.clip_point(
DisplayPoint::new(prev_row, map.line_len(prev_row)),
Bias::Left,
);
if point.row() < start.row() {
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_start {
return (start, goal);
} else {
point = DisplayPoint::new(0, 0);
goal_x = 0.0;
}
let mut clipped_point = map.clip_point(point, Bias::Left);
if clipped_point.row() < point.row() {
clipped_point = map.clip_point(point, Bias::Right);
}
(clipped_point, SelectionGoal::HorizontalPosition(goal_x))
}
pub fn down_by_rows(
map: &DisplaySnapshot,
start: DisplayPoint,
row_count: u32,
goal: SelectionGoal,
preserve_column_at_end: bool,
text_layout_details: &TextLayoutDetails,
) -> (DisplayPoint, SelectionGoal) {
let mut goal_x = match goal {
SelectionGoal::HorizontalPosition(x) => x,
SelectionGoal::WrappedHorizontalPosition((_, x)) => x,
SelectionGoal::HorizontalRange { end, .. } => end,
_ => map.x_for_point(start, text_layout_details),
};
let new_row = start.row() + row_count;
let mut point = map.clip_point(DisplayPoint::new(new_row, 0), Bias::Right);
if point.row() > start.row() {
*point.column_mut() = map.column_for_x(point.row(), goal_x, text_layout_details)
} else if preserve_column_at_end {
return (start, goal);
} else {
point = map.max_point();
goal_x = map.x_for_point(point, text_layout_details)
}
let mut clipped_point = map.clip_point(point, Bias::Right);
if clipped_point.row() > point.row() {
clipped_point = map.clip_point(point, Bias::Left);
}
(clipped_point, SelectionGoal::HorizontalPosition(goal_x))
}
pub fn line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
let line_start = map.prev_line_boundary(point).1;
if stop_at_soft_boundaries && display_point != soft_line_start {
soft_line_start
} else {
line_start
}
}
pub fn indented_line_beginning(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let point = display_point.to_point(map);
let soft_line_start = map.clip_point(DisplayPoint::new(display_point.row(), 0), Bias::Right);
let indent_start = Point::new(
point.row,
map.buffer_snapshot.indent_size_for_line(point.row).len,
)
.to_display_point(map);
let line_start = map.prev_line_boundary(point).1;
if stop_at_soft_boundaries && soft_line_start > indent_start && display_point != soft_line_start
{
soft_line_start
} else if stop_at_soft_boundaries && display_point != indent_start {
indent_start
} else {
line_start
}
}
pub fn line_end(
map: &DisplaySnapshot,
display_point: DisplayPoint,
stop_at_soft_boundaries: bool,
) -> DisplayPoint {
let soft_line_end = map.clip_point(
DisplayPoint::new(display_point.row(), map.line_len(display_point.row())),
Bias::Left,
);
if stop_at_soft_boundaries && display_point != soft_line_end {
soft_line_end
} else {
map.next_line_boundary(display_point.to_point(map)).1
}
}
pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
(char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace())
|| left == '\n'
})
}
pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_preceding_boundary(map, point, FindRange::MultiLine, |left, right| {
let is_word_start =
char_kind(&scope, left) != char_kind(&scope, right) && !right.is_whitespace();
let is_subword_start =
left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
})
}
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
(char_kind(&scope, left) != char_kind(&scope, right) && !left.is_whitespace())
|| right == '\n'
})
}
pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
let is_word_end =
(char_kind(&scope, left) != char_kind(&scope, right)) && !left.is_whitespace();
let is_subword_end =
left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
})
}
pub fn start_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return DisplayPoint::zero();
}
let mut found_non_blank_line = false;
for row in (0..point.row + 1).rev() {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_blank_line = false;
}
found_non_blank_line |= !blank;
}
DisplayPoint::zero()
}
pub fn end_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.max_buffer_row() {
return map.max_point();
}
let mut found_non_blank_line = false;
for row in point.row..map.max_buffer_row() + 1 {
let blank = map.buffer_snapshot.is_line_blank(row);
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_blank_line = false;
}
found_non_blank_line |= !blank;
}
map.max_point()
}
/// Scans for a boundary preceding the given start point `from` until a boundary is found,
/// indicated by the given predicate returning true.
/// The predicate is called with the character to the left and right of the candidate boundary location.
/// If FindRange::SingleLine is specified and no boundary is found before the start of the current line, the start of the current line will be returned.
pub fn find_preceding_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,
find_range: FindRange,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut prev_ch = None;
let mut offset = from.to_point(map).to_offset(&map.buffer_snapshot);
for ch in map.buffer_snapshot.reversed_chars_at(offset) {
if find_range == FindRange::SingleLine && ch == '\n' {
break;
}
if let Some(prev_ch) = prev_ch {
if is_boundary(ch, prev_ch) {
break;
}
}
offset -= ch.len_utf8();
prev_ch = Some(ch);
}
map.clip_point(offset.to_display_point(map), Bias::Left)
}
/// Scans for a boundary following the given start point until a boundary is found, indicated by the
/// given predicate returning true. The predicate is called with the character to the left and right
/// of the candidate boundary location, and will be called with `\n` characters indicating the start
/// or end of a line.
pub fn find_boundary(
map: &DisplaySnapshot,
from: DisplayPoint,
find_range: FindRange,
mut is_boundary: impl FnMut(char, char) -> bool,
) -> DisplayPoint {
let mut offset = from.to_offset(&map, Bias::Right);
let mut prev_ch = None;
for ch in map.buffer_snapshot.chars_at(offset) {
if find_range == FindRange::SingleLine && ch == '\n' {
break;
}
if let Some(prev_ch) = prev_ch {
if is_boundary(prev_ch, ch) {
break;
}
}
offset += ch.len_utf8();
prev_ch = Some(ch);
}
map.clip_point(offset.to_display_point(map), Bias::Right)
}
pub fn chars_after(
map: &DisplaySnapshot,
mut offset: usize,
) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
map.buffer_snapshot.chars_at(offset).map(move |ch| {
let before = offset;
offset = offset + ch.len_utf8();
(ch, before..offset)
})
}
pub fn chars_before(
map: &DisplaySnapshot,
mut offset: usize,
) -> impl Iterator<Item = (char, Range<usize>)> + '_ {
map.buffer_snapshot
.reversed_chars_at(offset)
.map(move |ch| {
let after = offset;
offset = offset - ch.len_utf8();
(ch, offset..after)
})
}
pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool {
let raw_point = point.to_point(map);
let scope = map.buffer_snapshot.language_scope_at(raw_point);
let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left);
let text = &map.buffer_snapshot;
let next_char_kind = text.chars_at(ix).next().map(|c| char_kind(&scope, c));
let prev_char_kind = text
.reversed_chars_at(ix)
.next()
.map(|c| char_kind(&scope, c));
prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word))
}
pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range<DisplayPoint> {
let position = map
.clip_point(position, Bias::Left)
.to_offset(map, Bias::Left);
let (range, _) = map.buffer_snapshot.surrounding_word(position);
let start = range
.start
.to_point(&map.buffer_snapshot)
.to_display_point(map);
let end = range
.end
.to_point(&map.buffer_snapshot)
.to_display_point(map);
start..end
}
pub fn split_display_range_by_lines(
map: &DisplaySnapshot,
range: Range<DisplayPoint>,
) -> Vec<Range<DisplayPoint>> {
let mut result = Vec::new();
let mut start = range.start;
// Loop over all the covered rows until the one containing the range end
for row in range.start.row()..range.end.row() {
let row_end_column = map.line_len(row);
let end = map.clip_point(DisplayPoint::new(row, row_end_column), Bias::Left);
if start != end {
result.push(start..end);
}
start = map.clip_point(DisplayPoint::new(row + 1, 0), Bias::Left);
}
// Add the final range from the start of the last end to the original range end.
result.push(start..range.end);
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
display_map::Inlay,
test::{editor_test_context::EditorTestContext, marked_display_snapshot},
Buffer, DisplayMap, ExcerptRange, InlayId, MultiBuffer,
};
use project::Project;
use settings::SettingsStore;
use util::post_inc;
#[gpui::test]
fn test_previous_word_start(cx: &mut gpui::AppContext) {
init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
previous_word_start(&snapshot, display_points[1]),
display_points[0]
);
}
assert("\nˇ ˇlorem", cx);
assert("ˇ\nˇ lorem", cx);
assert(" ˇloremˇ", cx);
assert("ˇ ˇlorem", cx);
assert(" ˇlorˇem", cx);
assert("\nlorem\nˇ ˇipsum", cx);
assert("\n\nˇ\nˇ", cx);
assert(" ˇlorem ˇipsum", cx);
assert("loremˇ-ˇipsum", cx);
assert("loremˇ-#$@ˇipsum", cx);
assert("ˇlorem_ˇipsum", cx);
assert(" ˇdefγˇ", cx);
assert(" ˇbcΔˇ", cx);
assert(" abˇ——ˇcd", cx);
}
#[gpui::test]
fn test_previous_subword_start(cx: &mut gpui::AppContext) {
init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
previous_subword_start(&snapshot, display_points[1]),
display_points[0]
);
}
// Subword boundaries are respected
assert("lorem_ˇipˇsum", cx);
assert("lorem_ˇipsumˇ", cx);
assert("ˇlorem_ˇipsum", cx);
assert("lorem_ˇipsum_ˇdolor", cx);
assert("loremˇIpˇsum", cx);
assert("loremˇIpsumˇ", cx);
// Word boundaries are still respected
assert("\nˇ ˇlorem", cx);
assert(" ˇloremˇ", cx);
assert(" ˇlorˇem", cx);
assert("\nlorem\nˇ ˇipsum", cx);
assert("\n\nˇ\nˇ", cx);
assert(" ˇlorem ˇipsum", cx);
assert("loremˇ-ˇipsum", cx);
assert("loremˇ-#$@ˇipsum", cx);
assert(" ˇdefγˇ", cx);
assert(" bcˇΔˇ", cx);
assert(" ˇbcδˇ", cx);
assert(" abˇ——ˇcd", cx);
}
#[gpui::test]
fn test_find_preceding_boundary(cx: &mut gpui::AppContext) {
init_test(cx);
fn assert(
marked_text: &str,
cx: &mut gpui::AppContext,
is_boundary: impl FnMut(char, char) -> bool,
) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
find_preceding_boundary(
&snapshot,
display_points[1],
FindRange::MultiLine,
is_boundary
),
display_points[0]
);
}
assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
left == 'c' && right == 'd'
});
assert("abcdef\nˇgh\nijˇk", cx, |left, right| {
left == '\n' && right == 'g'
});
let mut line_count = 0;
assert("abcdef\nˇgh\nijˇk", cx, |left, _| {
if left == '\n' {
line_count += 1;
line_count == 2
} else {
false
}
});
}
#[gpui::test]
fn test_find_preceding_boundary_with_inlays(cx: &mut gpui::AppContext) {
init_test(cx);
let input_text = "abcdefghijklmnopqrstuvwxys";
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple(input_text, cx);
let buffer_snapshot = buffer.read(cx).snapshot(cx);
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
// add all kinds of inlays between two word boundaries: we should be able to cross them all, when looking for another boundary
let mut id = 0;
let inlays = (0..buffer_snapshot.len())
.map(|offset| {
[
Inlay {
id: InlayId::Suggestion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Left),
text: format!("test").into(),
},
Inlay {
id: InlayId::Suggestion(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Right),
text: format!("test").into(),
},
Inlay {
id: InlayId::Hint(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Left),
text: format!("test").into(),
},
Inlay {
id: InlayId::Hint(post_inc(&mut id)),
position: buffer_snapshot.anchor_at(offset, Bias::Right),
text: format!("test").into(),
},
]
})
.flatten()
.collect();
let snapshot = display_map.update(cx, |map, cx| {
map.splice_inlays(Vec::new(), inlays, cx);
map.snapshot(cx)
});
assert_eq!(
find_preceding_boundary(
&snapshot,
buffer_snapshot.len().to_display_point(&snapshot),
FindRange::MultiLine,
|left, _| left == 'e',
),
snapshot
.buffer_snapshot
.offset_to_point(5)
.to_display_point(&snapshot),
"Should not stop at inlays when looking for boundaries"
);
}
#[gpui::test]
fn test_next_word_end(cx: &mut gpui::AppContext) {
init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
next_word_end(&snapshot, display_points[0]),
display_points[1]
);
}
assert("\nˇ loremˇ", cx);
assert(" ˇloremˇ", cx);
assert(" lorˇemˇ", cx);
assert(" loremˇ ˇ\nipsum\n", cx);
assert("\nˇ\nˇ\n\n", cx);
assert("loremˇ ipsumˇ ", cx);
assert("loremˇ-ˇipsum", cx);
assert("loremˇ#$@-ˇipsum", cx);
assert("loremˇ_ipsumˇ", cx);
assert(" ˇbcΔˇ", cx);
assert(" abˇ——ˇcd", cx);
}
#[gpui::test]
fn test_next_subword_end(cx: &mut gpui::AppContext) {
init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
next_subword_end(&snapshot, display_points[0]),
display_points[1]
);
}
// Subword boundaries are respected
assert("loˇremˇ_ipsum", cx);
assert("ˇloremˇ_ipsum", cx);
assert("loremˇ_ipsumˇ", cx);
assert("loremˇ_ipsumˇ_dolor", cx);
assert("loˇremˇIpsum", cx);
assert("loremˇIpsumˇDolor", cx);
// Word boundaries are still respected
assert("\nˇ loremˇ", cx);
assert(" ˇloremˇ", cx);
assert(" lorˇemˇ", cx);
assert(" loremˇ ˇ\nipsum\n", cx);
assert("\nˇ\nˇ\n\n", cx);
assert("loremˇ ipsumˇ ", cx);
assert("loremˇ-ˇipsum", cx);
assert("loremˇ#$@-ˇipsum", cx);
assert("loremˇ_ipsumˇ", cx);
assert(" ˇbcˇΔ", cx);
assert(" abˇ——ˇcd", cx);
}
#[gpui::test]
fn test_find_boundary(cx: &mut gpui::AppContext) {
init_test(cx);
fn assert(
marked_text: &str,
cx: &mut gpui::AppContext,
is_boundary: impl FnMut(char, char) -> bool,
) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
find_boundary(
&snapshot,
display_points[0],
FindRange::MultiLine,
is_boundary
),
display_points[1]
);
}
assert("abcˇdef\ngh\nijˇk", cx, |left, right| {
left == 'j' && right == 'k'
});
assert("abˇcdef\ngh\nˇijk", cx, |left, right| {
left == '\n' && right == 'i'
});
let mut line_count = 0;
assert("abcˇdef\ngh\nˇijk", cx, |left, _| {
if left == '\n' {
line_count += 1;
line_count == 2
} else {
false
}
});
}
#[gpui::test]
fn test_surrounding_word(cx: &mut gpui::AppContext) {
init_test(cx);
fn assert(marked_text: &str, cx: &mut gpui::AppContext) {
let (snapshot, display_points) = marked_display_snapshot(marked_text, cx);
assert_eq!(
surrounding_word(&snapshot, display_points[1]),
display_points[0]..display_points[2],
"{}",
marked_text.to_string()
);
}
assert("ˇˇloremˇ ipsum", cx);
assert("ˇloˇremˇ ipsum", cx);
assert("ˇloremˇˇ ipsum", cx);
assert("loremˇ ˇ ˇipsum", cx);
assert("lorem\nˇˇˇ\nipsum", cx);
assert("lorem\nˇˇipsumˇ", cx);
assert("loremˇ,ˇˇ ipsum", cx);
assert("ˇloremˇˇ, ipsum", cx);
}
#[gpui::test]
async fn test_move_up_and_down_with_excerpts(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
init_test(cx);
});
let mut cx = EditorTestContext::new(cx).await;
let editor = cx.editor.clone();
let window = cx.window.clone();
cx.update_window(window, |cx| {
let text_layout_details =
editor.read_with(cx, |editor, cx| editor.text_layout_details(cx));
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let buffer =
cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn"));
let multibuffer = cx.add_model(|cx| {
let mut multibuffer = MultiBuffer::new(0);
multibuffer.push_excerpts(
buffer.clone(),
[
ExcerptRange {
context: Point::new(0, 0)..Point::new(1, 4),
primary: None,
},
ExcerptRange {
context: Point::new(2, 0)..Point::new(3, 2),
primary: None,
},
],
cx,
);
multibuffer
});
let display_map =
cx.add_model(|cx| DisplayMap::new(multibuffer, font_id, 14.0, None, 2, 2, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn");
let col_2_x = snapshot.x_for_point(DisplayPoint::new(2, 2), &text_layout_details);
// Can't move up into the first excerpt's header
assert_eq!(
up(
&snapshot,
DisplayPoint::new(2, 2),
SelectionGoal::HorizontalPosition(col_2_x),
false,
&text_layout_details
),
(
DisplayPoint::new(2, 0),
SelectionGoal::HorizontalPosition(0.0)
),
);
assert_eq!(
up(
&snapshot,
DisplayPoint::new(2, 0),
SelectionGoal::None,
false,
&text_layout_details
),
(
DisplayPoint::new(2, 0),
SelectionGoal::HorizontalPosition(0.0)
),
);
let col_4_x = snapshot.x_for_point(DisplayPoint::new(3, 4), &text_layout_details);
// Move up and down within first excerpt
assert_eq!(
up(
&snapshot,
DisplayPoint::new(3, 4),
SelectionGoal::HorizontalPosition(col_4_x),
false,
&text_layout_details
),
(
DisplayPoint::new(2, 3),
SelectionGoal::HorizontalPosition(col_4_x)
),
);
assert_eq!(
down(
&snapshot,
DisplayPoint::new(2, 3),
SelectionGoal::HorizontalPosition(col_4_x),
false,
&text_layout_details
),
(
DisplayPoint::new(3, 4),
SelectionGoal::HorizontalPosition(col_4_x)
),
);
let col_5_x = snapshot.x_for_point(DisplayPoint::new(6, 5), &text_layout_details);
// Move up and down across second excerpt's header
assert_eq!(
up(
&snapshot,
DisplayPoint::new(6, 5),
SelectionGoal::HorizontalPosition(col_5_x),
false,
&text_layout_details
),
(
DisplayPoint::new(3, 4),
SelectionGoal::HorizontalPosition(col_5_x)
),
);
assert_eq!(
down(
&snapshot,
DisplayPoint::new(3, 4),
SelectionGoal::HorizontalPosition(col_5_x),
false,
&text_layout_details
),
(
DisplayPoint::new(6, 5),
SelectionGoal::HorizontalPosition(col_5_x)
),
);
let max_point_x = snapshot.x_for_point(DisplayPoint::new(7, 2), &text_layout_details);
// Can't move down off the end
assert_eq!(
down(
&snapshot,
DisplayPoint::new(7, 0),
SelectionGoal::HorizontalPosition(0.0),
false,
&text_layout_details
),
(
DisplayPoint::new(7, 2),
SelectionGoal::HorizontalPosition(max_point_x)
),
);
assert_eq!(
down(
&snapshot,
DisplayPoint::new(7, 2),
SelectionGoal::HorizontalPosition(max_point_x),
false,
&text_layout_details
),
(
DisplayPoint::new(7, 2),
SelectionGoal::HorizontalPosition(max_point_x)
),
);
});
}
fn init_test(cx: &mut gpui::AppContext) {
cx.set_global(SettingsStore::test(cx));
theme::init((), cx);
language::init(cx);
crate::init(cx);
Project::init_settings(cx);
}
}

View file

@ -0,0 +1,83 @@
use std::path::PathBuf;
use db::sqlez_macros::sql;
use db::{define_connection, query};
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
define_connection!(
// Current schema shape using pseudo-rust syntax:
// editors(
// item_id: usize,
// workspace_id: usize,
// path: PathBuf,
// scroll_top_row: usize,
// scroll_vertical_offset: f32,
// scroll_horizontal_offset: f32,
// )
pub static ref DB: EditorDb<WorkspaceDb> =
&[sql! (
CREATE TABLE editors(
item_id INTEGER NOT NULL,
workspace_id INTEGER NOT NULL,
path BLOB NOT NULL,
PRIMARY KEY(item_id, workspace_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
ON UPDATE CASCADE
) STRICT;
),
sql! (
ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
)];
);
impl EditorDb {
query! {
pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
SELECT path FROM editors
WHERE item_id = ? AND workspace_id = ?
}
}
query! {
pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
INSERT INTO editors
(item_id, workspace_id, path)
VALUES
(?1, ?2, ?3)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2,
path = ?3
}
}
// Returns the scroll top row, and offset
query! {
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
FROM editors
WHERE item_id = ? AND workspace_id = ?
}
}
query! {
pub async fn save_scroll_position(
item_id: ItemId,
workspace_id: WorkspaceId,
top_row: u32,
vertical_offset: f32,
horizontal_offset: f32
) -> Result<()> {
UPDATE OR IGNORE editors
SET
scroll_top_row = ?3,
scroll_horizontal_offset = ?4,
scroll_vertical_offset = ?5
WHERE item_id = ?1 AND workspace_id = ?2
}
}
}

View file

@ -0,0 +1,436 @@
pub mod actions;
pub mod autoscroll;
pub mod scroll_amount;
use std::{
cmp::Ordering,
time::{Duration, Instant},
};
use gpui::{
geometry::vector::{vec2f, Vector2F},
AppContext, Axis, Task, ViewContext,
};
use language::{Bias, Point};
use util::ResultExt;
use workspace::WorkspaceId;
use crate::{
display_map::{DisplaySnapshot, ToDisplayPoint},
hover_popover::hide_hover,
persistence::DB,
Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot,
ToPoint,
};
use self::{
autoscroll::{Autoscroll, AutoscrollStrategy},
scroll_amount::ScrollAmount,
};
pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
pub const VERTICAL_SCROLL_MARGIN: f32 = 3.;
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Default)]
pub struct ScrollbarAutoHide(pub bool);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollAnchor {
pub offset: Vector2F,
pub anchor: Anchor,
}
impl ScrollAnchor {
fn new() -> Self {
Self {
offset: Vector2F::zero(),
anchor: Anchor::min(),
}
}
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
let mut scroll_position = self.offset;
if self.anchor != Anchor::min() {
let scroll_top = self.anchor.to_display_point(snapshot).row() as f32;
scroll_position.set_y(scroll_top + scroll_position.y());
} else {
scroll_position.set_y(0.);
}
scroll_position
}
pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
self.anchor.to_point(buffer).row
}
}
#[derive(Clone, Copy, Debug)]
pub struct OngoingScroll {
last_event: Instant,
axis: Option<Axis>,
}
impl OngoingScroll {
fn new() -> Self {
Self {
last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
axis: None,
}
}
pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
const UNLOCK_PERCENT: f32 = 1.9;
const UNLOCK_LOWER_BOUND: f32 = 6.;
let mut axis = self.axis;
let x = delta.x().abs();
let y = delta.y().abs();
let duration = Instant::now().duration_since(self.last_event);
if duration > SCROLL_EVENT_SEPARATION {
//New ongoing scroll will start, determine axis
axis = if x <= y {
Some(Axis::Vertical)
} else {
Some(Axis::Horizontal)
};
} else if x.max(y) >= UNLOCK_LOWER_BOUND {
//Check if the current ongoing will need to unlock
match axis {
Some(Axis::Vertical) => {
if x > y && x >= y * UNLOCK_PERCENT {
axis = None;
}
}
Some(Axis::Horizontal) => {
if y > x && y >= x * UNLOCK_PERCENT {
axis = None;
}
}
None => {}
}
}
match axis {
Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
None => {}
}
axis
}
}
pub struct ScrollManager {
vertical_scroll_margin: f32,
anchor: ScrollAnchor,
ongoing: OngoingScroll,
autoscroll_request: Option<(Autoscroll, bool)>,
last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
show_scrollbars: bool,
hide_scrollbar_task: Option<Task<()>>,
visible_line_count: Option<f32>,
}
impl ScrollManager {
pub fn new() -> Self {
ScrollManager {
vertical_scroll_margin: VERTICAL_SCROLL_MARGIN,
anchor: ScrollAnchor::new(),
ongoing: OngoingScroll::new(),
autoscroll_request: None,
show_scrollbars: true,
hide_scrollbar_task: None,
last_autoscroll: None,
visible_line_count: None,
}
}
pub fn clone_state(&mut self, other: &Self) {
self.anchor = other.anchor;
self.ongoing = other.ongoing;
}
pub fn anchor(&self) -> ScrollAnchor {
self.anchor
}
pub fn ongoing_scroll(&self) -> OngoingScroll {
self.ongoing
}
pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
self.ongoing.last_event = Instant::now();
self.ongoing.axis = axis;
}
pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
self.anchor.scroll_position(snapshot)
}
fn set_scroll_position(
&mut self,
scroll_position: Vector2F,
map: &DisplaySnapshot,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
let (new_anchor, top_row) = if scroll_position.y() <= 0. {
(
ScrollAnchor {
anchor: Anchor::min(),
offset: scroll_position.max(vec2f(0., 0.)),
},
0,
)
} else {
let scroll_top_buffer_point =
DisplayPoint::new(scroll_position.y() as u32, 0).to_point(&map);
let top_anchor = map
.buffer_snapshot
.anchor_at(scroll_top_buffer_point, Bias::Right);
(
ScrollAnchor {
anchor: top_anchor,
offset: vec2f(
scroll_position.x(),
scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
),
},
scroll_top_buffer_point.row,
)
};
self.set_anchor(new_anchor, top_row, local, autoscroll, workspace_id, cx);
}
fn set_anchor(
&mut self,
anchor: ScrollAnchor,
top_row: u32,
local: bool,
autoscroll: bool,
workspace_id: Option<i64>,
cx: &mut ViewContext<Editor>,
) {
self.anchor = anchor;
cx.emit(Event::ScrollPositionChanged { local, autoscroll });
self.show_scrollbar(cx);
self.autoscroll_request.take();
if let Some(workspace_id) = workspace_id {
let item_id = cx.view_id();
cx.background()
.spawn(async move {
DB.save_scroll_position(
item_id,
workspace_id,
top_row,
anchor.offset.x(),
anchor.offset.y(),
)
.await
.log_err()
})
.detach()
}
cx.notify();
}
pub fn show_scrollbar(&mut self, cx: &mut ViewContext<Editor>) {
if !self.show_scrollbars {
self.show_scrollbars = true;
cx.notify();
}
if cx.default_global::<ScrollbarAutoHide>().0 {
self.hide_scrollbar_task = Some(cx.spawn(|editor, mut cx| async move {
cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await;
editor
.update(&mut cx, |editor, cx| {
editor.scroll_manager.show_scrollbars = false;
cx.notify();
})
.log_err();
}));
} else {
self.hide_scrollbar_task = None;
}
}
pub fn scrollbars_visible(&self) -> bool {
self.show_scrollbars
}
pub fn has_autoscroll_request(&self) -> bool {
self.autoscroll_request.is_some()
}
pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
if max < self.anchor.offset.x() {
self.anchor.offset.set_x(max);
true
} else {
false
}
}
}
impl Editor {
pub fn vertical_scroll_margin(&mut self) -> usize {
self.scroll_manager.vertical_scroll_margin as usize
}
pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
cx.notify();
}
pub fn visible_line_count(&self) -> Option<f32> {
self.scroll_manager.visible_line_count
}
pub(crate) fn set_visible_line_count(&mut self, lines: f32, cx: &mut ViewContext<Self>) {
let opened_first_time = self.scroll_manager.visible_line_count.is_none();
self.scroll_manager.visible_line_count = Some(lines);
if opened_first_time {
cx.spawn(|editor, mut cx| async move {
editor
.update(&mut cx, |editor, cx| {
editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
})
.ok()
})
.detach()
}
}
pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
self.set_scroll_position_internal(scroll_position, true, false, cx);
}
pub(crate) fn set_scroll_position_internal(
&mut self,
scroll_position: Vector2F,
local: bool,
autoscroll: bool,
cx: &mut ViewContext<Self>,
) {
let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
self.scroll_manager.set_scroll_position(
scroll_position,
&map,
local,
autoscroll,
workspace_id,
cx,
);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
}
pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
self.scroll_manager.anchor.scroll_position(&display_map)
}
pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, true, false, workspace_id, cx);
}
pub(crate) fn set_scroll_anchor_remote(
&mut self,
scroll_anchor: ScrollAnchor,
cx: &mut ViewContext<Self>,
) {
hide_hover(self, cx);
let workspace_id = self.workspace.as_ref().map(|workspace| workspace.1);
let top_row = scroll_anchor
.anchor
.to_point(&self.buffer().read(cx).snapshot(cx))
.row;
self.scroll_manager
.set_anchor(scroll_anchor, top_row, false, false, workspace_id, cx);
}
pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return;
}
if self.take_rename(true, cx).is_some() {
return;
}
let cur_position = self.scroll_position(cx);
let new_pos = cur_position + vec2f(0., amount.lines(self));
self.set_scroll_position(new_pos, cx);
}
/// Returns an ordering. The newest selection is:
/// Ordering::Equal => on screen
/// Ordering::Less => above the screen
/// Ordering::Greater => below the screen
pub fn newest_selection_on_screen(&self, cx: &mut AppContext) -> Ordering {
let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let newest_head = self
.selections
.newest_anchor()
.head()
.to_display_point(&snapshot);
let screen_top = self
.scroll_manager
.anchor
.anchor
.to_display_point(&snapshot);
if screen_top > newest_head {
return Ordering::Less;
}
if let Some(visible_lines) = self.visible_line_count() {
if newest_head.row() < screen_top.row() + visible_lines as u32 {
return Ordering::Equal;
}
}
Ordering::Greater
}
pub fn read_scroll_position_from_db(
&mut self,
item_id: usize,
workspace_id: WorkspaceId,
cx: &mut ViewContext<Editor>,
) {
let scroll_position = DB.get_scroll_position(item_id, workspace_id);
if let Ok(Some((top_row, x, y))) = scroll_position {
let top_anchor = self
.buffer()
.read(cx)
.snapshot(cx)
.anchor_at(Point::new(top_row as u32, 0), Bias::Left);
let scroll_anchor = ScrollAnchor {
offset: Vector2F::new(x, y),
anchor: top_anchor,
};
self.set_scroll_anchor(scroll_anchor, cx);
}
}
}

View file

@ -0,0 +1,152 @@
use gpui::{actions, geometry::vector::Vector2F, AppContext, Axis, ViewContext};
use language::Bias;
use crate::{Editor, EditorMode};
use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor};
actions!(
editor,
[
LineDown,
LineUp,
HalfPageDown,
HalfPageUp,
PageDown,
PageUp,
NextScreen,
ScrollCursorTop,
ScrollCursorCenter,
ScrollCursorBottom,
]
);
pub fn init(cx: &mut AppContext) {
cx.add_action(Editor::next_screen);
cx.add_action(Editor::scroll_cursor_top);
cx.add_action(Editor::scroll_cursor_center);
cx.add_action(Editor::scroll_cursor_bottom);
cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
this.scroll_screen(&ScrollAmount::Line(1.), cx)
});
cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
this.scroll_screen(&ScrollAmount::Line(-1.), cx)
});
cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
this.scroll_screen(&ScrollAmount::Page(0.5), cx)
});
cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
this.scroll_screen(&ScrollAmount::Page(-0.5), cx)
});
cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
this.scroll_screen(&ScrollAmount::Page(1.), cx)
});
cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
this.scroll_screen(&ScrollAmount::Page(-1.), cx)
});
}
impl Editor {
pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) -> Option<()> {
if self.take_rename(true, cx).is_some() {
return None;
}
if self.mouse_context_menu.read(cx).visible() {
return None;
}
if matches!(self.mode, EditorMode::SingleLine) {
cx.propagate_action();
return None;
}
self.request_autoscroll(Autoscroll::Next, cx);
Some(())
}
pub fn scroll(
&mut self,
scroll_position: Vector2F,
axis: Option<Axis>,
cx: &mut ViewContext<Self>,
) {
self.scroll_manager.update_ongoing_scroll(axis);
self.set_scroll_position(scroll_position, cx);
}
fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
let snapshot = editor.snapshot(cx).display_snapshot;
let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
let mut new_screen_top = editor.selections.newest_display(cx).head();
*new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows);
*new_screen_top.column_mut() = 0;
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
editor.set_scroll_anchor(
ScrollAnchor {
anchor: new_anchor,
offset: Default::default(),
},
cx,
)
}
fn scroll_cursor_center(
editor: &mut Editor,
_: &ScrollCursorCenter,
cx: &mut ViewContext<Editor>,
) {
let snapshot = editor.snapshot(cx).display_snapshot;
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
visible_rows as u32
} else {
return;
};
let mut new_screen_top = editor.selections.newest_display(cx).head();
*new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2);
*new_screen_top.column_mut() = 0;
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
editor.set_scroll_anchor(
ScrollAnchor {
anchor: new_anchor,
offset: Default::default(),
},
cx,
)
}
fn scroll_cursor_bottom(
editor: &mut Editor,
_: &ScrollCursorBottom,
cx: &mut ViewContext<Editor>,
) {
let snapshot = editor.snapshot(cx).display_snapshot;
let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
visible_rows as u32
} else {
return;
};
let mut new_screen_top = editor.selections.newest_display(cx).head();
*new_screen_top.row_mut() = new_screen_top
.row()
.saturating_sub(visible_rows.saturating_sub(scroll_margin_rows));
*new_screen_top.column_mut() = 0;
let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
editor.set_scroll_anchor(
ScrollAnchor {
anchor: new_anchor,
offset: Default::default(),
},
cx,
)
}
}

View file

@ -0,0 +1,258 @@
use std::cmp;
use gpui::ViewContext;
use language::Point;
use crate::{display_map::ToDisplayPoint, Editor, EditorMode, LineWithInvisibles};
#[derive(PartialEq, Eq)]
pub enum Autoscroll {
Next,
Strategy(AutoscrollStrategy),
}
impl Autoscroll {
pub fn fit() -> Self {
Self::Strategy(AutoscrollStrategy::Fit)
}
pub fn newest() -> Self {
Self::Strategy(AutoscrollStrategy::Newest)
}
pub fn center() -> Self {
Self::Strategy(AutoscrollStrategy::Center)
}
}
#[derive(PartialEq, Eq, Default)]
pub enum AutoscrollStrategy {
Fit,
Newest,
#[default]
Center,
Top,
Bottom,
}
impl AutoscrollStrategy {
fn next(&self) -> Self {
match self {
AutoscrollStrategy::Center => AutoscrollStrategy::Top,
AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
_ => AutoscrollStrategy::Center,
}
}
}
impl Editor {
pub fn autoscroll_vertically(
&mut self,
viewport_height: f32,
line_height: f32,
cx: &mut ViewContext<Editor>,
) -> bool {
let visible_lines = viewport_height / line_height;
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
(display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
} else {
display_map.max_point().row() as f32
};
if scroll_position.y() > max_scroll_top {
scroll_position.set_y(max_scroll_top);
self.set_scroll_position(scroll_position, cx);
}
let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else {
return false;
};
let mut target_top;
let mut target_bottom;
if let Some(highlighted_rows) = &self.highlighted_rows {
target_top = highlighted_rows.start as f32;
target_bottom = target_top + 1.;
} else {
let selections = self.selections.all::<Point>(cx);
target_top = selections
.first()
.unwrap()
.head()
.to_display_point(&display_map)
.row() as f32;
target_bottom = selections
.last()
.unwrap()
.head()
.to_display_point(&display_map)
.row() as f32
+ 1.0;
// If the selections can't all fit on screen, scroll to the newest.
if autoscroll == Autoscroll::newest()
|| autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines
{
let newest_selection_top = selections
.iter()
.max_by_key(|s| s.id)
.unwrap()
.head()
.to_display_point(&display_map)
.row() as f32;
target_top = newest_selection_top;
target_bottom = newest_selection_top + 1.;
}
}
let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
0.
} else {
((visible_lines - (target_bottom - target_top)) / 2.0).floor()
};
let strategy = match autoscroll {
Autoscroll::Strategy(strategy) => strategy,
Autoscroll::Next => {
let last_autoscroll = &self.scroll_manager.last_autoscroll;
if let Some(last_autoscroll) = last_autoscroll {
if self.scroll_manager.anchor.offset == last_autoscroll.0
&& target_top == last_autoscroll.1
&& target_bottom == last_autoscroll.2
{
last_autoscroll.3.next()
} else {
AutoscrollStrategy::default()
}
} else {
AutoscrollStrategy::default()
}
}
};
match strategy {
AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
let target_top = (target_top - margin).max(0.0);
let target_bottom = target_bottom + margin;
let start_row = scroll_position.y();
let end_row = start_row + visible_lines;
let needs_scroll_up = target_top < start_row;
let needs_scroll_down = target_bottom >= end_row;
if needs_scroll_up && !needs_scroll_down {
scroll_position.set_y(target_top);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
if !needs_scroll_up && needs_scroll_down {
scroll_position.set_y(target_bottom - visible_lines);
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}
AutoscrollStrategy::Center => {
scroll_position.set_y((target_top - margin).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Top => {
scroll_position.set_y((target_top).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
AutoscrollStrategy::Bottom => {
scroll_position.set_y((target_bottom - visible_lines).max(0.0));
self.set_scroll_position_internal(scroll_position, local, true, cx);
}
}
self.scroll_manager.last_autoscroll = Some((
self.scroll_manager.anchor.offset,
target_top,
target_bottom,
strategy,
));
true
}
pub fn autoscroll_horizontally(
&mut self,
start_row: u32,
viewport_width: f32,
scroll_width: f32,
max_glyph_width: f32,
layouts: &[LineWithInvisibles],
cx: &mut ViewContext<Self>,
) -> bool {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
let mut target_left;
let mut target_right;
if self.highlighted_rows.is_some() {
target_left = 0.0_f32;
target_right = 0.0_f32;
} else {
target_left = std::f32::INFINITY;
target_right = 0.0_f32;
for selection in selections {
let head = selection.head().to_display_point(&display_map);
if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
let start_column = head.column().saturating_sub(3);
let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
target_left = target_left.min(
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(start_column as usize),
);
target_right = target_right.max(
layouts[(head.row() - start_row) as usize]
.line
.x_for_index(end_column as usize)
+ max_glyph_width,
);
}
}
}
target_right = target_right.min(scroll_width);
if target_right - target_left > viewport_width {
return false;
}
let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width;
let scroll_right = scroll_left + viewport_width;
if target_left < scroll_left {
self.scroll_manager
.anchor
.offset
.set_x(target_left / max_glyph_width);
true
} else if target_right > scroll_right {
self.scroll_manager
.anchor
.offset
.set_x((target_right - viewport_width) / max_glyph_width);
true
} else {
false
}
}
pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
self.scroll_manager.autoscroll_request = Some((autoscroll, true));
cx.notify();
}
pub(crate) fn request_autoscroll_remotely(
&mut self,
autoscroll: Autoscroll,
cx: &mut ViewContext<Self>,
) {
self.scroll_manager.autoscroll_request = Some((autoscroll, false));
cx.notify();
}
}

View file

@ -0,0 +1,28 @@
use crate::Editor;
use serde::Deserialize;
#[derive(Clone, PartialEq, Deserialize)]
pub enum ScrollAmount {
// Scroll N lines (positive is towards the end of the document)
Line(f32),
// Scroll N pages (positive is towards the end of the document)
Page(f32),
}
impl ScrollAmount {
pub fn lines(&self, editor: &mut Editor) -> f32 {
match self {
Self::Line(count) => *count,
Self::Page(count) => editor
.visible_line_count()
.map(|mut l| {
// for full pages subtract one to leave an anchor line
if count.abs() == 1.0 {
l -= 1.0
}
(l * count).trunc()
})
.unwrap_or(0.),
}
}
}

View file

@ -0,0 +1,886 @@
use std::{
cell::Ref,
iter, mem,
ops::{Deref, DerefMut, Range, Sub},
sync::Arc,
};
use collections::HashMap;
use gpui::{AppContext, ModelHandle};
use itertools::Itertools;
use language::{Bias, Point, Selection, SelectionGoal, TextDimension, ToPoint};
use util::post_inc;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
Anchor, DisplayPoint, ExcerptId, MultiBuffer, MultiBufferSnapshot, SelectMode, ToOffset,
};
#[derive(Debug, Clone)]
pub struct PendingSelection {
pub selection: Selection<Anchor>,
pub mode: SelectMode,
}
#[derive(Debug, Clone)]
pub struct SelectionsCollection {
display_map: ModelHandle<DisplayMap>,
buffer: ModelHandle<MultiBuffer>,
pub next_selection_id: usize,
pub line_mode: bool,
disjoint: Arc<[Selection<Anchor>]>,
pending: Option<PendingSelection>,
}
impl SelectionsCollection {
pub fn new(display_map: ModelHandle<DisplayMap>, buffer: ModelHandle<MultiBuffer>) -> Self {
Self {
display_map,
buffer,
next_selection_id: 1,
line_mode: false,
disjoint: Arc::from([]),
pending: Some(PendingSelection {
selection: Selection {
id: 0,
start: Anchor::min(),
end: Anchor::min(),
reversed: false,
goal: SelectionGoal::None,
},
mode: SelectMode::Character,
}),
}
}
pub fn display_map(&self, cx: &mut AppContext) -> DisplaySnapshot {
self.display_map.update(cx, |map, cx| map.snapshot(cx))
}
fn buffer<'a>(&self, cx: &'a AppContext) -> Ref<'a, MultiBufferSnapshot> {
self.buffer.read(cx).read(cx)
}
pub fn clone_state(&mut self, other: &SelectionsCollection) {
self.next_selection_id = other.next_selection_id;
self.line_mode = other.line_mode;
self.disjoint = other.disjoint.clone();
self.pending = other.pending.clone();
}
pub fn count(&self) -> usize {
let mut count = self.disjoint.len();
if self.pending.is_some() {
count += 1;
}
count
}
/// The non-pending, non-overlapping selections. There could still be a pending
/// selection that overlaps these if the mouse is being dragged, etc. Returned as
/// selections over Anchors.
pub fn disjoint_anchors(&self) -> Arc<[Selection<Anchor>]> {
self.disjoint.clone()
}
pub fn pending_anchor(&self) -> Option<Selection<Anchor>> {
self.pending
.as_ref()
.map(|pending| pending.selection.clone())
}
pub fn pending<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Option<Selection<D>> {
self.pending_anchor()
.as_ref()
.map(|pending| pending.map(|p| p.summary::<D>(&self.buffer(cx))))
}
pub fn pending_mode(&self) -> Option<SelectMode> {
self.pending.as_ref().map(|pending| pending.mode.clone())
}
pub fn all<'a, D>(&self, cx: &AppContext) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
{
let disjoint_anchors = &self.disjoint;
let mut disjoint =
resolve_multiple::<D, _>(disjoint_anchors.iter(), &self.buffer(cx)).peekable();
let mut pending_opt = self.pending::<D>(cx);
iter::from_fn(move || {
if let Some(pending) = pending_opt.as_mut() {
while let Some(next_selection) = disjoint.peek() {
if pending.start <= next_selection.end && pending.end >= next_selection.start {
let next_selection = disjoint.next().unwrap();
if next_selection.start < pending.start {
pending.start = next_selection.start;
}
if next_selection.end > pending.end {
pending.end = next_selection.end;
}
} else if next_selection.end < pending.start {
return disjoint.next();
} else {
break;
}
}
pending_opt.take()
} else {
disjoint.next()
}
})
.collect()
}
/// Returns all of the selections, adjusted to take into account the selection line_mode
pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec<Selection<Point>> {
let mut selections = self.all::<Point>(cx);
if self.line_mode {
let map = self.display_map(cx);
for selection in &mut selections {
let new_range = map.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
}
}
selections
}
pub fn all_adjusted_display(
&self,
cx: &mut AppContext,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
if self.line_mode {
let selections = self.all::<Point>(cx);
let map = self.display_map(cx);
let result = selections
.into_iter()
.map(|mut selection| {
let new_range = map.expand_to_line(selection.range());
selection.start = new_range.start;
selection.end = new_range.end;
selection.map(|point| point.to_display_point(&map))
})
.collect();
(map, result)
} else {
self.all_display(cx)
}
}
pub fn disjoint_in_range<'a, D>(
&self,
range: Range<Anchor>,
cx: &AppContext,
) -> Vec<Selection<D>>
where
D: 'a + TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
{
let buffer = self.buffer(cx);
let start_ix = match self
.disjoint
.binary_search_by(|probe| probe.end.cmp(&range.start, &buffer))
{
Ok(ix) | Err(ix) => ix,
};
let end_ix = match self
.disjoint
.binary_search_by(|probe| probe.start.cmp(&range.end, &buffer))
{
Ok(ix) => ix + 1,
Err(ix) => ix,
};
resolve_multiple(&self.disjoint[start_ix..end_ix], &buffer).collect()
}
pub fn all_display(
&self,
cx: &mut AppContext,
) -> (DisplaySnapshot, Vec<Selection<DisplayPoint>>) {
let display_map = self.display_map(cx);
let selections = self
.all::<Point>(cx)
.into_iter()
.map(|selection| selection.map(|point| point.to_display_point(&display_map)))
.collect();
(display_map, selections)
}
pub fn newest_anchor(&self) -> &Selection<Anchor> {
self.pending
.as_ref()
.map(|s| &s.selection)
.or_else(|| self.disjoint.iter().max_by_key(|s| s.id))
.unwrap()
}
pub fn newest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
resolve(self.newest_anchor(), &self.buffer(cx))
}
pub fn newest_display(&self, cx: &mut AppContext) -> Selection<DisplayPoint> {
let display_map = self.display_map(cx);
let selection = self
.newest_anchor()
.map(|point| point.to_display_point(&display_map));
selection
}
pub fn oldest_anchor(&self) -> &Selection<Anchor> {
self.disjoint
.iter()
.min_by_key(|s| s.id)
.or_else(|| self.pending.as_ref().map(|p| &p.selection))
.unwrap()
}
pub fn oldest<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
resolve(self.oldest_anchor(), &self.buffer(cx))
}
pub fn first_anchor(&self) -> Selection<Anchor> {
self.disjoint[0].clone()
}
pub fn first<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
self.all(cx).first().unwrap().clone()
}
pub fn last<D: TextDimension + Ord + Sub<D, Output = D>>(
&self,
cx: &AppContext,
) -> Selection<D> {
self.all(cx).last().unwrap().clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn ranges<D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug>(
&self,
cx: &AppContext,
) -> Vec<Range<D>> {
self.all::<D>(cx)
.iter()
.map(|s| {
if s.reversed {
s.end.clone()..s.start.clone()
} else {
s.start.clone()..s.end.clone()
}
})
.collect()
}
#[cfg(any(test, feature = "test-support"))]
pub fn display_ranges(&self, cx: &mut AppContext) -> Vec<Range<DisplayPoint>> {
let display_map = self.display_map(cx);
self.disjoint_anchors()
.iter()
.chain(self.pending_anchor().as_ref())
.map(|s| {
if s.reversed {
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
} else {
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
}
})
.collect()
}
pub fn build_columnar_selection(
&mut self,
display_map: &DisplaySnapshot,
row: u32,
positions: &Range<f32>,
reversed: bool,
text_layout_details: &TextLayoutDetails,
) -> Option<Selection<Point>> {
let is_empty = positions.start == positions.end;
let line_len = display_map.line_len(row);
let layed_out_line = display_map.lay_out_line_for_row(row, &text_layout_details);
let start_col = layed_out_line.closest_index_for_x(positions.start) as u32;
if start_col < line_len || (is_empty && positions.start == layed_out_line.width()) {
let start = DisplayPoint::new(row, start_col);
let end_col = layed_out_line.closest_index_for_x(positions.end) as u32;
let end = DisplayPoint::new(row, end_col);
Some(Selection {
id: post_inc(&mut self.next_selection_id),
start: start.to_point(display_map),
end: end.to_point(display_map),
reversed,
goal: SelectionGoal::HorizontalRange {
start: positions.start,
end: positions.end,
},
})
} else {
None
}
}
pub(crate) fn change_with<R>(
&mut self,
cx: &mut AppContext,
change: impl FnOnce(&mut MutableSelectionsCollection) -> R,
) -> (bool, R) {
let mut mutable_collection = MutableSelectionsCollection {
collection: self,
selections_changed: false,
cx,
};
let result = change(&mut mutable_collection);
assert!(
!mutable_collection.disjoint.is_empty() || mutable_collection.pending.is_some(),
"There must be at least one selection"
);
(mutable_collection.selections_changed, result)
}
}
pub struct MutableSelectionsCollection<'a> {
collection: &'a mut SelectionsCollection,
selections_changed: bool,
cx: &'a mut AppContext,
}
impl<'a> MutableSelectionsCollection<'a> {
pub fn display_map(&mut self) -> DisplaySnapshot {
self.collection.display_map(self.cx)
}
fn buffer(&self) -> Ref<MultiBufferSnapshot> {
self.collection.buffer(self.cx)
}
pub fn clear_disjoint(&mut self) {
self.collection.disjoint = Arc::from([]);
}
pub fn delete(&mut self, selection_id: usize) {
let mut changed = false;
self.collection.disjoint = self
.disjoint
.iter()
.filter(|selection| {
let found = selection.id == selection_id;
changed |= found;
!found
})
.cloned()
.collect();
self.selections_changed |= changed;
}
pub fn clear_pending(&mut self) {
if self.collection.pending.is_some() {
self.collection.pending = None;
self.selections_changed = true;
}
}
pub fn set_pending_anchor_range(&mut self, range: Range<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection {
selection: Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: range.start,
end: range.end,
reversed: false,
goal: SelectionGoal::None,
},
mode,
});
self.selections_changed = true;
}
pub fn set_pending_display_range(&mut self, range: Range<DisplayPoint>, mode: SelectMode) {
let (start, end, reversed) = {
let display_map = self.display_map();
let buffer = self.buffer();
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
let end_bias = if end > start { Bias::Left } else { Bias::Right };
(
buffer.anchor_before(start.to_point(&display_map)),
buffer.anchor_at(end.to_point(&display_map), end_bias),
reversed,
)
};
let new_pending = PendingSelection {
selection: Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
},
mode,
};
self.collection.pending = Some(new_pending);
self.selections_changed = true;
}
pub fn set_pending(&mut self, selection: Selection<Anchor>, mode: SelectMode) {
self.collection.pending = Some(PendingSelection { selection, mode });
self.selections_changed = true;
}
pub fn try_cancel(&mut self) -> bool {
if let Some(pending) = self.collection.pending.take() {
if self.disjoint.is_empty() {
self.collection.disjoint = Arc::from([pending.selection]);
}
self.selections_changed = true;
return true;
}
let mut oldest = self.oldest_anchor().clone();
if self.count() > 1 {
self.collection.disjoint = Arc::from([oldest]);
self.selections_changed = true;
return true;
}
if !oldest.start.cmp(&oldest.end, &self.buffer()).is_eq() {
let head = oldest.head();
oldest.start = head.clone();
oldest.end = head;
self.collection.disjoint = Arc::from([oldest]);
self.selections_changed = true;
return true;
}
false
}
pub fn insert_range<T>(&mut self, range: Range<T>)
where
T: 'a + ToOffset + ToPoint + TextDimension + Ord + Sub<T, Output = T> + std::marker::Copy,
{
let mut selections = self.all(self.cx);
let mut start = range.start.to_offset(&self.buffer());
let mut end = range.end.to_offset(&self.buffer());
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
selections.push(Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
});
self.select(selections);
}
pub fn select<T>(&mut self, mut selections: Vec<Selection<T>>)
where
T: ToOffset + ToPoint + Ord + std::marker::Copy + std::fmt::Debug,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
selections.sort_unstable_by_key(|s| s.start);
// Merge overlapping selections.
let mut i = 1;
while i < selections.len() {
if selections[i - 1].end >= selections[i].start {
let removed = selections.remove(i);
if removed.start < selections[i - 1].start {
selections[i - 1].start = removed.start;
}
if removed.end > selections[i - 1].end {
selections[i - 1].end = removed.end;
}
} else {
i += 1;
}
}
self.collection.disjoint = Arc::from_iter(selections.into_iter().map(|selection| {
let end_bias = if selection.end > selection.start {
Bias::Left
} else {
Bias::Right
};
Selection {
id: selection.id,
start: buffer.anchor_after(selection.start),
end: buffer.anchor_at(selection.end, end_bias),
reversed: selection.reversed,
goal: selection.goal,
}
}));
self.collection.pending = None;
self.selections_changed = true;
}
pub fn select_anchors(&mut self, selections: Vec<Selection<Anchor>>) {
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let resolved_selections =
resolve_multiple::<usize, _>(&selections, &buffer).collect::<Vec<_>>();
self.select(resolved_selections);
}
pub fn select_ranges<I, T>(&mut self, ranges: I)
where
I: IntoIterator<Item = Range<T>>,
T: ToOffset,
{
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let ranges = ranges
.into_iter()
.map(|range| range.start.to_offset(&buffer)..range.end.to_offset(&buffer));
self.select_offset_ranges(ranges);
}
fn select_offset_ranges<I>(&mut self, ranges: I)
where
I: IntoIterator<Item = Range<usize>>,
{
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
}
})
.collect::<Vec<_>>();
self.select(selections)
}
pub fn select_anchor_ranges<I: IntoIterator<Item = Range<Anchor>>>(&mut self, ranges: I) {
let buffer = self.buffer.read(self.cx).snapshot(self.cx);
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start;
let mut end = range.end;
let reversed = if start.cmp(&end, &buffer).is_gt() {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start,
end,
reversed,
goal: SelectionGoal::None,
}
})
.collect::<Vec<_>>();
self.select_anchors(selections)
}
pub fn new_selection_id(&mut self) -> usize {
post_inc(&mut self.next_selection_id)
}
pub fn select_display_ranges<T>(&mut self, ranges: T)
where
T: IntoIterator<Item = Range<DisplayPoint>>,
{
let display_map = self.display_map();
let selections = ranges
.into_iter()
.map(|range| {
let mut start = range.start;
let mut end = range.end;
let reversed = if start > end {
mem::swap(&mut start, &mut end);
true
} else {
false
};
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: start.to_point(&display_map),
end: end.to_point(&display_map),
reversed,
goal: SelectionGoal::None,
}
})
.collect();
self.select(selections);
}
pub fn move_with(
&mut self,
mut move_selection: impl FnMut(&DisplaySnapshot, &mut Selection<DisplayPoint>),
) {
let mut changed = false;
let display_map = self.display_map();
let selections = self
.all::<Point>(self.cx)
.into_iter()
.map(|selection| {
let mut moved_selection =
selection.map(|point| point.to_display_point(&display_map));
move_selection(&display_map, &mut moved_selection);
let moved_selection =
moved_selection.map(|display_point| display_point.to_point(&display_map));
if selection != moved_selection {
changed = true;
}
moved_selection
})
.collect();
if changed {
self.select(selections)
}
}
pub fn move_offsets_with(
&mut self,
mut move_selection: impl FnMut(&MultiBufferSnapshot, &mut Selection<usize>),
) {
let mut changed = false;
let snapshot = self.buffer().clone();
let selections = self
.all::<usize>(self.cx)
.into_iter()
.map(|selection| {
let mut moved_selection = selection.clone();
move_selection(&snapshot, &mut moved_selection);
if selection != moved_selection {
changed = true;
}
moved_selection
})
.collect();
drop(snapshot);
if changed {
self.select(selections)
}
}
pub fn move_heads_with(
&mut self,
mut update_head: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> (DisplayPoint, SelectionGoal),
) {
self.move_with(|map, selection| {
let (new_head, new_goal) = update_head(map, selection.head(), selection.goal);
selection.set_head(new_head, new_goal);
});
}
pub fn move_cursors_with(
&mut self,
mut update_cursor_position: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> (DisplayPoint, SelectionGoal),
) {
self.move_with(|map, selection| {
let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal);
selection.collapse_to(cursor, new_goal)
});
}
pub fn maybe_move_cursors_with(
&mut self,
mut update_cursor_position: impl FnMut(
&DisplaySnapshot,
DisplayPoint,
SelectionGoal,
) -> Option<(DisplayPoint, SelectionGoal)>,
) {
self.move_cursors_with(|map, point, goal| {
update_cursor_position(map, point, goal).unwrap_or((point, goal))
})
}
pub fn replace_cursors_with(
&mut self,
mut find_replacement_cursors: impl FnMut(&DisplaySnapshot) -> Vec<DisplayPoint>,
) {
let display_map = self.display_map();
let new_selections = find_replacement_cursors(&display_map)
.into_iter()
.map(|cursor| {
let cursor_point = cursor.to_point(&display_map);
Selection {
id: post_inc(&mut self.collection.next_selection_id),
start: cursor_point,
end: cursor_point,
reversed: false,
goal: SelectionGoal::None,
}
})
.collect();
self.select(new_selections);
}
/// Compute new ranges for any selections that were located in excerpts that have
/// since been removed.
///
/// Returns a `HashMap` indicating which selections whose former head position
/// was no longer present. The keys of the map are selection ids. The values are
/// the id of the new excerpt where the head of the selection has been moved.
pub fn refresh(&mut self) -> HashMap<usize, ExcerptId> {
let mut pending = self.collection.pending.take();
let mut selections_with_lost_position = HashMap::default();
let anchors_with_status = {
let buffer = self.buffer();
let disjoint_anchors = self
.disjoint
.iter()
.flat_map(|selection| [&selection.start, &selection.end]);
buffer.refresh_anchors(disjoint_anchors)
};
let adjusted_disjoint: Vec<_> = anchors_with_status
.chunks(2)
.map(|selection_anchors| {
let (anchor_ix, start, kept_start) = selection_anchors[0].clone();
let (_, end, kept_end) = selection_anchors[1].clone();
let selection = &self.disjoint[anchor_ix / 2];
let kept_head = if selection.reversed {
kept_start
} else {
kept_end
};
if !kept_head {
selections_with_lost_position.insert(selection.id, selection.head().excerpt_id);
}
Selection {
id: selection.id,
start,
end,
reversed: selection.reversed,
goal: selection.goal,
}
})
.collect();
if !adjusted_disjoint.is_empty() {
let resolved_selections =
resolve_multiple(adjusted_disjoint.iter(), &self.buffer()).collect();
self.select::<usize>(resolved_selections);
}
if let Some(pending) = pending.as_mut() {
let buffer = self.buffer();
let anchors =
buffer.refresh_anchors([&pending.selection.start, &pending.selection.end]);
let (_, start, kept_start) = anchors[0].clone();
let (_, end, kept_end) = anchors[1].clone();
let kept_head = if pending.selection.reversed {
kept_start
} else {
kept_end
};
if !kept_head {
selections_with_lost_position
.insert(pending.selection.id, pending.selection.head().excerpt_id);
}
pending.selection.start = start;
pending.selection.end = end;
}
self.collection.pending = pending;
self.selections_changed = true;
selections_with_lost_position
}
}
impl<'a> Deref for MutableSelectionsCollection<'a> {
type Target = SelectionsCollection;
fn deref(&self) -> &Self::Target {
self.collection
}
}
impl<'a> DerefMut for MutableSelectionsCollection<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.collection
}
}
// Panics if passed selections are not in order
pub fn resolve_multiple<'a, D, I>(
selections: I,
snapshot: &MultiBufferSnapshot,
) -> impl 'a + Iterator<Item = Selection<D>>
where
D: TextDimension + Ord + Sub<D, Output = D> + std::fmt::Debug,
I: 'a + IntoIterator<Item = &'a Selection<Anchor>>,
{
let (to_summarize, selections) = selections.into_iter().tee();
let mut summaries = snapshot
.summaries_for_anchors::<D, _>(
to_summarize
.flat_map(|s| [&s.start, &s.end])
.collect::<Vec<_>>(),
)
.into_iter();
selections.map(move |s| Selection {
id: s.id,
start: summaries.next().unwrap(),
end: summaries.next().unwrap(),
reversed: s.reversed,
goal: s.goal,
})
}
fn resolve<D: TextDimension + Ord + Sub<D, Output = D>>(
selection: &Selection<Anchor>,
buffer: &MultiBufferSnapshot,
) -> Selection<D> {
selection.map(|p| p.summary::<D>(buffer))
}

View file

@ -0,0 +1,83 @@
pub mod editor_lsp_test_context;
pub mod editor_test_context;
use crate::{
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
DisplayPoint, Editor, EditorMode, MultiBuffer,
};
use gpui::{ModelHandle, ViewContext};
use project::Project;
use util::test::{marked_text_offsets, marked_text_ranges};
#[cfg(test)]
#[ctor::ctor]
fn init_logger() {
if std::env::var("RUST_LOG").is_ok() {
env_logger::init();
}
}
// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one.
pub fn marked_display_snapshot(
text: &str,
cx: &mut gpui::AppContext,
) -> (DisplaySnapshot, Vec<DisplayPoint>) {
let (unmarked_text, markers) = marked_text_offsets(text);
let family_id = cx
.font_cache()
.load_family(&["Helvetica"], &Default::default())
.unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = MultiBuffer::build_simple(&unmarked_text, cx);
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, font_id, font_size, None, 1, 1, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
let markers = markers
.into_iter()
.map(|offset| offset.to_display_point(&snapshot))
.collect();
(snapshot, markers)
}
pub fn select_ranges(editor: &mut Editor, marked_text: &str, cx: &mut ViewContext<Editor>) {
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(None, cx, |s| s.select_ranges(text_ranges));
}
pub fn assert_text_with_selections(
editor: &mut Editor,
marked_text: &str,
cx: &mut ViewContext<Editor>,
) {
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), unmarked_text);
assert_eq!(editor.selections.ranges(cx), text_ranges);
}
// RA thinks this is dead code even though it is used in a whole lot of tests
#[allow(dead_code)]
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn build_editor(
buffer: ModelHandle<MultiBuffer>,
cx: &mut ViewContext<Editor>,
) -> Editor {
Editor::new(EditorMode::Full, buffer, None, None, cx)
}
pub(crate) fn build_editor_with_project(
project: ModelHandle<Project>,
buffer: ModelHandle<MultiBuffer>,
cx: &mut ViewContext<Editor>,
) -> Editor {
Editor::new(EditorMode::Full, buffer, Some(project), None, cx)
}

View file

@ -0,0 +1,297 @@
use std::{
borrow::Cow,
ops::{Deref, DerefMut, Range},
sync::Arc,
};
use anyhow::Result;
use crate::{Editor, ToPoint};
use collections::HashSet;
use futures::Future;
use gpui::{json, ViewContext, ViewHandle};
use indoc::indoc;
use language::{point_to_lsp, FakeLspAdapter, Language, LanguageConfig, LanguageQueries};
use lsp::{notification, request};
use multi_buffer::ToPointUtf16;
use project::Project;
use smol::stream::StreamExt;
use workspace::{AppState, Workspace, WorkspaceHandle};
use super::editor_test_context::EditorTestContext;
pub struct EditorLspTestContext<'a> {
pub cx: EditorTestContext<'a>,
pub lsp: lsp::FakeLanguageServer,
pub workspace: ViewHandle<Workspace>,
pub buffer_lsp_url: lsp::Url,
}
impl<'a> EditorLspTestContext<'a> {
pub async fn new(
mut language: Language,
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
use json::json;
let app_state = cx.update(AppState::test);
cx.update(|cx| {
language::init(cx);
crate::init(cx);
workspace::init(app_state.clone(), cx);
Project::init_settings(cx);
});
let file_name = format!(
"file.{}",
language
.path_suffixes()
.first()
.expect("language must have a path suffix for EditorLspTestContext")
);
let mut fake_servers = language
.set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
capabilities,
..Default::default()
}))
.await;
let project = Project::test(app_state.fs.clone(), [], cx).await;
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
app_state
.fs
.as_fake()
.insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
.await;
let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
let workspace = window.root(cx);
project
.update(cx, |project, cx| {
project.find_or_create_local_worktree("/root", true, cx)
})
.await
.unwrap();
cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx))
.await;
let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone());
let item = workspace
.update(cx, |workspace, cx| {
workspace.open_path(file, None, true, cx)
})
.await
.expect("Could not open test file");
let editor = cx.update(|cx| {
item.act_as::<Editor>(cx)
.expect("Opened test file wasn't an editor")
});
editor.update(cx, |_, cx| cx.focus_self());
let lsp = fake_servers.next().await.unwrap();
Self {
cx: EditorTestContext {
cx,
window: window.into(),
editor,
},
lsp,
workspace,
buffer_lsp_url: lsp::Url::from_file_path(format!("/root/dir/{file_name}")).unwrap(),
}
}
pub async fn new_rust(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let language = Language::new(
LanguageConfig {
name: "Rust".into(),
path_suffixes: vec!["rs".to_string()],
..Default::default()
},
Some(tree_sitter_rust::language()),
)
.with_queries(LanguageQueries {
indents: Some(Cow::from(indoc! {r#"
[
((where_clause) _ @end)
(field_expression)
(call_expression)
(assignment_expression)
(let_declaration)
(let_chain)
(await_expression)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent"#})),
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)
(closure_parameters "|" @open "|" @close)"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, capabilities, cx).await
}
pub async fn new_typescript(
capabilities: lsp::ServerCapabilities,
cx: &'a mut gpui::TestAppContext,
) -> EditorLspTestContext<'a> {
let mut word_characters: HashSet<char> = Default::default();
word_characters.insert('$');
word_characters.insert('#');
let language = Language::new(
LanguageConfig {
name: "Typescript".into(),
path_suffixes: vec!["ts".to_string()],
brackets: language::BracketPairConfig {
pairs: vec![language::BracketPair {
start: "{".to_string(),
end: "}".to_string(),
close: true,
newline: true,
}],
disabled_scopes_by_bracket_ix: Default::default(),
},
word_characters,
..Default::default()
},
Some(tree_sitter_typescript::language_typescript()),
)
.with_queries(LanguageQueries {
brackets: Some(Cow::from(indoc! {r#"
("(" @open ")" @close)
("[" @open "]" @close)
("{" @open "}" @close)
("<" @open ">" @close)
("\"" @open "\"" @close)"#})),
indents: Some(Cow::from(indoc! {r#"
[
(call_expression)
(assignment_expression)
(member_expression)
(lexical_declaration)
(variable_declaration)
(assignment_expression)
(if_statement)
(for_statement)
] @indent
(_ "[" "]" @end) @indent
(_ "<" ">" @end) @indent
(_ "{" "}" @end) @indent
(_ "(" ")" @end) @indent
"#})),
..Default::default()
})
.expect("Could not parse queries");
Self::new(language, capabilities, cx).await
}
// Constructs lsp range using a marked string with '[', ']' range delimiters
pub fn lsp_range(&mut self, marked_text: &str) -> lsp::Range {
let ranges = self.ranges(marked_text);
self.to_lsp_range(ranges[0].clone())
}
pub fn to_lsp_range(&mut self, range: Range<usize>) -> lsp::Range {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let start_point = range.start.to_point(&snapshot.buffer_snapshot);
let end_point = range.end.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
let start = point_to_lsp(
buffer
.point_to_buffer_offset(start_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
let end = point_to_lsp(
buffer
.point_to_buffer_offset(end_point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
);
lsp::Range { start, end }
})
}
pub fn to_lsp(&mut self, offset: usize) -> lsp::Position {
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let point = offset.to_point(&snapshot.buffer_snapshot);
self.editor(|editor, cx| {
let buffer = editor.buffer().read(cx);
point_to_lsp(
buffer
.point_to_buffer_offset(point, cx)
.unwrap()
.1
.to_point_utf16(&buffer.read(cx)),
)
})
}
pub fn update_workspace<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Workspace, &mut ViewContext<Workspace>) -> T,
{
self.workspace.update(self.cx.cx, update)
}
pub fn handle_request<T, F, Fut>(
&self,
mut handler: F,
) -> futures::channel::mpsc::UnboundedReceiver<()>
where
T: 'static + request::Request,
T::Params: 'static + Send,
F: 'static + Send + FnMut(lsp::Url, T::Params, gpui::AsyncAppContext) -> Fut,
Fut: 'static + Send + Future<Output = Result<T::Result>>,
{
let url = self.buffer_lsp_url.clone();
self.lsp.handle_request::<T, _, _>(move |params, cx| {
let url = url.clone();
handler(url, params, cx)
})
}
pub fn notify<T: notification::Notification>(&self, params: T::Params) {
self.lsp.notify::<T>(params);
}
}
impl<'a> Deref for EditorLspTestContext<'a> {
type Target = EditorTestContext<'a>;
fn deref(&self) -> &Self::Target {
&self.cx
}
}
impl<'a> DerefMut for EditorLspTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}

View file

@ -0,0 +1,332 @@
use crate::{
display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer,
};
use futures::Future;
use gpui::{
executor::Foreground, keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle,
ModelContext, ViewContext, ViewHandle,
};
use indoc::indoc;
use language::{Buffer, BufferSnapshot};
use project::{FakeFs, Project};
use std::{
any::TypeId,
ops::{Deref, DerefMut, Range},
};
use util::{
assert_set_eq,
test::{generate_marked_text, marked_text_ranges},
};
use super::build_editor_with_project;
pub struct EditorTestContext<'a> {
pub cx: &'a mut gpui::TestAppContext,
pub window: AnyWindowHandle,
pub editor: ViewHandle<Editor>,
}
impl<'a> EditorTestContext<'a> {
pub async fn new(cx: &'a mut gpui::TestAppContext) -> EditorTestContext<'a> {
let fs = FakeFs::new(cx.background());
// fs.insert_file("/file", "".to_owned()).await;
fs.insert_tree(
"/root",
gpui::serde_json::json!({
"file": "",
}),
)
.await;
let project = Project::test(fs, ["/root".as_ref()], cx).await;
let buffer = project
.update(cx, |project, cx| {
project.open_local_buffer("/root/file", cx)
})
.await
.unwrap();
let window = cx.add_window(|cx| {
cx.focus_self();
build_editor_with_project(project, MultiBuffer::build_from_buffer(buffer, cx), cx)
});
let editor = window.root(cx);
Self {
cx,
window: window.into(),
editor,
}
}
pub fn condition(
&self,
predicate: impl FnMut(&Editor, &AppContext) -> bool,
) -> impl Future<Output = ()> {
self.editor.condition(self.cx, predicate)
}
pub fn editor<F, T>(&self, read: F) -> T
where
F: FnOnce(&Editor, &ViewContext<Editor>) -> T,
{
self.editor.read_with(self.cx, read)
}
pub fn update_editor<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Editor, &mut ViewContext<Editor>) -> T,
{
self.editor.update(self.cx, update)
}
pub fn multibuffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&MultiBuffer, &AppContext) -> T,
{
self.editor(|editor, cx| read(editor.buffer().read(cx), cx))
}
pub fn update_multibuffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut MultiBuffer, &mut ModelContext<MultiBuffer>) -> T,
{
self.update_editor(|editor, cx| editor.buffer().update(cx, update))
}
pub fn buffer_text(&self) -> String {
self.multibuffer(|buffer, cx| buffer.snapshot(cx).text())
}
pub fn buffer<F, T>(&self, read: F) -> T
where
F: FnOnce(&Buffer, &AppContext) -> T,
{
self.multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap().read(cx);
read(buffer, cx)
})
}
pub fn update_buffer<F, T>(&mut self, update: F) -> T
where
F: FnOnce(&mut Buffer, &mut ModelContext<Buffer>) -> T,
{
self.update_multibuffer(|multibuffer, cx| {
let buffer = multibuffer.as_singleton().unwrap();
buffer.update(cx, update)
})
}
pub fn buffer_snapshot(&self) -> BufferSnapshot {
self.buffer(|buffer, _| buffer.snapshot())
}
pub fn simulate_keystroke(&mut self, keystroke_text: &str) -> ContextHandle {
let keystroke_under_test_handle =
self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text));
let keystroke = Keystroke::parse(keystroke_text).unwrap();
self.cx.dispatch_keystroke(self.window, keystroke, false);
keystroke_under_test_handle
}
pub fn simulate_keystrokes<const COUNT: usize>(
&mut self,
keystroke_texts: [&str; COUNT],
) -> ContextHandle {
let keystrokes_under_test_handle =
self.add_assertion_context(format!("Simulated Keystrokes: {:?}", keystroke_texts));
for keystroke_text in keystroke_texts.into_iter() {
self.simulate_keystroke(keystroke_text);
}
// it is common for keyboard shortcuts to kick off async actions, so this ensures that they are complete
// before returning.
// NOTE: we don't do this in simulate_keystroke() because a possible cause of bugs is that typing too
// quickly races with async actions.
if let Foreground::Deterministic { cx_id: _, executor } = self.cx.foreground().as_ref() {
executor.run_until_parked();
} else {
unreachable!();
}
keystrokes_under_test_handle
}
pub fn ranges(&self, marked_text: &str) -> Vec<Range<usize>> {
let (unmarked_text, ranges) = marked_text_ranges(marked_text, false);
assert_eq!(self.buffer_text(), unmarked_text);
ranges
}
pub fn display_point(&mut self, marked_text: &str) -> DisplayPoint {
let ranges = self.ranges(marked_text);
let snapshot = self
.editor
.update(self.cx, |editor, cx| editor.snapshot(cx));
ranges[0].start.to_display_point(&snapshot)
}
// Returns anchors for the current buffer using `«` and `»`
pub fn text_anchor_range(&self, marked_text: &str) -> Range<language::Anchor> {
let ranges = self.ranges(marked_text);
let snapshot = self.buffer_snapshot();
snapshot.anchor_before(ranges[0].start)..snapshot.anchor_after(ranges[0].end)
}
pub fn set_diff_base(&mut self, diff_base: Option<&str>) {
let diff_base = diff_base.map(String::from);
self.update_buffer(|buffer, cx| buffer.set_diff_base(diff_base, cx));
}
/// Change the editor's text and selections using a string containing
/// embedded range markers that represent the ranges and directions of
/// each selection.
///
/// Returns a context handle so that assertion failures can print what
/// editor state was needed to cause the failure.
///
/// See the `util::test::marked_text_ranges` function for more information.
pub fn set_state(&mut self, marked_text: &str) -> ContextHandle {
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
editor.set_text(unmarked_text, cx);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges)
})
});
state_context
}
/// Only change the editor's selections
pub fn set_selections_state(&mut self, marked_text: &str) -> ContextHandle {
let state_context = self.add_assertion_context(format!(
"Initial Editor State: \"{}\"",
marked_text.escape_debug().to_string()
));
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update(self.cx, |editor, cx| {
assert_eq!(editor.text(cx), unmarked_text);
editor.change_selections(Some(Autoscroll::fit()), cx, |s| {
s.select_ranges(selection_ranges)
})
});
state_context
}
/// Make an assertion about the editor's text and the ranges and directions
/// of its selections using a string containing embedded range markers.
///
/// See the `util::test::marked_text_ranges` function for more information.
#[track_caller]
pub fn assert_editor_state(&mut self, marked_text: &str) {
let (unmarked_text, expected_selections) = marked_text_ranges(marked_text, true);
let buffer_text = self.buffer_text();
if buffer_text != unmarked_text {
panic!("Unmarked text doesn't match buffer text\nBuffer text: {buffer_text:?}\nUnmarked text: {unmarked_text:?}\nRaw buffer text\n{buffer_text}Raw unmarked text\n{unmarked_text}");
}
self.assert_selections(expected_selections, marked_text.to_string())
}
pub fn editor_state(&mut self) -> String {
generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true)
}
#[track_caller]
pub fn assert_editor_background_highlights<Tag: 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let actual_ranges: Vec<Range<usize>> = self.update_editor(|editor, cx| {
let snapshot = editor.snapshot(cx);
editor
.background_highlights
.get(&TypeId::of::<Tag>())
.map(|h| h.1.clone())
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect()
});
assert_set_eq!(actual_ranges, expected_ranges);
}
#[track_caller]
pub fn assert_editor_text_highlights<Tag: ?Sized + 'static>(&mut self, marked_text: &str) {
let expected_ranges = self.ranges(marked_text);
let snapshot = self.update_editor(|editor, cx| editor.snapshot(cx));
let actual_ranges: Vec<Range<usize>> = snapshot
.text_highlight_ranges::<Tag>()
.map(|ranges| ranges.as_ref().clone().1)
.unwrap_or_default()
.into_iter()
.map(|range| range.to_offset(&snapshot.buffer_snapshot))
.collect();
assert_set_eq!(actual_ranges, expected_ranges);
}
#[track_caller]
pub fn assert_editor_selections(&mut self, expected_selections: Vec<Range<usize>>) {
let expected_marked_text =
generate_marked_text(&self.buffer_text(), &expected_selections, true);
self.assert_selections(expected_selections, expected_marked_text)
}
fn editor_selections(&self) -> Vec<Range<usize>> {
self.editor
.read_with(self.cx, |editor, cx| editor.selections.all::<usize>(cx))
.into_iter()
.map(|s| {
if s.reversed {
s.end..s.start
} else {
s.start..s.end
}
})
.collect::<Vec<_>>()
}
#[track_caller]
fn assert_selections(
&mut self,
expected_selections: Vec<Range<usize>>,
expected_marked_text: String,
) {
let actual_selections = self.editor_selections();
let actual_marked_text =
generate_marked_text(&self.buffer_text(), &actual_selections, true);
if expected_selections != actual_selections {
panic!(
indoc! {"
{}Editor has unexpected selections.
Expected selections:
{}
Actual selections:
{}
"},
self.assertion_context(),
expected_marked_text,
actual_marked_text,
);
}
}
}
impl<'a> Deref for EditorTestContext<'a> {
type Target = gpui::TestAppContext;
fn deref(&self) -> &Self::Target {
self.cx
}
}
impl<'a> DerefMut for EditorTestContext<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cx
}
}