From fc78fbeb3e5e47505d1d1d77f29eb57186cacab2 Mon Sep 17 00:00:00 2001 From: Nathan Vegdahl Date: Fri, 2 Jan 2015 15:05:21 -0800 Subject: [PATCH] Non-uniform-width characters are now properly handled (modulo bugs). --- src/buffer/line.rs | 78 ++++++++++++++++++ src/buffer/mod.rs | 19 +++++ src/editor.rs | 196 ++++++++++++++++++++++++++++++++------------- src/term_ui.rs | 14 ++-- todo.md | 3 - 5 files changed, 243 insertions(+), 67 deletions(-) diff --git a/src/buffer/line.rs b/src/buffer/line.rs index 13fe5ff..69ec5e0 100644 --- a/src/buffer/line.rs +++ b/src/buffer/line.rs @@ -245,6 +245,56 @@ impl Line { } + /// Translates a grapheme index into a visual horizontal position + pub fn grapheme_index_to_closest_vis_pos(&self, index: uint) -> uint { + let mut pos = 0; + let mut iter = self.as_str().graphemes(true); + + for _ in range(0, index) { + if let Some(g) = iter.next() { + let w = grapheme_vis_width_at_vis_pos(g, pos); + pos += w; + } + else { + panic!("Line::grapheme_index_to_vis_pos(): index past end of line."); + } + } + + return pos; + } + + + /// Translates a visual horizontal position to the closest grapheme index + pub fn vis_pos_to_closest_grapheme_index(&self, vis_pos: uint) -> uint { + let mut pos = 0; + let mut i = 0; + let mut iter = self.as_str().graphemes(true); + + while pos < vis_pos { + if let Some(g) = iter.next() { + let w = grapheme_vis_width_at_vis_pos(g, pos); + if (w + pos) > vis_pos { + let d1 = vis_pos - pos; + let d2 = (pos + w) - vis_pos; + if d2 < d1 { + i += 1; + } + break; + } + else { + pos += w; + i += 1; + } + } + else { + break; + } + } + + return i; + } + + /// Returns an immutable string slice into the text block's memory pub fn as_str<'a>(&'a self) -> &'a str { unsafe { @@ -828,6 +878,34 @@ fn text_line_split_beginning() { assert!(tl2.ending == LineEnding::CRLF); } +#[test] +fn grapheme_index_to_closest_vis_pos_1() { + let tl = Line::new_from_str("Hello!"); + + assert!(tl.grapheme_index_to_closest_vis_pos(0) == 0); +} + +#[test] +fn grapheme_index_to_closest_vis_pos_2() { + let tl = Line::new_from_str("\tHello!"); + + assert!(tl.grapheme_index_to_closest_vis_pos(1) == TAB_WIDTH); +} + +#[test] +fn vis_pos_to_closest_grapheme_index_1() { + let tl = Line::new_from_str("Hello!"); + + assert!(tl.vis_pos_to_closest_grapheme_index(0) == 0); +} + +#[test] +fn vis_pos_to_closest_grapheme_index_2() { + let tl = Line::new_from_str("\tHello!"); + + assert!(tl.vis_pos_to_closest_grapheme_index(TAB_WIDTH) == 1); +} + //========================================================================= // LineGraphemeIter tests diff --git a/src/buffer/mod.rs b/src/buffer/mod.rs index 238a242..fd631e4 100644 --- a/src/buffer/mod.rs +++ b/src/buffer/mod.rs @@ -89,10 +89,29 @@ impl Buffer { return self.root.pos_2d_to_closest_1d_recursive(pos); } + + pub fn pos_vis_2d_to_closest_1d(&self, pos: (uint, uint)) -> uint { + if pos.0 >= self.line_count() { + return self.len(); + } + else { + let gs = self.pos_2d_to_closest_1d((pos.0, 0)); + let h = self.get_line(pos.0).vis_pos_to_closest_grapheme_index(pos.1); + return gs + h; + } + } + pub fn pos_1d_to_closest_2d(&self, pos: uint) -> (uint, uint) { return self.root.pos_1d_to_closest_2d_recursive(pos); } + + + pub fn pos_1d_to_closest_vis_2d(&self, pos: uint) -> (uint, uint) { + let (v, h) = self.root.pos_1d_to_closest_2d_recursive(pos); + let vis_h = self.get_line(v).grapheme_index_to_closest_vis_pos(h); + return (v, vis_h); + } /// Insert 'text' at grapheme position 'pos'. diff --git a/src/editor.rs b/src/editor.rs index 59cc415..2fc1d1c 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -6,6 +6,32 @@ use files::{load_file_to_buffer, save_buffer_to_file}; use string_utils::grapheme_count; +/// A text cursor. Also represents selections when range.0 != range.1. +/// +/// `range` is a pair of 1d grapheme indexes into the text. +/// +/// `vis_start` is the visual 2d horizontal position of the cursor. This +/// doesn't affect editing operations at all, but is used for cursor movement. +pub struct Cursor { + pub range: (uint, uint), // start, end + pub vis_start: uint, // start +} + +impl Cursor { + pub fn new() -> Cursor { + Cursor { + range: (0, 0), + vis_start: 0, + } + } + + pub fn update_vis_start(&mut self, buf: &Buffer) { + let (v, h) = buf.pos_1d_to_closest_2d(self.range.0); + self.vis_start = buf.get_line(v).grapheme_index_to_closest_vis_pos(h); + } +} + + pub struct Editor { pub buffer: Buffer, pub file_path: Path, @@ -16,7 +42,7 @@ pub struct Editor { pub view_pos: (uint, uint), // (line, col) // The editing cursor position - pub cursor: (uint, uint), // (line, col) + pub cursor: Cursor, } @@ -29,7 +55,7 @@ impl Editor { dirty: false, view_dim: (0, 0), view_pos: (0, 0), - cursor: (0, 0), + cursor: Cursor::new(), } } @@ -42,7 +68,7 @@ impl Editor { dirty: false, view_dim: (0, 0), view_pos: (0, 0), - cursor: (0, 0), + cursor: Cursor::new(), } } @@ -60,35 +86,38 @@ impl Editor { /// Moves the editor's view the minimum amount to show the cursor pub fn move_view_to_cursor(&mut self) { + let (v, h) = self.buffer.pos_1d_to_closest_vis_2d(self.cursor.range.0); + // Horizontal - if self.cursor.1 < self.view_pos.1 { - self.view_pos.1 = self.cursor.1; + if h < self.view_pos.1 { + self.view_pos.1 = h; } - else if self.cursor.1 >= (self.view_pos.1 + self.view_dim.1) { - self.view_pos.1 = 1 + self.cursor.1 - self.view_dim.1; + else if h >= (self.view_pos.1 + self.view_dim.1) { + self.view_pos.1 = 1 + h - self.view_dim.1; } // Vertical - if self.cursor.0 < self.view_pos.0 { - self.view_pos.0 = self.cursor.0; + if v < self.view_pos.0 { + self.view_pos.0 = v; } - else if self.cursor.0 >= (self.view_pos.0 + self.view_dim.0) { - self.view_pos.0 = 1 + self.cursor.0 - self.view_dim.0; + else if v >= (self.view_pos.0 + self.view_dim.0) { + self.view_pos.0 = 1 + v - self.view_dim.0; } } pub fn insert_text_at_cursor(&mut self, text: &str) { - let pos = self.buffer.pos_2d_to_closest_1d(self.cursor); let str_len = grapheme_count(text); - let p = self.buffer.pos_2d_to_closest_1d(self.cursor); // Insert text - self.buffer.insert_text(text, pos); + self.buffer.insert_text(text, self.cursor.range.0); self.dirty = true; // Move cursor - self.cursor = self.buffer.pos_1d_to_closest_2d(p + str_len); + self.cursor.range.0 += str_len; + self.cursor.range.1 += str_len; + self.cursor.update_vis_start(&(self.buffer)); + // Adjust view self.move_view_to_cursor(); } @@ -99,67 +128,127 @@ impl Editor { } pub fn remove_text_behind_cursor(&mut self, grapheme_count: uint) { - let pos_b = self.buffer.pos_2d_to_closest_1d(self.cursor); + let pos_b = self.cursor.range.0; let pos_a = if pos_b >= grapheme_count {pos_b - grapheme_count} else {0}; - - // Move cursor - self.cursor = self.buffer.pos_1d_to_closest_2d(pos_a); + let tot_g = pos_b - pos_a; // Remove text self.buffer.remove_text(pos_a, pos_b); - self.dirty = true; + // Move cursor + self.cursor.range.0 -= tot_g; + self.cursor.range.1 -= tot_g; + self.cursor.update_vis_start(&(self.buffer)); + + // Adjust view + self.move_view_to_cursor(); + } + + pub fn remove_text_in_front_of_cursor(&mut self, grapheme_count: uint) { + let pos_a = self.cursor.range.1; + let pos_b = if (pos_a + grapheme_count) <= self.buffer.len() {pos_a + grapheme_count} else {self.buffer.len()}; + + // Remove text + self.buffer.remove_text(pos_a, pos_b); + self.dirty = true; + + // Move cursor + self.cursor.update_vis_start(&(self.buffer)); + + // Adjust view + self.move_view_to_cursor(); + } + + pub fn remove_text_inside_cursor(&mut self) { + // If selection, remove text + if self.cursor.range.0 < self.cursor.range.1 { + self.buffer.remove_text(self.cursor.range.0, self.cursor.range.1); + self.dirty = true; + } + + // Move cursor + self.cursor.range.1 = self.cursor.range.0; + self.cursor.update_vis_start(&(self.buffer)); + + // Adjust view self.move_view_to_cursor(); } pub fn cursor_to_beginning_of_buffer(&mut self) { - self.cursor = (0, 0); + self.cursor.range = (0, 0); + self.cursor.update_vis_start(&(self.buffer)); + + // Adjust view + self.move_view_to_cursor(); } pub fn cursor_to_end_of_buffer(&mut self) { - self.cursor = self.buffer.pos_1d_to_closest_2d(self.buffer.len()+1); + let end = self.buffer.len(); + self.cursor.range = (end, end); + self.cursor.update_vis_start(&(self.buffer)); + + // Adjust view + self.move_view_to_cursor(); } - pub fn cursor_left(&mut self) { - let p = self.buffer.pos_2d_to_closest_1d(self.cursor); - - if p > 0 { - self.cursor = self.buffer.pos_1d_to_closest_2d(p - 1); + pub fn cursor_left(&mut self, n: uint) { + if self.cursor.range.0 >= n { + self.cursor.range.0 -= n; } else { - self.cursor = self.buffer.pos_1d_to_closest_2d(0); + self.cursor.range.0 = 0; } - self.move_view_to_cursor(); - } - - pub fn cursor_right(&mut self) { - let p = self.buffer.pos_2d_to_closest_1d(self.cursor); - self.cursor = self.buffer.pos_1d_to_closest_2d(p + 1); + self.cursor.range.1 = self.cursor.range.0; + self.cursor.update_vis_start(&(self.buffer)); + // Adjust view self.move_view_to_cursor(); } - pub fn cursor_up(&mut self) { - if self.cursor.0 > 0 { - self.cursor.0 -= 1; + pub fn cursor_right(&mut self, n: uint) { + if self.cursor.range.1 <= (self.buffer.len() - n) { + self.cursor.range.1 += n; + } + else { + self.cursor.range.1 = self.buffer.len(); + } + + self.cursor.range.0 = self.cursor.range.1; + self.cursor.update_vis_start(&(self.buffer)); + + // Adjust view + self.move_view_to_cursor(); + } + + pub fn cursor_up(&mut self, n: uint) { + let (v, _) = self.buffer.pos_1d_to_closest_vis_2d(self.cursor.range.0); + + if v >= n { + self.cursor.range.0 = self.buffer.pos_vis_2d_to_closest_1d((v - n, self.cursor.vis_start)); + self.cursor.range.1 = self.cursor.range.0; } else { self.cursor_to_beginning_of_buffer(); } + // Adjust view self.move_view_to_cursor(); } - pub fn cursor_down(&mut self) { - if self.cursor.0 < (self.buffer.line_count() - 1) { - self.cursor.0 += 1; + pub fn cursor_down(&mut self, n: uint) { + let (v, _) = self.buffer.pos_1d_to_closest_vis_2d(self.cursor.range.0); + + if v < (self.buffer.line_count() - n) { + self.cursor.range.0 = self.buffer.pos_vis_2d_to_closest_1d((v + n, self.cursor.vis_start)); + self.cursor.range.1 = self.cursor.range.0; } else { self.cursor_to_end_of_buffer(); } + // Adjust view self.move_view_to_cursor(); } @@ -167,35 +256,29 @@ impl Editor { if self.view_pos.0 > 0 { let move_amount = self.view_dim.0 - (self.view_dim.0 / 8); if self.view_pos.0 >= move_amount { - if self.cursor.0 >= move_amount { - self.cursor.0 -= move_amount; - } self.view_pos.0 -= move_amount; } else { - if self.cursor.0 >= self.view_pos.0 { - self.cursor.0 -= self.view_pos.0; - } - else { - self.cursor_to_beginning_of_buffer(); - } self.view_pos.0 = 0; - } + } + + self.cursor_up(move_amount); } else { self.cursor_to_beginning_of_buffer(); } + // Adjust view self.move_view_to_cursor(); } pub fn page_down(&mut self) { + // TODO let nlc = self.buffer.line_count() - 1; if self.view_pos.0 < nlc { let move_amount = self.view_dim.0 - (self.view_dim.0 / 8); let max_move = nlc - self.view_pos.0; - let cursor_max_move = nlc - self.cursor.0; if max_move >= move_amount { self.view_pos.0 += move_amount; @@ -204,14 +287,13 @@ impl Editor { self.view_pos.0 += max_move; } - if cursor_max_move >= move_amount { - self.cursor.0 += move_amount; - } - else { - self.cursor_to_end_of_buffer(); - } + self.cursor_down(move_amount); + } + else { + self.cursor_to_end_of_buffer(); } + // Adjust view self.move_view_to_cursor(); } } diff --git a/src/term_ui.rs b/src/term_ui.rs index af8f4c1..e828ee7 100644 --- a/src/term_ui.rs +++ b/src/term_ui.rs @@ -87,19 +87,19 @@ impl TermUI { }, K_UP => { - self.editor.cursor_up(); + self.editor.cursor_up(1); }, K_DOWN => { - self.editor.cursor_down(); + self.editor.cursor_down(1); }, K_LEFT => { - self.editor.cursor_left(); + self.editor.cursor_left(1); }, K_RIGHT => { - self.editor.cursor_right(); + self.editor.cursor_right(1); }, K_ENTER => { @@ -151,6 +151,7 @@ impl TermUI { } } + pub fn draw_editor(&self, editor: &Editor, c1: (uint, uint), c2: (uint, uint)) { let mut line_iter = editor.buffer.line_iter_at_index(editor.view_pos.0); @@ -163,8 +164,7 @@ impl TermUI { let max_print_line = c2.0 - c1.0; let max_print_col = c2.1 - c1.1; - let cursor_pos_1d = editor.buffer.pos_2d_to_closest_1d(editor.cursor); - let cursor_pos = editor.buffer.pos_1d_to_closest_2d(cursor_pos_1d); + let cursor_pos = editor.buffer.pos_1d_to_closest_2d(editor.cursor.range.0); let print_cursor_pos = (cursor_pos.0 + editor.view_pos.0, cursor_pos.1 + editor.view_pos.1); loop { @@ -207,7 +207,7 @@ impl TermUI { } } else if print_cursor_pos.0 >= c1.0 && print_cursor_pos.0 < c2.0 && print_cursor_pos.1 >= c1.1 && print_cursor_pos.1 < c2.1 { - if cursor_pos_1d >= editor.buffer.len() { + if editor.cursor.range.0 >= editor.buffer.len() { self.rb.print(print_cursor_pos.1, print_cursor_pos.0, rustbox::RB_NORMAL, Color::Black, Color::White, " "); } break; diff --git a/todo.md b/todo.md index 4a080b8..ad65714 100644 --- a/todo.md +++ b/todo.md @@ -1,6 +1,3 @@ -- Proper handling of non-uniform-width characters. Specifically, this needs - to address tabs. But it should be done to handle the general case anyway, - since that's unlikely to be more complex and will future-proof things. - Line number display - Editor info display (filename, current line/column, indentation style, etc.) - File opening by entering path