aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2024-01-06 10:10:52 -0800
committerRose Hogenson <rosehogenson@posteo.net>2024-01-06 10:10:52 -0800
commitdbb1ba202bb5803af1b580b588e7aeef0c6f7e31 (patch)
tree937c755caafc14367b108eae6103b8987ee44aa8 /src
parentf50c6f210a353d8b12552f6a20bf58c2a7bdd3df (diff)
downloadeditor-dbb1ba202bb5803af1b580b588e7aeef0c6f7e31.tar.zst
Fix unicode handling.
It's hard to say if this counts as "fixed," but I think it's at least better than GNU nano.
Diffstat (limited to 'src')
-rw-r--r--src/main.rs194
-rw-r--r--src/rope.rs86
2 files changed, 212 insertions, 68 deletions
diff --git a/src/main.rs b/src/main.rs
index 2b359f8..599ed36 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,13 +3,100 @@ mod term;
use rope::Rope;
use std::ffi::{OsStr, OsString};
-use std::io::{Stdout, Write};
+use std::io::{stdout, stdin, Write, Read};
use std::path::Path;
use term::Key;
-fn move_cursor(stdout: &mut Stdout, row: usize, col: usize) {
- let _ = write!(stdout, "\x1b[{};{}H", row + 1, col + 1);
- let _ = stdout.flush();
+struct State {
+ buf: Rope,
+ cursor_row: usize,
+ cursor_col: usize,
+ line_offset: usize,
+ line_cols: usize,
+}
+
+impl State {
+ fn cursor_pos(&mut self) -> Result<(usize, usize), String> {
+ if let Err(err) = write!(stdout(), "\x1b[6n") {
+ return Err(format!("cursor position: status report: {}", err));
+ }
+ if let Err(err) = stdout().flush() {
+ return Err(format!("cursor position: status report: {}", err));
+ }
+ let mut buf = vec![0; 20];
+ let n = match stdin().read(&mut buf) {
+ Ok(n) => n,
+ Err(err) => {
+ return Err(format!("cursor position: read status report: {}", err));
+ }
+ };
+ buf.truncate(n);
+ let mut semicolon = 0;
+ for (i, &c) in buf.iter().enumerate() {
+ if c == b';' {
+ semicolon = i;
+ break;
+ }
+ }
+ let row_str = match std::str::from_utf8(&buf[2..semicolon]) {
+ Ok(row_str) => row_str,
+ Err(_) => {
+ return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf)));
+ }
+ };
+ let col_str = match std::str::from_utf8(&buf[semicolon+1..buf.len()-1]) {
+ Ok(col_str) => col_str,
+ Err(_) => {
+ return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf)));
+ }
+ };
+ let row = match row_str.parse::<usize>() {
+ Ok(row) => row,
+ Err(_) => {
+ return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf)));
+ }
+ };
+ let col = match col_str.parse::<usize>() {
+ Ok(col) => col,
+ Err(_) => {
+ return Err(format!("cursor position: invalid response {}", String::from_utf8_lossy(&buf)));
+ }
+ };
+ // Yuck... 1 indexing
+ return Ok((row-1, col-1));
+ }
+
+ fn repaint_line_full(&mut self) {
+ let _ = write!(stdout(), "\x1b[{}H", self.cursor_row+1);
+ let line = self.buf.line(self.cursor_row);
+
+ let _ = line.print(&mut stdout());
+ let (_, col) = self.cursor_pos().expect("asdf");
+ self.line_cols = col;
+ if self.cursor_col >= self.line_cols {
+ self.line_offset = line.len();
+ return;
+ }
+
+ let mut lo = line.char(self.cursor_col).expect("at this point, cursor_col should have some valid offset");
+ let mut hi = line.len();
+ // Binary search :D
+ while hi > lo {
+ let x = line.floor_char_boundary(lo + (hi - lo) / 2);
+ let _ = write!(stdout(), "\x1b[G");
+ let _ = line.slice(0, x).print(&mut stdout());
+ let (_, col) = self.cursor_pos().expect("ouchie, I should really handle this I guess");
+ if col <= self.cursor_col {
+ lo = line.ceil_char_boundary(x+1);
+ } else {
+ hi = x;
+ }
+ }
+ self.line_offset = line.floor_char_boundary(lo-1);
+ let _ = write!(stdout(), "\x1b[G\x1b[K");
+ let _ = line.print(&mut stdout());
+ let _ = write!(stdout(), "\x1b[{}G", self.cursor_col+1);
+ }
}
fn edit(file: &OsStr) -> Result<(), String> {
@@ -20,7 +107,7 @@ fn edit(file: &OsStr) -> Result<(), String> {
}
};
- let mut r = match Rope::open(Path::new(file)) {
+ let r = match Rope::open(Path::new(file)) {
Ok(r) => r,
Err(err) => {
return Err(format!("open file {}: {}", file.to_string_lossy(), err));
@@ -32,28 +119,34 @@ fn edit(file: &OsStr) -> Result<(), String> {
return Err(String::from("cannot get terminal size"));
}
};
- let mut stdout = std::io::stdout();
- let _ = stdout.write(b"\x1b[J");
+ let mut state = State{
+ buf: r,
+ cursor_row: 0,
+ cursor_col: 0,
+ line_offset: 0,
+ line_cols: 0,
+ };
+ let _ = write!(stdout(), "\x1b[J");
for i in 0..usize::from(size.ws_row) {
if i > 0 {
- let _ = stdout.write(b"\r\n");
+ let _ = write!(stdout(), "\r\n");
}
- if i > r.lines() {
+ if i > state.buf.lines() {
break;
}
- let line = r.line(i);
+ let line = state.buf.line(i);
// TODO: handle line-wrapping.
- let _ = line.print(&mut stdout);
+ let _ = line.print(&mut stdout());
+ if i == 0 {
+ let (_, col) = state.cursor_pos()?;
+ state.line_cols = col;
+ }
}
- let _ = stdout.write(b"\x1b[H");
- let _ = stdout.flush();
+ let _ = write!(stdout(), "\x1b[H");
+ let _ = stdout().flush();
- let mut cursor_row = 0;
- let mut cursor_col = 0;
-
- let mut stdin = std::io::stdin();
loop {
- let c = match term::read_key(&mut stdin) {
+ let c = match term::read_key(&mut stdin()) {
Ok(c) => c,
Err(err) => {
return Err(err);
@@ -64,70 +157,51 @@ fn edit(file: &OsStr) -> Result<(), String> {
break;
}
Key::CtrlS => {
- if let Err(err) = r.save(Path::new(file)) {
+ if let Err(err) = state.buf.save(Path::new(file)) {
return Err(format!("write file {}: {}", file.to_string_lossy(), err));
}
}
Key::Up => {
- if cursor_row == 0 {
+ if state.cursor_row == 0 {
continue;
}
- cursor_row -= 1;
+ state.cursor_row -= 1;
- let len = r
- .line(cursor_row)
- .len();
- if cursor_col > len {
- move_cursor(&mut stdout, cursor_row, len);
- } else {
- move_cursor(&mut stdout, cursor_row, cursor_col);
- }
+ state.repaint_line_full();
+ let _ = stdout().flush();
}
Key::Down => {
- if cursor_row == usize::from(size.ws_row - 1) || cursor_row == r.lines() {
+ if state.cursor_row == usize::from(size.ws_row - 1) || state.cursor_row == state.buf.lines() {
continue;
}
- cursor_row += 1;
+ state.cursor_row += 1;
- let len = r
- .line(cursor_row)
- .len();
- if cursor_col > len {
- move_cursor(&mut stdout, cursor_row, len);
- } else {
- move_cursor(&mut stdout, cursor_row, cursor_col);
- }
+ state.repaint_line_full();
+ let _ = stdout().flush();
}
Key::Left => {
- let len = r
- .line(cursor_row)
- .len();
- cursor_col = std::cmp::min(cursor_col, len);
- if cursor_col == 0 {
+ state.cursor_col = std::cmp::min(state.cursor_col, state.line_cols);
+ if state.cursor_col == 0 {
continue;
}
- cursor_col -= 1;
- move_cursor(&mut stdout, cursor_row, cursor_col);
+ state.cursor_col -= 1;
+ state.repaint_line_full();
+ let _ = stdout().flush();
}
Key::Right => {
- let len = r
- .line(cursor_row)
- .len();
- if cursor_col >= len {
+ if state.cursor_col >= state.line_cols {
continue;
}
- cursor_col += 1;
- move_cursor(&mut stdout, cursor_row, cursor_col);
+ state.cursor_col += 1;
+ state.repaint_line_full();
+ let _ = stdout().flush();
}
Key::Char(c) => {
- cursor_col = std::cmp::min(cursor_col, r.line(cursor_row).len());
- r = r.insert(r.line_idx(cursor_row)+cursor_col, c);
- move_cursor(&mut stdout, cursor_row, 0);
- let _ = r
- .line(cursor_row)
- .print(&mut stdout);
- cursor_col += 1;
- move_cursor(&mut stdout, cursor_row, cursor_col);
+ state.cursor_col = std::cmp::min(state.cursor_col, state.line_cols);
+ state.buf = state.buf.insert(state.buf.line_idx(state.cursor_row)+state.line_offset, c);
+ state.cursor_col += 1;
+ state.repaint_line_full();
+ let _ = stdout().flush();
}
}
}
diff --git a/src/rope.rs b/src/rope.rs
index ca057bd..ea37e6c 100644
--- a/src/rope.rs
+++ b/src/rope.rs
@@ -76,6 +76,9 @@ impl Rope {
}
pub fn print(&self, out: &mut dyn Write) -> Result<(), std::io::Error> {
+ if self.len() == 0 {
+ return Ok(());
+ }
// TODO: escape unprintable characters
match self {
Rope(Node::Leaf(l)) => {
@@ -209,9 +212,9 @@ impl Rope {
return Ok(());
}
- fn slice(&self, start: usize, end: usize) -> Rope {
- if start >= self.len() {
- panic!("Index {} out of range 0..{}", start, self.len());
+ pub fn slice(&self, start: usize, end: usize) -> Rope {
+ if start > self.len() {
+ panic!("Index {} out of range 0..{}", start, self.len()+1);
}
if end > self.len() {
panic!("Index {} out of range 0..{}", end, self.len());
@@ -223,8 +226,8 @@ impl Rope {
Rope(Node::Leaf(l)) => {
return Rope(Node::Leaf(Leaf{
buf: l.buf.clone(),
- start: u8::try_from(start).expect("buffer too long"),
- end: u8::try_from(end).expect("buffer too long"),
+ start: l.start + u8::try_from(start).expect("buffer too long"),
+ end: l.start + u8::try_from(end).expect("buffer too long"),
}));
}
Rope(Node::Branch(b)) => {
@@ -234,11 +237,11 @@ impl Rope {
}
let mut right = Rope::leaf(Vec::new());
if end > b.left.len() {
- let mut left_start = 0;
+ let mut right_start = 0;
if start > b.left.len() {
- left_start = start - b.left.len();
+ right_start = start - b.left.len();
}
- right = b.right.slice(left_start, end - b.left.len());
+ right = b.right.slice(right_start, end - b.left.len());
}
return left.concat(&right);
}
@@ -256,4 +259,71 @@ impl Rope {
}
return self.slice(start, end-1);
}
+
+ pub fn char(&self, n: usize) -> Result<usize, usize> {
+ match self {
+ Rope(Node::Leaf(l)) => {
+ let mut count = 0;
+ for (i, &b) in l.bytes().iter().enumerate() {
+ if b & 0xc0 == 0x80 {
+ continue;
+ }
+ if count == n {
+ return Ok(i);
+ }
+ count += 1;
+ }
+ return Err(count);
+ }
+ Rope(Node::Branch(b)) => {
+ match b.left.char(n) {
+ Ok(i) => {
+ return Ok(i);
+ }
+ Err(len) => {
+ match b.right.char(n - len) {
+ Ok(i) => {
+ return Ok(b.left.len() + i);
+ }
+ Err(right_len) => {
+ return Err(len + right_len);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn is_char_boundary(&self, index: usize) -> bool {
+ match self {
+ Rope(Node::Leaf(l)) => {
+ return l.bytes()[index] & 0xc0 != 0x80;
+ }
+ Rope(Node::Branch(b)) => {
+ if index < b.left.len() {
+ return b.left.is_char_boundary(index);
+ }
+ return b.right.is_char_boundary(index - b.left.len());
+ }
+ }
+ }
+
+ pub fn floor_char_boundary(&self, index: usize) -> usize {
+ for i in (0..index+1).rev() {
+ if self.is_char_boundary(i) {
+ return i;
+ }
+ }
+ panic!("I'm not valid UTF-8: {:?}", self);
+ }
+
+ pub fn ceil_char_boundary(&self, index: usize) -> usize {
+ for i in index..self.len() {
+ if self.is_char_boundary(i) {
+ return i;
+ }
+ }
+ return self.len();
+ }
}