Move editor into its own crate

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2021-10-04 15:06:12 -07:00
parent d5b60ad124
commit 1d97f08901
28 changed files with 456 additions and 400 deletions

View file

@ -33,6 +33,7 @@ clock = { path = "../clock" }
crossbeam-channel = "0.5.0"
ctor = "0.1.20"
dirs = "3.0"
editor = { path = "../editor" }
easy-parallel = "3.1.0"
fsevent = { path = "../fsevent" }
futures = "0.3"
@ -70,7 +71,7 @@ tree-sitter = "0.19.5"
tree-sitter-rust = "0.19.0"
url = "2.2"
util = { path = "../util" }
worktree = { path = "../worktree" }
worktree = { path = "../worktree" }
rpc = { path = "../rpc" }
[dev-dependencies]
@ -80,6 +81,7 @@ serde_json = { version = "1.0.64", features = ["preserve_order"] }
tempdir = { version = "0.3.7" }
unindent = "0.1.7"
buffer = { path = "../buffer", features = ["test-support"] }
editor = { path = "../editor", features = ["test-support"] }
gpui = { path = "../gpui", features = ["test-support"] }
rpc_client = { path = "../rpc_client", features = ["test-support"] }
util = { path = "../util", features = ["test-support"] }

View file

@ -208,7 +208,7 @@ padding = { left = 16, right = 16, top = 8, bottom = 4 }
[selector.item]
text = "$text.1"
highlight_text = { extends = "$text.base", color = "$syntax.keyword.color", weight = "$syntax.keyword.weight" }
highlight_text = { extends = "$text.base", color = "$editor.syntax.keyword.color", weight = "$editor.syntax.keyword.weight" }
padding = { left = 16, right = 16, top = 4, bottom = 4 }
corner_radius = 6

View file

@ -26,7 +26,7 @@ guests = [
{ selection = "#3B874B33", cursor = "#3B874B" },
{ selection = "#BD7CB433", cursor = "#BD7CB4" },
{ selection = "#EE823133", cursor = "#EE8231" },
{ selection = "#5A2B9233", cursor = "#5A2B92" }
{ selection = "#5A2B9233", cursor = "#5A2B92" },
]
[status]
@ -39,7 +39,7 @@ bad = "#b7372e"
active_line = "#00000033"
hover = "#00000033"
[syntax]
[editor.syntax]
keyword = { color = "#0086c0", weight = "bold" }
function = "#dcdcaa"
string = "#cb8f77"

View file

@ -26,7 +26,7 @@ guests = [
{ selection = "#3B874B33", cursor = "#3B874B" },
{ selection = "#BD7CB433", cursor = "#BD7CB4" },
{ selection = "#EE823133", cursor = "#EE8231" },
{ selection = "#5A2B9233", cursor = "#5A2B92" }
{ selection = "#5A2B9233", cursor = "#5A2B92" },
]
[status]
@ -39,7 +39,7 @@ bad = "#b7372e"
active_line = "#00000022"
hover = "#00000033"
[syntax]
[editor.syntax]
keyword = { color = "#0086c0", weight = "bold" }
function = "#dcdcaa"
string = "#cb8f77"

View file

@ -26,7 +26,7 @@ guests = [
{ selection = "#3B874B33", cursor = "#3B874B" },
{ selection = "#BD7CB433", cursor = "#BD7CB4" },
{ selection = "#EE823133", cursor = "#EE8231" },
{ selection = "#5A2B9233", cursor = "#5A2B92" }
{ selection = "#5A2B9233", cursor = "#5A2B92" },
]
[status]
@ -39,7 +39,7 @@ bad = "#b7372e"
active_line = "#00000008"
hover = "#0000000D"
[syntax]
[editor.syntax]
keyword = { color = "#0000fa", weight = "bold" }
function = "#795e26"
string = "#a82121"

View file

@ -1,10 +1,8 @@
use std::sync::Arc;
use crate::{
channel::{Channel, ChannelEvent, ChannelList, ChannelMessage},
editor::Editor,
theme, Settings,
};
use editor::{Editor, EditorSettings};
use gpui::{
action,
elements::*,
@ -16,6 +14,7 @@ use gpui::{
};
use postage::{prelude::Stream, watch};
use rpc_client as rpc;
use std::sync::Arc;
use time::{OffsetDateTime, UtcOffset};
use util::{ResultExt, TryFutureExt};
@ -55,10 +54,15 @@ impl ChatPanel {
let input_editor = cx.add_view(|cx| {
Editor::auto_height(
4,
settings.clone(),
{
let settings = settings.clone();
move |_| settings.borrow().theme.chat_panel.input_editor.as_editor()
move |_| {
let settings = settings.borrow();
EditorSettings {
tab_size: settings.tab_size,
style: settings.theme.chat_panel.input_editor.as_editor(),
}
}
},
cx,
)

File diff suppressed because it is too large Load diff

View file

@ -1,992 +0,0 @@
mod fold_map;
mod tab_map;
mod wrap_map;
use buffer::{self, Anchor, Buffer, Point, ToOffset, ToPoint};
use fold_map::{FoldMap, ToFoldPoint as _};
use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle};
use std::ops::Range;
use sum_tree::Bias;
use tab_map::TabMap;
use wrap_map::WrapMap;
pub use wrap_map::{BufferRows, HighlightedChunks};
pub trait ToDisplayPoint {
fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint;
}
pub struct DisplayMap {
buffer: ModelHandle<Buffer>,
fold_map: FoldMap,
tab_map: TabMap,
wrap_map: ModelHandle<WrapMap>,
}
impl Entity for DisplayMap {
type Event = ();
}
impl DisplayMap {
pub fn new(
buffer: ModelHandle<Buffer>,
tab_size: usize,
font_id: FontId,
font_size: f32,
wrap_width: Option<f32>,
cx: &mut ModelContext<Self>,
) -> Self {
let (fold_map, snapshot) = FoldMap::new(buffer.clone(), cx);
let (tab_map, snapshot) = TabMap::new(snapshot, tab_size);
let wrap_map =
cx.add_model(|cx| WrapMap::new(snapshot, font_id, font_size, wrap_width, cx));
cx.observe(&wrap_map, |_, _, cx| cx.notify()).detach();
DisplayMap {
buffer,
fold_map,
tab_map,
wrap_map,
}
}
pub fn snapshot(&self, cx: &mut ModelContext<Self>) -> DisplayMapSnapshot {
let (folds_snapshot, edits) = self.fold_map.read(cx);
let (tabs_snapshot, edits) = self.tab_map.sync(folds_snapshot.clone(), edits);
let wraps_snapshot = self
.wrap_map
.update(cx, |map, cx| map.sync(tabs_snapshot.clone(), edits, cx));
DisplayMapSnapshot {
buffer_snapshot: self.buffer.read(cx).snapshot(),
folds_snapshot,
tabs_snapshot,
wraps_snapshot,
}
}
pub fn fold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
cx: &mut ModelContext<Self>,
) {
let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
self.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let (snapshot, edits) = fold_map.fold(ranges, cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
self.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
}
pub fn unfold<T: ToOffset>(
&mut self,
ranges: impl IntoIterator<Item = Range<T>>,
cx: &mut ModelContext<Self>,
) {
let (mut fold_map, snapshot, edits) = self.fold_map.write(cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
self.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
let (snapshot, edits) = fold_map.unfold(ranges, cx);
let (snapshot, edits) = self.tab_map.sync(snapshot, edits);
self.wrap_map
.update(cx, |map, cx| map.sync(snapshot, edits, cx));
}
pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext<Self>) {
self.wrap_map
.update(cx, |map, cx| map.set_font(font_id, font_size, cx));
}
pub fn set_wrap_width(&self, width: Option<f32>, cx: &mut ModelContext<Self>) -> bool {
self.wrap_map
.update(cx, |map, cx| map.set_wrap_width(width, cx))
}
#[cfg(test)]
pub fn is_rewrapping(&self, cx: &gpui::AppContext) -> bool {
self.wrap_map.read(cx).is_rewrapping()
}
}
pub struct DisplayMapSnapshot {
buffer_snapshot: buffer::Snapshot,
folds_snapshot: fold_map::Snapshot,
tabs_snapshot: tab_map::Snapshot,
wraps_snapshot: wrap_map::Snapshot,
}
impl DisplayMapSnapshot {
#[cfg(test)]
pub fn fold_count(&self) -> usize {
self.folds_snapshot.fold_count()
}
pub fn is_empty(&self) -> bool {
self.buffer_snapshot.len() == 0
}
pub fn buffer_rows(&self, start_row: u32) -> BufferRows {
self.wraps_snapshot.buffer_rows(start_row)
}
pub fn buffer_row_count(&self) -> u32 {
self.buffer_snapshot.max_point().row + 1
}
pub fn prev_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
loop {
*display_point.column_mut() = 0;
let mut point = display_point.to_buffer_point(self, Bias::Left);
point.column = 0;
let next_display_point = point.to_display_point(self, Bias::Left);
if next_display_point == display_point {
return (display_point, point);
}
display_point = next_display_point;
}
}
pub fn next_row_boundary(&self, mut display_point: DisplayPoint) -> (DisplayPoint, Point) {
loop {
*display_point.column_mut() = self.line_len(display_point.row());
let mut point = display_point.to_buffer_point(self, Bias::Right);
point.column = self.buffer_snapshot.line_len(point.row);
let next_display_point = point.to_display_point(self, Bias::Right);
if next_display_point == display_point {
return (display_point, point);
}
display_point = next_display_point;
}
}
pub fn max_point(&self) -> DisplayPoint {
DisplayPoint(self.wraps_snapshot.max_point())
}
pub fn chunks_at(&self, display_row: u32) -> wrap_map::Chunks {
self.wraps_snapshot.chunks_at(display_row)
}
pub fn highlighted_chunks_for_rows(
&mut self,
display_rows: Range<u32>,
) -> wrap_map::HighlightedChunks {
self.wraps_snapshot
.highlighted_chunks_for_rows(display_rows)
}
pub fn chars_at<'a>(&'a self, point: DisplayPoint) -> impl Iterator<Item = char> + 'a {
let mut column = 0;
let mut chars = self.chunks_at(point.row()).flat_map(str::chars);
while column < point.column() {
if let Some(c) = chars.next() {
column += c.len_utf8() as u32;
} else {
break;
}
}
chars
}
pub fn column_to_chars(&self, display_row: u32, target: u32) -> u32 {
let mut count = 0;
let mut column = 0;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
if column >= target {
break;
}
count += 1;
column += c.len_utf8() as u32;
}
count
}
pub fn column_from_chars(&self, display_row: u32, char_count: u32) -> u32 {
let mut count = 0;
let mut column = 0;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
if c == '\n' || count >= char_count {
break;
}
count += 1;
column += c.len_utf8() as u32;
}
column
}
pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint {
DisplayPoint(self.wraps_snapshot.clip_point(point.0, bias))
}
pub fn folds_in_range<'a, T>(
&'a self,
range: Range<T>,
) -> impl Iterator<Item = &'a Range<Anchor>>
where
T: ToOffset,
{
self.folds_snapshot.folds_in_range(range)
}
pub fn intersects_fold<T: ToOffset>(&self, offset: T) -> bool {
self.folds_snapshot.intersects_fold(offset)
}
pub fn is_line_folded(&self, display_row: u32) -> bool {
let wrap_point = DisplayPoint::new(display_row, 0).0;
let row = self.wraps_snapshot.to_tab_point(wrap_point).row();
self.folds_snapshot.is_line_folded(row)
}
pub fn soft_wrap_indent(&self, display_row: u32) -> Option<u32> {
self.wraps_snapshot.soft_wrap_indent(display_row)
}
pub fn text(&self) -> String {
self.chunks_at(0).collect()
}
pub fn line(&self, display_row: u32) -> String {
let mut result = String::new();
for chunk in self.chunks_at(display_row) {
if let Some(ix) = chunk.find('\n') {
result.push_str(&chunk[0..ix]);
break;
} else {
result.push_str(chunk);
}
}
result
}
pub fn line_indent(&self, display_row: u32) -> (u32, bool) {
let mut indent = 0;
let mut is_blank = true;
for c in self.chars_at(DisplayPoint::new(display_row, 0)) {
if c == ' ' {
indent += 1;
} else {
is_blank = c == '\n';
break;
}
}
(indent, is_blank)
}
pub fn line_len(&self, row: u32) -> u32 {
self.wraps_snapshot.line_len(row)
}
pub fn longest_row(&self) -> u32 {
self.wraps_snapshot.longest_row()
}
pub fn anchor_before(&self, point: DisplayPoint, bias: Bias) -> Anchor {
self.buffer_snapshot
.anchor_before(point.to_buffer_point(self, bias))
}
pub fn anchor_after(&self, point: DisplayPoint, bias: Bias) -> Anchor {
self.buffer_snapshot
.anchor_after(point.to_buffer_point(self, bias))
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct DisplayPoint(wrap_map::WrapPoint);
impl DisplayPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(wrap_map::WrapPoint::new(row, column))
}
pub fn zero() -> Self {
Self::new(0, 0)
}
#[cfg(test)]
pub fn is_zero(&self) -> bool {
self.0.is_zero()
}
pub fn row(self) -> u32 {
self.0.row()
}
pub fn column(self) -> u32 {
self.0.column()
}
pub fn row_mut(&mut self) -> &mut u32 {
self.0.row_mut()
}
pub fn column_mut(&mut self) -> &mut u32 {
self.0.column_mut()
}
pub fn to_buffer_point(self, map: &DisplayMapSnapshot, bias: Bias) -> Point {
let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
unexpanded_point.to_buffer_point(&map.folds_snapshot)
}
pub fn to_buffer_offset(self, map: &DisplayMapSnapshot, bias: Bias) -> usize {
let unwrapped_point = map.wraps_snapshot.to_tab_point(self.0);
let unexpanded_point = map.tabs_snapshot.to_fold_point(unwrapped_point, bias).0;
unexpanded_point.to_buffer_offset(&map.folds_snapshot)
}
}
impl ToDisplayPoint for Point {
fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
let fold_point = self.to_fold_point(&map.folds_snapshot, bias);
let tab_point = map.tabs_snapshot.to_tab_point(fold_point);
let wrap_point = map.wraps_snapshot.to_wrap_point(tab_point);
DisplayPoint(wrap_point)
}
}
impl ToDisplayPoint for Anchor {
fn to_display_point(&self, map: &DisplayMapSnapshot, bias: Bias) -> DisplayPoint {
self.to_point(&map.buffer_snapshot)
.to_display_point(map, bias)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{editor::movement, test::*};
use buffer::{History, Language, LanguageConfig, RandomCharIter, SelectionGoal, SyntaxTheme};
use gpui::{color::Color, MutableAppContext};
use rand::{prelude::StdRng, Rng};
use std::{env, sync::Arc};
use Bias::*;
#[gpui::test(iterations = 100)]
async fn test_random(mut cx: gpui::TestAppContext, mut rng: StdRng) {
cx.foreground().set_block_on_ticks(0..=50);
cx.foreground().forbid_parking();
let operations = env::var("OPERATIONS")
.map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
.unwrap_or(10);
let font_cache = cx.font_cache().clone();
let tab_size = rng.gen_range(1..=4);
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let max_wrap_width = 300.0;
let mut wrap_width = if rng.gen_bool(0.1) {
None
} else {
Some(rng.gen_range(0.0..=max_wrap_width))
};
log::info!("tab size: {}", tab_size);
log::info!("wrap width: {:?}", wrap_width);
let buffer = cx.add_model(|cx| {
let len = rng.gen_range(0..10);
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
Buffer::new(0, text, cx)
});
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
});
let (_observer, notifications) = Observer::new(&map, &mut cx);
let mut fold_count = 0;
for _i in 0..operations {
match rng.gen_range(0..100) {
0..=19 => {
wrap_width = if rng.gen_bool(0.2) {
None
} else {
Some(rng.gen_range(0.0..=max_wrap_width))
};
log::info!("setting wrap width to {:?}", wrap_width);
map.update(&mut cx, |map, cx| map.set_wrap_width(wrap_width, cx));
}
20..=80 => {
let mut ranges = Vec::new();
for _ in 0..rng.gen_range(1..=3) {
buffer.read_with(&cx, |buffer, _| {
let end = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Right);
let start = buffer.clip_offset(rng.gen_range(0..=end), Left);
ranges.push(start..end);
});
}
if rng.gen() && fold_count > 0 {
log::info!("unfolding ranges: {:?}", ranges);
map.update(&mut cx, |map, cx| {
map.unfold(ranges, cx);
});
} else {
log::info!("folding ranges: {:?}", ranges);
map.update(&mut cx, |map, cx| {
map.fold(ranges, cx);
});
}
}
_ => {
buffer.update(&mut cx, |buffer, cx| buffer.randomly_mutate(&mut rng, cx));
}
}
if map.read_with(&cx, |map, cx| map.is_rewrapping(cx)) {
notifications.recv().await.unwrap();
}
let snapshot = map.update(&mut cx, |map, cx| map.snapshot(cx));
fold_count = snapshot.fold_count();
log::info!("buffer text: {:?}", buffer.read_with(&cx, |b, _| b.text()));
log::info!("display text: {:?}", snapshot.text());
// Line boundaries
for _ in 0..5 {
let row = rng.gen_range(0..=snapshot.max_point().row());
let column = rng.gen_range(0..=snapshot.line_len(row));
let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
let (prev_display_bound, prev_buffer_bound) = snapshot.prev_row_boundary(point);
let (next_display_bound, next_buffer_bound) = snapshot.next_row_boundary(point);
assert!(prev_display_bound <= point);
assert!(next_display_bound >= point);
assert_eq!(prev_buffer_bound.column, 0);
assert_eq!(prev_display_bound.column(), 0);
if next_display_bound < snapshot.max_point() {
assert_eq!(
buffer
.read_with(&cx, |buffer, _| buffer.chars_at(next_buffer_bound).next()),
Some('\n')
)
}
assert_eq!(
prev_display_bound,
prev_buffer_bound.to_display_point(&snapshot, Left),
"row boundary before {:?}. reported buffer row boundary: {:?}",
point,
prev_buffer_bound
);
assert_eq!(
next_display_bound,
next_buffer_bound.to_display_point(&snapshot, Right),
"display row boundary after {:?}. reported buffer row boundary: {:?}",
point,
next_buffer_bound
);
assert_eq!(
prev_buffer_bound,
prev_display_bound.to_buffer_point(&snapshot, Left),
"row boundary before {:?}. reported display row boundary: {:?}",
point,
prev_display_bound
);
assert_eq!(
next_buffer_bound,
next_display_bound.to_buffer_point(&snapshot, Right),
"row boundary after {:?}. reported display row boundary: {:?}",
point,
next_display_bound
);
}
// Movement
for _ in 0..5 {
let row = rng.gen_range(0..=snapshot.max_point().row());
let column = rng.gen_range(0..=snapshot.line_len(row));
let point = snapshot.clip_point(DisplayPoint::new(row, column), Left);
log::info!("Moving from point {:?}", point);
let moved_right = movement::right(&snapshot, point).unwrap();
log::info!("Right {:?}", moved_right);
if point < snapshot.max_point() {
assert!(moved_right > point);
if point.column() == snapshot.line_len(point.row())
|| snapshot.soft_wrap_indent(point.row()).is_some()
&& point.column() == snapshot.line_len(point.row()) - 1
{
assert!(moved_right.row() > point.row());
}
} else {
assert_eq!(moved_right, point);
}
let moved_left = movement::left(&snapshot, point).unwrap();
log::info!("Left {:?}", moved_left);
if !point.is_zero() {
assert!(moved_left < point);
if point.column() == 0 {
assert!(moved_left.row() < point.row());
}
} else {
assert!(moved_left.is_zero());
}
}
}
}
#[gpui::test]
fn test_soft_wraps(cx: &mut MutableAppContext) {
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
cx.foreground().forbid_parking();
let font_cache = cx.font_cache();
let tab_size = 4;
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 12.0;
let wrap_width = Some(64.);
let text = "one two three four five\nsix seven eight";
let buffer = cx.add_model(|cx| Buffer::new(0, text.to_string(), cx));
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, wrap_width, cx)
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(0).collect::<String>(),
"one two \nthree four \nfive\nsix seven \neight"
);
assert_eq!(
snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Left),
DisplayPoint::new(0, 7)
);
assert_eq!(
snapshot.clip_point(DisplayPoint::new(0, 8), Bias::Right),
DisplayPoint::new(1, 0)
);
assert_eq!(
movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
DisplayPoint::new(1, 0)
);
assert_eq!(
movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(),
DisplayPoint::new(0, 7)
);
assert_eq!(
movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(),
(DisplayPoint::new(0, 7), SelectionGoal::Column(10))
);
assert_eq!(
movement::down(
&snapshot,
DisplayPoint::new(0, 7),
SelectionGoal::Column(10)
)
.unwrap(),
(DisplayPoint::new(1, 10), SelectionGoal::Column(10))
);
assert_eq!(
movement::down(
&snapshot,
DisplayPoint::new(1, 10),
SelectionGoal::Column(10)
)
.unwrap(),
(DisplayPoint::new(2, 4), SelectionGoal::Column(10))
);
buffer.update(cx, |buffer, cx| {
let ix = buffer.text().find("seven").unwrap();
buffer.edit(vec![ix..ix], "and ", cx);
});
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(1).collect::<String>(),
"three four \nfive\nsix and \nseven eight"
);
// Re-wrap on font size changes
map.update(cx, |map, cx| map.set_font(font_id, font_size + 3., cx));
let snapshot = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
snapshot.chunks_at(1).collect::<String>(),
"three \nfour five\nsix and \nseven \neight"
)
}
#[gpui::test]
fn test_chunks_at(cx: &mut gpui::MutableAppContext) {
let text = sample_text(6, 6);
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
buffer.update(cx, |buffer, cx| {
buffer.edit(
vec![
Point::new(1, 0)..Point::new(1, 0),
Point::new(1, 1)..Point::new(1, 1),
Point::new(2, 1)..Point::new(2, 1),
],
"\t",
cx,
)
});
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx))
.chunks_at(1)
.collect::<String>()
.lines()
.next(),
Some(" b bbbbb")
);
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx))
.chunks_at(2)
.collect::<String>()
.lines()
.next(),
Some("c ccccc")
);
}
#[gpui::test]
async fn test_highlighted_chunks_at(mut cx: gpui::TestAppContext) {
use unindent::Unindent as _;
let grammar = tree_sitter_rust::language();
let text = r#"
fn outer() {}
mod module {
fn inner() {}
}"#
.unindent();
let highlight_query = tree_sitter::Query::new(
grammar,
r#"
(mod_item name: (identifier) body: _ @mod.body)
(function_item name: (identifier) @fn.name)"#,
)
.unwrap();
let theme = SyntaxTheme::new(vec![
("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
]);
let lang = Arc::new(Language {
config: LanguageConfig {
name: "Test".to_string(),
path_suffixes: vec![".test".to_string()],
..Default::default()
},
grammar: grammar.clone(),
highlight_query,
brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
highlight_map: Default::default(),
});
lang.set_theme(&theme);
let buffer = cx.add_model(|cx| {
Buffer::from_history(0, History::new(text.into()), None, Some(lang), cx)
});
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let tab_size = 2;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
assert_eq!(
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
vec![
("fn ".to_string(), None),
("outer".to_string(), Some("fn.name")),
("() {}\n\nmod module ".to_string(), None),
("{\n fn ".to_string(), Some("mod.body")),
("inner".to_string(), Some("fn.name")),
("() {}\n}".to_string(), Some("mod.body")),
]
);
assert_eq!(
cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
vec![
(" fn ".to_string(), Some("mod.body")),
("inner".to_string(), Some("fn.name")),
("() {}\n}".to_string(), Some("mod.body")),
]
);
map.update(&mut cx, |map, cx| {
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
});
assert_eq!(
cx.update(|cx| highlighted_chunks(0..2, &map, &theme, cx)),
vec![
("fn ".to_string(), None),
("out".to_string(), Some("fn.name")),
("".to_string(), None),
(" fn ".to_string(), Some("mod.body")),
("inner".to_string(), Some("fn.name")),
("() {}\n}".to_string(), Some("mod.body")),
]
);
}
#[gpui::test]
async fn test_highlighted_chunks_with_soft_wrapping(mut cx: gpui::TestAppContext) {
use unindent::Unindent as _;
cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX);
let grammar = tree_sitter_rust::language();
let text = r#"
fn outer() {}
mod module {
fn inner() {}
}"#
.unindent();
let highlight_query = tree_sitter::Query::new(
grammar,
r#"
(mod_item name: (identifier) body: _ @mod.body)
(function_item name: (identifier) @fn.name)"#,
)
.unwrap();
let theme = SyntaxTheme::new(vec![
("mod.body".to_string(), Color::from_u32(0xff0000ff).into()),
("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()),
]);
let lang = Arc::new(Language {
config: LanguageConfig {
name: "Test".to_string(),
path_suffixes: vec![".test".to_string()],
..Default::default()
},
grammar: grammar.clone(),
highlight_query,
brackets_query: tree_sitter::Query::new(grammar, "").unwrap(),
highlight_map: Default::default(),
});
lang.set_theme(&theme);
let buffer = cx.add_model(|cx| {
Buffer::from_history(0, History::new(text.into()), None, Some(lang), cx)
});
buffer.condition(&cx, |buf, _| !buf.is_parsing()).await;
let font_cache = cx.font_cache();
let tab_size = 4;
let family_id = font_cache.load_family(&["Courier"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 16.0;
let map = cx
.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), cx));
assert_eq!(
cx.update(|cx| highlighted_chunks(0..5, &map, &theme, cx)),
[
("fn \n".to_string(), None),
("oute\nr".to_string(), Some("fn.name")),
("() \n{}\n\n".to_string(), None),
]
);
assert_eq!(
cx.update(|cx| highlighted_chunks(3..5, &map, &theme, cx)),
[("{}\n\n".to_string(), None)]
);
map.update(&mut cx, |map, cx| {
map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx)
});
assert_eq!(
cx.update(|cx| highlighted_chunks(1..4, &map, &theme, cx)),
[
("out".to_string(), Some("fn.name")),
("\n".to_string(), None),
(" \nfn ".to_string(), Some("mod.body")),
("i\n".to_string(), Some("fn.name"))
]
);
}
#[gpui::test]
fn test_clip_point(cx: &mut gpui::MutableAppContext) {
use Bias::{Left, Right};
let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n";
let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), display_text);
for (input_column, bias, output_column) in vec![
("'a', '".len(), Left, "'a', '".len()),
("'a', '".len() + 1, Left, "'a', '".len()),
("'a', '".len() + 1, Right, "'a', 'α".len()),
("'a', 'α', ".len(), Left, "'a', 'α',".len()),
("'a', 'α', ".len(), Right, "'a', 'α', ".len()),
("'a', 'α', '".len() + 1, Left, "'a', 'α', '".len()),
("'a', 'α', '".len() + 1, Right, "'a', 'α', '✋".len()),
("'a', 'α', '✋',".len(), Right, "'a', 'α', '✋',".len()),
("'a', 'α', '✋', ".len(), Left, "'a', 'α', '✋',".len()),
(
"'a', 'α', '✋', ".len(),
Right,
"'a', 'α', '✋', ".len(),
),
] {
assert_eq!(
map.clip_point(DisplayPoint::new(1, input_column as u32), bias),
DisplayPoint::new(1, output_column as u32),
"clip_point(({}, {}))",
1,
input_column,
);
}
}
#[gpui::test]
fn test_tabs_with_multibyte_chars(cx: &mut gpui::MutableAppContext) {
let text = "\t\tα\nβ\t\n🏀β\t\tγ";
let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
let map = map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(map.text(), "α\nβ \n🏀β γ");
assert_eq!(
map.chunks_at(0).collect::<String>(),
"α\nβ \n🏀β γ"
);
assert_eq!(map.chunks_at(1).collect::<String>(), "β \n🏀β γ");
assert_eq!(map.chunks_at(2).collect::<String>(), "🏀β γ");
let point = Point::new(0, "\t\t".len() as u32);
let display_point = DisplayPoint::new(0, "".len() as u32);
assert_eq!(point.to_display_point(&map, Left), display_point);
assert_eq!(display_point.to_buffer_point(&map, Left), point,);
let point = Point::new(1, "β\t".len() as u32);
let display_point = DisplayPoint::new(1, "β ".len() as u32);
assert_eq!(point.to_display_point(&map, Left), display_point);
assert_eq!(display_point.to_buffer_point(&map, Left), point,);
let point = Point::new(2, "🏀β\t\t".len() as u32);
let display_point = DisplayPoint::new(2, "🏀β ".len() as u32);
assert_eq!(point.to_display_point(&map, Left), display_point);
assert_eq!(display_point.to_buffer_point(&map, Left), point,);
// Display points inside of expanded tabs
assert_eq!(
DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Right),
Point::new(0, "\t\t".len() as u32),
);
assert_eq!(
DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Left),
Point::new(0, "\t".len() as u32),
);
assert_eq!(
DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Right),
Point::new(0, "\t".len() as u32),
);
assert_eq!(
DisplayPoint::new(0, "".len() as u32).to_buffer_point(&map, Left),
Point::new(0, "".len() as u32),
);
// Clipping display points inside of multi-byte characters
assert_eq!(
map.clip_point(DisplayPoint::new(0, "".len() as u32 - 1), Left),
DisplayPoint::new(0, 0)
);
assert_eq!(
map.clip_point(DisplayPoint::new(0, "".len() as u32 - 1), Bias::Right),
DisplayPoint::new(0, "".len() as u32)
);
}
#[gpui::test]
fn test_max_point(cx: &mut gpui::MutableAppContext) {
let buffer = cx.add_model(|cx| Buffer::new(0, "aaa\n\t\tbbb", cx));
let tab_size = 4;
let font_cache = cx.font_cache();
let family_id = font_cache.load_family(&["Helvetica"]).unwrap();
let font_id = font_cache
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let map = cx.add_model(|cx| {
DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, cx)
});
assert_eq!(
map.update(cx, |map, cx| map.snapshot(cx)).max_point(),
DisplayPoint::new(1, 11)
)
}
fn highlighted_chunks<'a>(
rows: Range<u32>,
map: &ModelHandle<DisplayMap>,
theme: &'a SyntaxTheme,
cx: &mut MutableAppContext,
) -> Vec<(String, Option<&'a str>)> {
let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx));
let mut chunks: Vec<(String, Option<&str>)> = Vec::new();
for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) {
let style_name = theme.highlight_name(style_id);
if let Some((last_chunk, last_style_name)) = chunks.last_mut() {
if style_name == *last_style_name {
last_chunk.push_str(chunk);
} else {
chunks.push((chunk.to_string(), style_name));
}
} else {
chunks.push((chunk.to_string(), style_name));
}
}
chunks
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,475 +0,0 @@
use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot};
use buffer::{rope, HighlightId};
use parking_lot::Mutex;
use std::{mem, ops::Range};
use sum_tree::Bias;
pub struct TabMap(Mutex<Snapshot>);
impl TabMap {
pub fn new(input: FoldSnapshot, tab_size: usize) -> (Self, Snapshot) {
let snapshot = Snapshot {
fold_snapshot: input,
tab_size,
};
(Self(Mutex::new(snapshot.clone())), snapshot)
}
pub fn sync(
&self,
fold_snapshot: FoldSnapshot,
mut fold_edits: Vec<FoldEdit>,
) -> (Snapshot, Vec<Edit>) {
let mut old_snapshot = self.0.lock();
let new_snapshot = Snapshot {
fold_snapshot,
tab_size: old_snapshot.tab_size,
};
let mut tab_edits = Vec::with_capacity(fold_edits.len());
for fold_edit in &mut fold_edits {
let mut delta = 0;
for chunk in old_snapshot
.fold_snapshot
.chunks_at(fold_edit.old_bytes.end)
{
let patterns: &[_] = &['\t', '\n'];
if let Some(ix) = chunk.find(patterns) {
if &chunk[ix..ix + 1] == "\t" {
fold_edit.old_bytes.end.0 += delta + ix + 1;
fold_edit.new_bytes.end.0 += delta + ix + 1;
}
break;
}
delta += chunk.len();
}
}
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_bytes.end >= edit.old_bytes.start {
prev_edit.old_bytes.end = edit.old_bytes.end;
prev_edit.new_bytes.end = edit.new_bytes.end;
fold_edits.remove(ix);
} else {
ix += 1;
}
}
for fold_edit in fold_edits {
let old_start = fold_edit
.old_bytes
.start
.to_point(&old_snapshot.fold_snapshot);
let old_end = fold_edit
.old_bytes
.end
.to_point(&old_snapshot.fold_snapshot);
let new_start = fold_edit
.new_bytes
.start
.to_point(&new_snapshot.fold_snapshot);
let new_end = fold_edit
.new_bytes
.end
.to_point(&new_snapshot.fold_snapshot);
tab_edits.push(Edit {
old_lines: old_snapshot.to_tab_point(old_start)..old_snapshot.to_tab_point(old_end),
new_lines: new_snapshot.to_tab_point(new_start)..new_snapshot.to_tab_point(new_end),
});
}
*old_snapshot = new_snapshot;
(old_snapshot.clone(), tab_edits)
}
}
#[derive(Clone)]
pub struct Snapshot {
pub fold_snapshot: FoldSnapshot,
pub tab_size: usize,
}
impl Snapshot {
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 mut first_line_bytes = 0;
for c in self.chunks_at(range.start).flat_map(|chunk| chunk.chars()) {
if c == '\n'
|| (range.start.row() == range.end.row() && first_line_bytes == range.end.column())
{
break;
}
first_line_chars += 1;
first_line_bytes += c.len_utf8() as u32;
}
let mut last_line_chars = 0;
let mut last_line_bytes = 0;
for c in self
.chunks_at(TabPoint::new(range.end.row(), 0).max(range.start))
.flat_map(|chunk| chunk.chars())
{
if last_line_bytes == range.end.column() {
break;
}
last_line_chars += 1;
last_line_bytes += c.len_utf8() as u32;
}
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 version(&self) -> usize {
self.fold_snapshot.version
}
pub fn chunks_at(&self, point: TabPoint) -> Chunks {
let (point, expanded_char_column, to_next_stop) = self.to_fold_point(point, Bias::Left);
let fold_chunks = self
.fold_snapshot
.chunks_at(point.to_offset(&self.fold_snapshot));
Chunks {
fold_chunks,
column: expanded_char_column,
tab_size: self.tab_size,
chunk: &SPACES[0..to_next_stop],
skip_leading_tab: to_next_stop > 0,
}
}
pub fn highlighted_chunks(&mut self, range: Range<TabPoint>) -> HighlightedChunks {
let (input_start, expanded_char_column, to_next_stop) =
self.to_fold_point(range.start, Bias::Left);
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);
HighlightedChunks {
fold_chunks: self
.fold_snapshot
.highlighted_chunks(input_start..input_end),
column: expanded_char_column,
tab_size: self.tab_size,
chunk: &SPACES[0..to_next_stop],
skip_leading_tab: to_next_stop > 0,
style_id: Default::default(),
}
}
pub fn buffer_rows(&self, row: u32) -> fold_map::BufferRows {
self.fold_snapshot.buffer_rows(row)
}
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks_at(Default::default()).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() as usize, self.tab_size);
TabPoint::new(input.row(), expanded as u32)
}
pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) {
let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0));
let expanded = output.column() as usize;
let (collapsed, expanded_char_column, to_next_stop) =
Self::collapse_tabs(chars, expanded, bias, self.tab_size);
(
FoldPoint::new(output.row(), collapsed as u32),
expanded_char_column,
to_next_stop,
)
}
fn expand_tabs(chars: impl Iterator<Item = char>, column: usize, tab_size: usize) -> usize {
let mut expanded_chars = 0;
let mut expanded_bytes = 0;
let mut collapsed_bytes = 0;
for c in chars {
if collapsed_bytes == 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();
expanded_chars += 1;
}
collapsed_bytes += c.len_utf8();
}
expanded_bytes
}
fn collapse_tabs(
mut chars: impl Iterator<Item = char>,
column: usize,
bias: Bias,
tab_size: usize,
) -> (usize, usize, usize) {
let mut expanded_bytes = 0;
let mut expanded_chars = 0;
let mut collapsed_bytes = 0;
while let Some(c) = chars.next() {
if expanded_bytes >= 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();
}
if expanded_bytes > column && matches!(bias, Bias::Left) {
expanded_chars -= 1;
break;
}
collapsed_bytes += c.len_utf8();
}
(collapsed_bytes, expanded_chars, 0)
}
}
#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)]
pub struct TabPoint(pub super::Point);
impl TabPoint {
pub fn new(row: u32, column: u32) -> Self {
Self(super::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<super::Point> for TabPoint {
fn from(point: super::Point) -> Self {
Self(point)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Edit {
pub old_lines: Range<TabPoint>,
pub new_lines: Range<TabPoint>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct TextSummary {
pub lines: super::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 = rope::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: &'static str = " ";
pub struct Chunks<'a> {
fold_chunks: fold_map::Chunks<'a>,
chunk: &'a str,
column: usize,
tab_size: usize,
skip_leading_tab: bool,
}
impl<'a> Iterator for Chunks<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.is_empty() {
if let Some(chunk) = self.fold_chunks.next() {
self.chunk = chunk;
if self.skip_leading_tab {
self.chunk = &self.chunk[1..];
self.skip_leading_tab = false;
}
} else {
return None;
}
}
for (ix, c) in self.chunk.char_indices() {
match c {
'\t' => {
if ix > 0 {
let (prefix, suffix) = self.chunk.split_at(ix);
self.chunk = suffix;
return Some(prefix);
} else {
self.chunk = &self.chunk[1..];
let len = self.tab_size - self.column % self.tab_size;
self.column += len;
return Some(&SPACES[0..len]);
}
}
'\n' => self.column = 0,
_ => self.column += 1,
}
}
let result = Some(self.chunk);
self.chunk = "";
result
}
}
pub struct HighlightedChunks<'a> {
fold_chunks: fold_map::HighlightedChunks<'a>,
chunk: &'a str,
style_id: HighlightId,
column: usize,
tab_size: usize,
skip_leading_tab: bool,
}
impl<'a> Iterator for HighlightedChunks<'a> {
type Item = (&'a str, HighlightId);
fn next(&mut self) -> Option<Self::Item> {
if self.chunk.is_empty() {
if let Some((chunk, style_id)) = self.fold_chunks.next() {
self.chunk = chunk;
self.style_id = style_id;
if self.skip_leading_tab {
self.chunk = &self.chunk[1..];
self.skip_leading_tab = false;
}
} else {
return None;
}
}
for (ix, c) in self.chunk.char_indices() {
match c {
'\t' => {
if ix > 0 {
let (prefix, suffix) = self.chunk.split_at(ix);
self.chunk = suffix;
return Some((prefix, self.style_id));
} else {
self.chunk = &self.chunk[1..];
let len = self.tab_size - self.column % self.tab_size;
self.column += len;
return Some((&SPACES[0..len], self.style_id));
}
}
'\n' => self.column = 0,
_ => self.column += 1,
}
}
Some((mem::take(&mut self.chunk), mem::take(&mut self.style_id)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_expand_tabs() {
assert_eq!(Snapshot::expand_tabs("\t".chars(), 0, 4), 0);
assert_eq!(Snapshot::expand_tabs("\t".chars(), 1, 4), 4);
assert_eq!(Snapshot::expand_tabs("\ta".chars(), 2, 4), 5);
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,257 +0,0 @@
use super::{Bias, DisplayMapSnapshot, DisplayPoint, SelectionGoal};
use anyhow::Result;
pub fn left(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<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());
}
Ok(map.clip_point(point, Bias::Left))
}
pub fn right(map: &DisplayMapSnapshot, mut point: DisplayPoint) -> Result<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;
}
Ok(map.clip_point(point, Bias::Right))
}
pub fn up(
map: &DisplayMapSnapshot,
mut point: DisplayPoint,
goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> {
let goal_column = if let SelectionGoal::Column(column) = goal {
column
} else {
map.column_to_chars(point.row(), point.column())
};
if point.row() > 0 {
*point.row_mut() -= 1;
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
} else {
point = DisplayPoint::new(0, 0);
}
let clip_bias = if point.column() == map.line_len(point.row()) {
Bias::Left
} else {
Bias::Right
};
Ok((
map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column),
))
}
pub fn down(
map: &DisplayMapSnapshot,
mut point: DisplayPoint,
goal: SelectionGoal,
) -> Result<(DisplayPoint, SelectionGoal)> {
let max_point = map.max_point();
let goal_column = if let SelectionGoal::Column(column) = goal {
column
} else {
map.column_to_chars(point.row(), point.column())
};
if point.row() < max_point.row() {
*point.row_mut() += 1;
*point.column_mut() = map.column_from_chars(point.row(), goal_column);
} else {
point = max_point;
}
let clip_bias = if point.column() == map.line_len(point.row()) {
Bias::Left
} else {
Bias::Right
};
Ok((
map.clip_point(point, clip_bias),
SelectionGoal::Column(goal_column),
))
}
pub fn line_beginning(
map: &DisplayMapSnapshot,
point: DisplayPoint,
toggle_indent: bool,
) -> Result<DisplayPoint> {
let (indent, is_blank) = map.line_indent(point.row());
if toggle_indent && !is_blank && point.column() != indent {
Ok(DisplayPoint::new(point.row(), indent))
} else {
Ok(DisplayPoint::new(point.row(), 0))
}
}
pub fn line_end(map: &DisplayMapSnapshot, point: DisplayPoint) -> Result<DisplayPoint> {
let line_end = DisplayPoint::new(point.row(), map.line_len(point.row()));
Ok(map.clip_point(line_end, Bias::Left))
}
pub fn prev_word_boundary(
map: &DisplayMapSnapshot,
mut point: DisplayPoint,
) -> Result<DisplayPoint> {
let mut line_start = 0;
if point.row() > 0 {
if let Some(indent) = map.soft_wrap_indent(point.row() - 1) {
line_start = indent;
}
}
if point.column() == line_start {
if point.row() == 0 {
return Ok(DisplayPoint::new(0, 0));
} else {
let row = point.row() - 1;
point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left);
}
}
let mut boundary = DisplayPoint::new(point.row(), 0);
let mut column = 0;
let mut prev_char_kind = CharKind::Newline;
for c in map.chars_at(DisplayPoint::new(point.row(), 0)) {
if column >= point.column() {
break;
}
let char_kind = char_kind(c);
if char_kind != prev_char_kind
&& char_kind != CharKind::Whitespace
&& char_kind != CharKind::Newline
{
*boundary.column_mut() = column;
}
prev_char_kind = char_kind;
column += c.len_utf8() as u32;
}
Ok(boundary)
}
pub fn next_word_boundary(
map: &DisplayMapSnapshot,
mut point: DisplayPoint,
) -> Result<DisplayPoint> {
let mut prev_char_kind = None;
for c in map.chars_at(point) {
let char_kind = char_kind(c);
if let Some(prev_char_kind) = prev_char_kind {
if c == '\n' {
break;
}
if prev_char_kind != char_kind
&& prev_char_kind != CharKind::Whitespace
&& prev_char_kind != CharKind::Newline
{
break;
}
}
if c == '\n' {
*point.row_mut() += 1;
*point.column_mut() = 0;
} else {
*point.column_mut() += c.len_utf8() as u32;
}
prev_char_kind = Some(char_kind);
}
Ok(point)
}
#[derive(Copy, Clone, Eq, PartialEq)]
enum CharKind {
Newline,
Whitespace,
Punctuation,
Word,
}
fn char_kind(c: char) -> CharKind {
if c == '\n' {
CharKind::Newline
} else if c.is_whitespace() {
CharKind::Whitespace
} else if c.is_alphanumeric() || c == '_' {
CharKind::Word
} else {
CharKind::Punctuation
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::editor::{display_map::DisplayMap, Buffer};
#[gpui::test]
fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) {
let tab_size = 4;
let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap();
let font_id = cx
.font_cache()
.select_font(family_id, &Default::default())
.unwrap();
let font_size = 14.0;
let buffer = cx.add_model(|cx| Buffer::new(0, "a bcΔ defγ hi—jk", cx));
let display_map =
cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, cx));
let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx));
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)).unwrap(),
DisplayPoint::new(0, 7)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
DisplayPoint::new(0, 2)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
DisplayPoint::new(0, 2)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
DisplayPoint::new(0, 0)
);
assert_eq!(
prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
DisplayPoint::new(0, 0)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 0)).unwrap(),
DisplayPoint::new(0, 1)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 1)).unwrap(),
DisplayPoint::new(0, 6)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 2)).unwrap(),
DisplayPoint::new(0, 6)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 6)).unwrap(),
DisplayPoint::new(0, 12)
);
assert_eq!(
next_word_boundary(&snapshot, DisplayPoint::new(0, 7)).unwrap(),
DisplayPoint::new(0, 12)
);
}
}

View file

@ -1,10 +1,10 @@
use crate::{
editor::{self, Editor},
fuzzy::PathMatch,
project::{Project, ProjectPath},
settings::Settings,
workspace::Workspace,
};
use editor::{self, Editor, EditorSettings};
use gpui::{
action,
elements::*,
@ -271,10 +271,15 @@ impl FileFinder {
let query_editor = cx.add_view(|cx| {
Editor::single_line(
settings.clone(),
{
let settings = settings.clone();
move |_| settings.borrow().theme.selector.input_editor.as_editor()
move |_| {
let settings = settings.borrow();
EditorSettings {
style: settings.theme.selector.input_editor.as_editor(),
tab_size: settings.tab_size,
}
}
},
cx,
)
@ -420,11 +425,8 @@ impl FileFinder {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
editor::{self, Insert},
test::test_app_state,
workspace::Workspace,
};
use crate::{test::test_app_state, workspace::Workspace};
use editor::{self, Insert};
use serde_json::json;
use std::path::PathBuf;
use worktree::fs::FakeFs;

View file

@ -1,7 +1,6 @@
pub mod assets;
pub mod channel;
pub mod chat_panel;
pub mod editor;
pub mod file_finder;
mod fuzzy;
pub mod http;
@ -21,6 +20,7 @@ pub mod workspace;
pub use buffer;
use buffer::LanguageRegistry;
use channel::ChannelList;
pub use editor;
use gpui::{action, keymap::Binding, ModelHandle};
use parking_lot::Mutex;
use postage::watch;

View file

@ -33,7 +33,7 @@ fn main() {
let themes = settings::ThemeRegistry::new(Assets, app.font_cache());
let (settings_tx, settings) = settings::channel(&app.font_cache(), &themes).unwrap();
let languages = Arc::new(language::build_language_registry());
languages.set_theme(&settings.borrow().theme.syntax);
languages.set_theme(&settings.borrow().theme.editor.syntax);
app.run(move |cx| {
let rpc = rpc::Client::new();

View file

@ -4,8 +4,6 @@ use std::sync::Arc;
#[cfg(target_os = "macos")]
pub fn menus(state: &Arc<AppState>) -> Vec<Menu<'static>> {
use crate::editor;
vec![
Menu {
name: "Zed",

View file

@ -10,11 +10,10 @@ use crate::{
use anyhow::Result;
use buffer::LanguageRegistry;
use futures::{future::BoxFuture, Future};
use gpui::{Entity, ModelHandle, MutableAppContext};
use gpui::MutableAppContext;
use parking_lot::Mutex;
use rpc_client as rpc;
use smol::channel;
use std::{fmt, marker::PhantomData, sync::Arc};
use std::{fmt, sync::Arc};
use worktree::fs::FakeFs;
#[cfg(test)]
@ -23,19 +22,6 @@ fn init_logger() {
env_logger::init();
}
pub fn sample_text(rows: usize, cols: usize) -> String {
let mut text = String::new();
for row in 0..rows {
let c: char = ('a' as u32 + row as u32) as u8 as char;
let mut line = c.to_string().repeat(cols);
if row < rows - 1 {
line.push('\n');
}
text += &line;
}
text
}
pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
let (settings_tx, settings) = settings::test(cx);
let mut languages = LanguageRegistry::new();
@ -56,29 +42,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc<AppState> {
})
}
pub struct Observer<T>(PhantomData<T>);
impl<T: 'static> Entity for Observer<T> {
type Event = ();
}
impl<T: Entity> Observer<T> {
pub fn new(
handle: &ModelHandle<T>,
cx: &mut gpui::TestAppContext,
) -> (ModelHandle<Self>, channel::Receiver<()>) {
let (notify_tx, notify_rx) = channel::unbounded();
let observer = cx.add_model(|cx| {
cx.observe(handle, move |_, _, _| {
let _ = notify_tx.try_send(());
})
.detach();
Observer(PhantomData)
});
(observer, notify_rx)
}
}
pub struct FakeHttpClient {
handler:
Box<dyn 'static + Send + Sync + Fn(Request) -> BoxFuture<'static, Result<ServerResponse>>>,

View file

@ -1,8 +1,7 @@
mod resolution;
mod theme_registry;
use crate::editor::{EditorStyle, SelectionStyle};
use buffer::SyntaxTheme;
use editor::{EditorStyle, SelectionStyle};
use gpui::{
color::Color,
elements::{ContainerStyle, ImageStyle, LabelStyle},
@ -25,7 +24,6 @@ pub struct Theme {
pub project_panel: ProjectPanel,
pub selector: Selector,
pub editor: EditorStyle,
pub syntax: SyntaxTheme,
}
#[derive(Deserialize)]
@ -228,6 +226,7 @@ impl InputEditorStyle {
line_number: Default::default(),
line_number_active: Default::default(),
guest_selections: Default::default(),
syntax: Default::default(),
}
}
}

View file

@ -1,12 +1,12 @@
use std::{cmp, sync::Arc};
use crate::{
editor::{self, Editor},
fuzzy::{match_strings, StringMatch, StringMatchCandidate},
settings::ThemeRegistry,
workspace::Workspace,
AppState, Settings,
};
use editor::{self, Editor, EditorSettings};
use gpui::{
action,
elements::*,
@ -59,10 +59,15 @@ impl ThemeSelector {
) -> Self {
let query_editor = cx.add_view(|cx| {
Editor::single_line(
settings.clone(),
{
let settings = settings.clone();
move |_| settings.borrow().theme.selector.input_editor.as_editor()
move |_| {
let settings = settings.borrow();
EditorSettings {
tab_size: settings.tab_size,
style: settings.theme.selector.input_editor.as_editor(),
}
}
},
cx,
)

View file

@ -1,3 +1,4 @@
mod items;
pub mod pane;
pub mod pane_group;
pub mod sidebar;
@ -1156,11 +1157,8 @@ impl WorkspaceHandle for ViewHandle<Workspace> {
#[cfg(test)]
mod tests {
use super::*;
use crate::{
editor::{Editor, Insert},
fs::FakeFs,
test::test_app_state,
};
use crate::{fs::FakeFs, test::test_app_state};
use editor::{Editor, Insert};
use serde_json::json;
use std::collections::HashSet;
use util::test::temp_tree;

View file

@ -0,0 +1,153 @@
use super::{Item, ItemView};
use crate::{project::ProjectPath, Settings};
use anyhow::Result;
use buffer::{Buffer, File as _};
use editor::{Editor, EditorSettings, Event};
use gpui::{fonts::TextStyle, AppContext, ModelHandle, Task, ViewContext};
use postage::watch;
use std::path::Path;
use worktree::Worktree;
impl Item for Buffer {
type View = Editor;
fn build_view(
handle: ModelHandle<Self>,
settings: watch::Receiver<Settings>,
cx: &mut ViewContext<Self::View>,
) -> Self::View {
Editor::for_buffer(
handle,
move |cx| {
let settings = settings.borrow();
let font_cache = cx.font_cache();
let font_family_id = settings.buffer_font_family;
let font_family_name = cx.font_cache().family_name(font_family_id).unwrap();
let font_properties = Default::default();
let font_id = font_cache
.select_font(font_family_id, &font_properties)
.unwrap();
let font_size = settings.buffer_font_size;
let mut theme = settings.theme.editor.clone();
theme.text = TextStyle {
color: theme.text.color,
font_family_name,
font_family_id,
font_id,
font_size,
font_properties,
underline: false,
};
EditorSettings {
tab_size: settings.tab_size,
style: theme,
}
},
cx,
)
}
fn project_path(&self) -> Option<ProjectPath> {
self.file().map(|f| ProjectPath {
worktree_id: f.worktree_id(),
path: f.path().clone(),
})
}
}
impl ItemView for Editor {
fn should_activate_item_on_event(event: &Event) -> bool {
matches!(event, Event::Activate)
}
fn should_close_item_on_event(event: &Event) -> bool {
matches!(event, Event::Closed)
}
fn should_update_tab_on_event(event: &Event) -> bool {
matches!(
event,
Event::Saved | Event::Dirtied | Event::FileHandleChanged
)
}
fn title(&self, cx: &AppContext) -> String {
let filename = self
.buffer()
.read(cx)
.file()
.and_then(|file| file.file_name(cx));
if let Some(name) = filename {
name.to_string_lossy().into()
} else {
"untitled".into()
}
}
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
self.buffer().read(cx).file().map(|file| ProjectPath {
worktree_id: file.worktree_id(),
path: file.path().clone(),
})
}
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
where
Self: Sized,
{
Some(self.clone(cx))
}
fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
let save = self.buffer().update(cx, |b, cx| b.save(cx))?;
Ok(cx.spawn(|_, _| async move {
save.await?;
Ok(())
}))
}
fn save_as(
&mut self,
worktree: ModelHandle<Worktree>,
path: &Path,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
self.buffer().update(cx, |buffer, cx| {
let handle = cx.handle();
let text = buffer.as_rope().clone();
let version = buffer.version();
let save_as = worktree.update(cx, |worktree, cx| {
worktree
.as_local_mut()
.unwrap()
.save_buffer_as(handle, path, text, cx)
});
cx.spawn(|buffer, mut cx| async move {
save_as.await.map(|new_file| {
let language = worktree.read_with(&cx, |worktree, cx| {
worktree
.languages()
.select_language(new_file.full_path(cx))
.cloned()
});
buffer.update(&mut cx, |buffer, cx| {
buffer.did_save(version, new_file.mtime, Some(Box::new(new_file)), cx);
buffer.set_language(language, cx);
});
})
})
})
}
fn is_dirty(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).is_dirty()
}
fn has_conflict(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).has_conflict()
}
}