From 0a28c78a7a5ce13999fa04b1add211dc3f8bf404 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 29 Apr 2021 14:54:50 +0200 Subject: [PATCH 1/5] Implement `move_to_next_word_boundary` for buffer Co-Authored-By: Nathan Sobo --- zed/src/editor/buffer_view.rs | 27 ++++++++++++++++++++++++ zed/src/editor/movement.rs | 39 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index c0b320cfac568a70af20476ba05d0b5375f9160e..f5aa563ccde07bdf93ea5cf5f4d277094f34ec71 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -54,6 +54,11 @@ pub fn init(app: &mut MutableAppContext) { Binding::new("down", "buffer:move_down", Some("BufferView")), Binding::new("left", "buffer:move_left", Some("BufferView")), Binding::new("right", "buffer:move_right", Some("BufferView")), + Binding::new( + "alt-right", + "buffer:move_to_next_word_boundary", + Some("BufferView"), + ), Binding::new( "cmd-left", "buffer:move_to_beginning_of_line", @@ -141,6 +146,10 @@ pub fn init(app: &mut MutableAppContext) { app.add_action("buffer:move_down", BufferView::move_down); app.add_action("buffer:move_left", BufferView::move_left); app.add_action("buffer:move_right", BufferView::move_right); + app.add_action( + "buffer:move_to_next_word_boundary", + BufferView::move_to_next_word_boundary, + ); app.add_action( "buffer:move_to_beginning_of_line", BufferView::move_to_beginning_of_line, @@ -1085,6 +1094,24 @@ impl BufferView { self.update_selections(selections, true, ctx); } + pub fn move_to_next_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { + let app = ctx.as_ref(); + let mut selections = self.selections(app).to_vec(); + { + let map = self.display_map.read(app); + for selection in &mut selections { + let head = selection.head().to_display_point(map, app).unwrap(); + let new_head = movement::next_word_boundary(map, head, app).unwrap(); + let anchor = map.anchor_before(new_head, Bias::Left, app).unwrap(); + selection.start = anchor.clone(); + selection.end = anchor; + selection.reversed = false; + selection.goal_column = None; + } + } + self.update_selections(selections, true, ctx); + } + pub fn move_to_beginning_of_line(&mut self, _: &(), ctx: &mut ViewContext) { let app = ctx.as_ref(); let mut selections = self.selections(app).to_vec(); diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index 44e981f71b0bbddbe413cfe215057cbcb66363f4..040e59705dc40070269ca9ca26f8c9b753ddb593 100644 --- a/zed/src/editor/movement.rs +++ b/zed/src/editor/movement.rs @@ -79,3 +79,42 @@ pub fn line_end(map: &DisplayMap, point: DisplayPoint, app: &AppContext) -> Resu map.line_len(point.row(), app)?, )) } + +pub fn prev_word_boundary( + map: &DisplayMap, + point: DisplayPoint, + app: &AppContext, +) -> Result { + todo!() +} + +pub fn next_word_boundary( + map: &DisplayMap, + mut point: DisplayPoint, + app: &AppContext, +) -> Result { + let mut prev_c = None; + for c in map.chars_at(point, app)? { + if prev_c.is_some() && (c == '\n' || is_word_char(prev_c.unwrap()) != is_word_char(c)) { + break; + } + + if c == '\n' { + *point.row_mut() += 1; + *point.column_mut() = 0; + } else { + *point.column_mut() += 1; + } + prev_c = Some(c); + } + Ok(point) +} + +fn is_word_char(c: char) -> bool { + match c { + '/' | '\\' | '(' | ')' | '"' | '\'' | ':' | ',' | '.' | ';' | '<' | '>' | '~' | '!' + | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '|' | '+' | '=' | '[' | ']' | '{' | '}' + | '`' | '?' | '-' | '…' | ' ' | '\n' => false, + _ => true, + } +} From bc686b45615a67a9ea7395105641b684df250260 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 29 Apr 2021 18:52:11 +0200 Subject: [PATCH 2/5] Implement `move_to_previous_word_boundary` Co-Authored-By: Max Brunsfeld --- zed/src/editor/buffer_view.rs | 27 ++++++++++++++++++++ zed/src/editor/movement.rs | 48 +++++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 8 deletions(-) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index f5aa563ccde07bdf93ea5cf5f4d277094f34ec71..985dce20d2213bc8f7952db44e666acd2bbedd8a 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -54,6 +54,11 @@ pub fn init(app: &mut MutableAppContext) { Binding::new("down", "buffer:move_down", Some("BufferView")), Binding::new("left", "buffer:move_left", Some("BufferView")), Binding::new("right", "buffer:move_right", Some("BufferView")), + Binding::new( + "alt-left", + "buffer:move_to_previous_word_boundary", + Some("BufferView"), + ), Binding::new( "alt-right", "buffer:move_to_next_word_boundary", @@ -146,6 +151,10 @@ pub fn init(app: &mut MutableAppContext) { app.add_action("buffer:move_down", BufferView::move_down); app.add_action("buffer:move_left", BufferView::move_left); app.add_action("buffer:move_right", BufferView::move_right); + app.add_action( + "buffer:move_to_previous_word_boundary", + BufferView::move_to_previous_word_boundary, + ); app.add_action( "buffer:move_to_next_word_boundary", BufferView::move_to_next_word_boundary, @@ -1094,6 +1103,24 @@ impl BufferView { self.update_selections(selections, true, ctx); } + pub fn move_to_previous_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { + let app = ctx.as_ref(); + let mut selections = self.selections(app).to_vec(); + { + let map = self.display_map.read(app); + for selection in &mut selections { + let head = selection.head().to_display_point(map, app).unwrap(); + let new_head = movement::prev_word_boundary(map, head, app).unwrap(); + let anchor = map.anchor_before(new_head, Bias::Left, app).unwrap(); + selection.start = anchor.clone(); + selection.end = anchor; + selection.reversed = false; + selection.goal_column = None; + } + } + self.update_selections(selections, true, ctx); + } + pub fn move_to_next_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { let app = ctx.as_ref(); let mut selections = self.selections(app).to_vec(); diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index 040e59705dc40070269ca9ca26f8c9b753ddb593..5df463f43b2393862887722f62128bd450cfea1b 100644 --- a/zed/src/editor/movement.rs +++ b/zed/src/editor/movement.rs @@ -85,7 +85,31 @@ pub fn prev_word_boundary( point: DisplayPoint, app: &AppContext, ) -> Result { - todo!() + if point.column() == 0 { + if point.row() == 0 { + Ok(DisplayPoint::new(0, 0)) + } else { + let row = point.row() - 1; + Ok(DisplayPoint::new(row, map.line_len(row, app)?)) + } + } else { + let mut boundary = DisplayPoint::new(point.row(), 0); + let mut column = 0; + let mut prev_c = None; + for c in map.chars_at(boundary, app)? { + if column >= point.column() { + break; + } + + if prev_c.is_none() || char_kind(prev_c.unwrap()) != char_kind(c) { + *boundary.column_mut() = column; + } + + prev_c = Some(c); + column += 1; + } + Ok(boundary) + } } pub fn next_word_boundary( @@ -95,7 +119,7 @@ pub fn next_word_boundary( ) -> Result { let mut prev_c = None; for c in map.chars_at(point, app)? { - if prev_c.is_some() && (c == '\n' || is_word_char(prev_c.unwrap()) != is_word_char(c)) { + if prev_c.is_some() && (c == '\n' || char_kind(prev_c.unwrap()) != char_kind(c)) { break; } @@ -110,11 +134,19 @@ pub fn next_word_boundary( Ok(point) } -fn is_word_char(c: char) -> bool { - match c { - '/' | '\\' | '(' | ')' | '"' | '\'' | ':' | ',' | '.' | ';' | '<' | '>' | '~' | '!' - | '@' | '#' | '$' | '%' | '^' | '&' | '*' | '|' | '+' | '=' | '[' | ']' | '{' | '}' - | '`' | '?' | '-' | '…' | ' ' | '\n' => false, - _ => true, +#[derive(Copy, Clone, Eq, PartialEq)] +enum CharKind { + Whitespace, + Punctuation, + Word, +} + +fn char_kind(c: char) -> CharKind { + if c.is_whitespace() { + CharKind::Whitespace + } else if c.is_alphanumeric() || c == '_' { + CharKind::Word + } else { + CharKind::Punctuation } } From 1a0dbb2907af376d89b0ab8836ae4474e3125a4d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Apr 2021 09:41:38 +0200 Subject: [PATCH 3/5] Implement `select_to_{previous,next}_word_boundary` --- zed/src/editor/buffer_view.rs | 52 +++++++++++++++++++++++++++++++++++ zed/src/editor/movement.rs | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 985dce20d2213bc8f7952db44e666acd2bbedd8a..3ba8ceb3f1cfac14b120695602253f8878a6385b 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -86,6 +86,16 @@ pub fn init(app: &mut MutableAppContext) { Binding::new("shift-down", "buffer:select_down", Some("BufferView")), Binding::new("shift-left", "buffer:select_left", Some("BufferView")), Binding::new("shift-right", "buffer:select_right", Some("BufferView")), + Binding::new( + "alt-shift-left", + "buffer:select_to_previous_word_boundary", + Some("BufferView"), + ), + Binding::new( + "alt-shift-right", + "buffer:select_to_next_word_boundary", + Some("BufferView"), + ), Binding::new( "cmd-shift-left", "buffer:select_to_beginning_of_line", @@ -173,6 +183,14 @@ pub fn init(app: &mut MutableAppContext) { app.add_action("buffer:select_down", BufferView::select_down); app.add_action("buffer:select_left", BufferView::select_left); app.add_action("buffer:select_right", BufferView::select_right); + app.add_action( + "buffer:select_to_previous_word_boundary", + BufferView::select_to_previous_word_boundary, + ); + app.add_action( + "buffer:select_to_next_word_boundary", + BufferView::select_to_next_word_boundary, + ); app.add_action( "buffer:select_to_beginning_of_line", BufferView::select_to_beginning_of_line, @@ -1121,6 +1139,23 @@ impl BufferView { self.update_selections(selections, true, ctx); } + pub fn select_to_previous_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { + let app = ctx.as_ref(); + let mut selections = self.selections(app).to_vec(); + { + let buffer = self.buffer.read(ctx); + let map = self.display_map.read(app); + for selection in &mut selections { + let head = selection.head().to_display_point(map, app).unwrap(); + let new_head = movement::prev_word_boundary(map, head, app).unwrap(); + let anchor = map.anchor_before(new_head, Bias::Left, app).unwrap(); + selection.set_head(buffer, anchor); + selection.goal_column = None; + } + } + self.update_selections(selections, true, ctx); + } + pub fn move_to_next_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { let app = ctx.as_ref(); let mut selections = self.selections(app).to_vec(); @@ -1139,6 +1174,23 @@ impl BufferView { self.update_selections(selections, true, ctx); } + pub fn select_to_next_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { + let app = ctx.as_ref(); + let mut selections = self.selections(app).to_vec(); + { + let buffer = self.buffer.read(ctx); + let map = self.display_map.read(app); + for selection in &mut selections { + let head = selection.head().to_display_point(map, app).unwrap(); + let new_head = movement::next_word_boundary(map, head, app).unwrap(); + let anchor = map.anchor_before(new_head, Bias::Left, app).unwrap(); + selection.set_head(buffer, anchor); + selection.goal_column = None; + } + } + self.update_selections(selections, true, ctx); + } + pub fn move_to_beginning_of_line(&mut self, _: &(), ctx: &mut ViewContext) { let app = ctx.as_ref(); let mut selections = self.selections(app).to_vec(); diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index 5df463f43b2393862887722f62128bd450cfea1b..f4f0b0da4409c2105c55503f55be9aa731dbca21 100644 --- a/zed/src/editor/movement.rs +++ b/zed/src/editor/movement.rs @@ -119,7 +119,7 @@ pub fn next_word_boundary( ) -> Result { let mut prev_c = None; for c in map.chars_at(point, app)? { - if prev_c.is_some() && (c == '\n' || char_kind(prev_c.unwrap()) != char_kind(c)) { + if prev_c.is_some() && char_kind(prev_c.unwrap()) != char_kind(c) { break; } From f352cfb6865553adc12031a59c8727ae0c9637e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Apr 2021 09:45:22 +0200 Subject: [PATCH 4/5] Implement `delete_to_{previous,next}_word_boundary` --- zed/src/editor/buffer_view.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 3ba8ceb3f1cfac14b120695602253f8878a6385b..99c57e41f86674d6441bd4aeeb7ff68e634e24e5 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -34,6 +34,16 @@ pub fn init(app: &mut MutableAppContext) { Binding::new("ctrl-d", "buffer:delete", Some("BufferView")), Binding::new("enter", "buffer:newline", Some("BufferView")), Binding::new("ctrl-shift-K", "buffer:delete_line", Some("BufferView")), + Binding::new( + "alt-backspace", + "buffer:delete_to_previous_word_boundary", + Some("BufferView"), + ), + Binding::new( + "alt-delete", + "buffer:delete_to_next_word_boundary", + Some("BufferView"), + ), Binding::new( "cmd-backspace", "buffer:delete_to_beginning_of_line", @@ -143,6 +153,14 @@ pub fn init(app: &mut MutableAppContext) { app.add_action("buffer:backspace", BufferView::backspace); app.add_action("buffer:delete", BufferView::delete); app.add_action("buffer:delete_line", BufferView::delete_line); + app.add_action( + "buffer:delete_to_previous_word_boundary", + BufferView::delete_to_previous_word_boundary, + ); + app.add_action( + "buffer:delete_to_next_word_boundary", + BufferView::delete_to_next_word_boundary, + ); app.add_action( "buffer:delete_to_beginning_of_line", BufferView::delete_to_beginning_of_line, @@ -1156,6 +1174,13 @@ impl BufferView { self.update_selections(selections, true, ctx); } + pub fn delete_to_previous_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { + self.start_transaction(ctx); + self.select_to_previous_word_boundary(&(), ctx); + self.backspace(&(), ctx); + self.end_transaction(ctx); + } + pub fn move_to_next_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { let app = ctx.as_ref(); let mut selections = self.selections(app).to_vec(); @@ -1191,6 +1216,13 @@ impl BufferView { self.update_selections(selections, true, ctx); } + pub fn delete_to_next_word_boundary(&mut self, _: &(), ctx: &mut ViewContext) { + self.start_transaction(ctx); + self.select_to_next_word_boundary(&(), ctx); + self.delete(&(), ctx); + self.end_transaction(ctx); + } + pub fn move_to_beginning_of_line(&mut self, _: &(), ctx: &mut ViewContext) { let app = ctx.as_ref(); let mut selections = self.selections(app).to_vec(); From 9c3216507b254ed934b9854a8dce6bd5891ed63a Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 30 Apr 2021 10:13:45 +0200 Subject: [PATCH 5/5] Add test for word boundary movement/selection/deletion --- zed/src/editor/buffer_view.rs | 182 ++++++++++++++++++++++++++++++++++ zed/src/editor/movement.rs | 7 +- 2 files changed, 187 insertions(+), 2 deletions(-) diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 99c57e41f86674d6441bd4aeeb7ff68e634e24e5..f6217aa428d8237ccabd522e37744a50a60cc734 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -2357,6 +2357,188 @@ mod tests { }); } + #[test] + fn test_prev_next_word_boundary() { + App::test((), |app| { + let buffer = app + .add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx)); + let settings = settings::channel(&app.font_cache()).unwrap().1; + let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx)); + view.update(app, |view, ctx| { + view.select_display_ranges( + &[ + DisplayPoint::new(0, 11)..DisplayPoint::new(0, 11), + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + ], + ctx, + ) + .unwrap(); + }); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + + view.update(app, |view, ctx| { + view.move_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), + DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| view.move_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.move_right(&(), ctx); + view.select_to_previous_word_boundary(&(), ctx); + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| { + view.select_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 0), + ] + ); + + view.update(app, |view, ctx| view.select_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 2), + ] + ); + + view.update(app, |view, ctx| view.delete_to_next_word_boundary(&(), ctx)); + assert_eq!( + view.read(app).text(app.as_ref()), + "use std::s::{foo, bar}\n\n {az.qux()}" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 10)..DisplayPoint::new(0, 10), + DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), + ] + ); + + view.update(app, |view, ctx| { + view.delete_to_previous_word_boundary(&(), ctx) + }); + assert_eq!( + view.read(app).text(app.as_ref()), + "use std::::{foo, bar}\n\n az.qux()}" + ); + assert_eq!( + view.read(app).selection_ranges(app.as_ref()), + &[ + DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), + DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), + ] + ); + }); + } + #[test] fn test_backspace() { App::test((), |app| { diff --git a/zed/src/editor/movement.rs b/zed/src/editor/movement.rs index f4f0b0da4409c2105c55503f55be9aa731dbca21..f9ba5fe33d4189fef21a66f1284f2c5d9f133bb4 100644 --- a/zed/src/editor/movement.rs +++ b/zed/src/editor/movement.rs @@ -119,7 +119,7 @@ pub fn next_word_boundary( ) -> Result { let mut prev_c = None; for c in map.chars_at(point, app)? { - if prev_c.is_some() && char_kind(prev_c.unwrap()) != char_kind(c) { + if prev_c.is_some() && (c == '\n' || char_kind(prev_c.unwrap()) != char_kind(c)) { break; } @@ -136,13 +136,16 @@ pub fn next_word_boundary( #[derive(Copy, Clone, Eq, PartialEq)] enum CharKind { + Newline, Whitespace, Punctuation, Word, } fn char_kind(c: char) -> CharKind { - if c.is_whitespace() { + if c == '\n' { + CharKind::Newline + } else if c.is_whitespace() { CharKind::Whitespace } else if c.is_alphanumeric() || c == '_' { CharKind::Word