Improve text editor (#109)

* Rename offset_y to dy

* Add truncated line indicator

* Render tab as spaces

* Fix insertion

* Update help

* Add horizontal screen offset

* Support delete key in editor

* Support delete key in shell
This commit is contained in:
Vincent Ollivier 2020-11-05 21:36:16 +01:00 committed by GitHub
parent 2a584af6a8
commit 0d2c5343a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 186 additions and 70 deletions

View File

@ -1,7 +1,7 @@
use crate::{kernel, print, user};
use crate::kernel::console::Style;
use alloc::format;
use alloc::string::String;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::cmp;
@ -15,17 +15,25 @@ pub fn main(args: &[&str]) -> user::shell::ExitCode {
editor.run()
}
struct EditorConfig {
tab_size: usize,
}
pub struct Editor {
file: Option<kernel::fs::File>,
pathname: String,
lines: Vec<String>,
offset: usize, // TODO: Call it `offset_y` and introduce `offset_x`
dx: usize, // Horizontal offset
dy: usize, // Vertical offset
config: EditorConfig,
}
impl Editor {
pub fn new(pathname: &str) -> Self {
let offset = 0;
let dx = 0;
let dy = 0;
let mut lines = Vec::new();
let config = EditorConfig { tab_size: 4 };
let file = match kernel::fs::File::open(pathname) {
Some(file) => {
@ -43,7 +51,7 @@ impl Editor {
let pathname = pathname.into();
Self { file, pathname, lines, offset }
Self { file, pathname, lines, dx, dy, config }
}
pub fn save(&mut self) -> user::shell::ExitCode {
@ -69,30 +77,53 @@ impl Editor {
}
fn print_status(&mut self, status: &str, background: &str) {
let csi_color = Style::color("Black").with_background(background);
let csi_reset = Style::reset();
let color = Style::color("Black").with_background(background);
let reset = Style::reset();
let (x, y) = kernel::vga::cursor_position();
kernel::vga::set_writer_position(0, self.height());
print!("{}{:width$}{}", csi_color, status, csi_reset, width = self.width());
print!("{}{:width$}{}", color, status, reset, width = self.width());
kernel::vga::set_writer_position(x, y);
kernel::vga::set_cursor_position(x, y);
}
fn print_screen(&mut self) {
let mut lines: Vec<String> = Vec::new();
let from = self.offset;
let to = cmp::min(self.lines.len(), self.offset + self.height());
for i in from..to {
let n = self.width();
let line = format!("{:width$}", self.lines[i], width = n);
lines.push(line[0..n].into()); // TODO: Use `offset_x .. offset_x + n`
let mut rows: Vec<String> = Vec::new();
let a = self.dy;
let b = self.dy + self.height();
for y in a..b {
rows.push(self.render_line(y));
}
kernel::vga::set_writer_position(0, 0);
print!("{}", lines.join("\n"));
print!("{}", rows.join("\n"));
let status = format!("Editing '{}'", self.pathname);
self.print_status(&status, "LightGray");
}
fn render_line(&self, y: usize) -> String {
// Render line into a row of the screen, or an empty row when past eof
let line = if y < self.lines.len() { &self.lines[y] } else { "" };
let mut row = format!("{:width$}", line, width = self.dx);
let n = self.dx + self.width();
if row.len() > n {
row.truncate(n - 1);
row.push_str(&truncated_line_indicator());
} else {
row.push_str(&" ".repeat(n - row.len()));
}
row[self.dx..].to_string()
}
fn render_char(&self, c: char) -> Option<String> {
match c {
'!'..='~' => Some(c.to_string()), // graphic char
' ' => Some(" ".to_string()),
'\t' => Some(" ".repeat(self.config.tab_size).to_string()),
_ => None,
}
}
pub fn run(&mut self) -> user::shell::ExitCode {
kernel::vga::clear_screen();
self.print_screen();
@ -120,126 +151,172 @@ impl Editor {
return res;
},
'\n' => { // Newline
let new_line = self.lines[self.offset + y].split_off(x);
self.lines.insert(self.offset + y + 1, new_line);
let line = self.lines[self.dy + y].split_off(self.dx + x);
self.lines.insert(self.dy + y + 1, line);
if y == self.height() - 1 {
self.offset += 1;
self.dy += 1;
} else {
y += 1;
}
x = 0;
self.dx = 0;
self.print_screen();
},
'↑' => { // Arrow up
if y > 0 {
y -= 1
} else {
if self.offset > 0 {
self.offset -= 1;
if self.dy > 0 {
self.dy -= 1;
self.print_screen();
}
}
x = cmp::min(x, self.lines[self.offset + y].len());
x = self.next_pos(x, y);
},
'↓' => { // Arrow down
let is_eof = self.dy + y == self.lines.len() - 1;
let is_bottom = y == self.height() - 1;
let is_eof = self.offset + y == self.lines.len() - 1;
if y < cmp::min(self.height(), self.lines.len() - 1) {
if is_bottom || is_eof {
if !is_eof {
self.offset += 1;
self.dy += 1;
self.print_screen();
}
} else {
y += 1;
}
x = cmp::min(x, self.lines[self.offset + y].len());
x = self.next_pos(x, y);
}
},
'←' => { // Arrow left
if x == 0 {
if x + self.dx == 0 {
continue;
} else if x == 0 {
x = self.dx - 1;
self.dx -= self.width();
self.print_screen();
x = self.next_pos(x, y);
} else {
x -= 1;
}
x -= 1;
},
'→' => { // Arrow right
let line = &self.lines[self.offset + y];
if x == cmp::min(self.width() - 1, line.len()) {
continue;
let line = &self.lines[self.dy + y];
if line.len() == 0 || x + self.dx >= line.len() {
continue
} else if x == self.width() - 1 {
x = self.dx;
self.dx += self.width();
self.print_screen();
} else {
x += 1;
}
x += 1;
},
'\x14' => { // Ctrl T -> Go to top of file
x = 0;
y = 0;
self.offset = 0;
self.dx = 0;
self.dy = 0;
self.print_screen();
},
'\x02' => { // Ctrl B -> Go to bottom of file
x = 0;
y = cmp::min(self.height(), self.lines.len()) - 1;
self.offset = self.lines.len() - 1 - y;
self.dx = 0;
self.dy = self.lines.len() - 1 - y;
self.print_screen();
},
'\x01' => { // Ctrl A -> Go to beginning of line
x = 0;
self.dx = 0;
self.print_screen();
},
'\x05' => { // Ctrl E -> Go to end of line
let line_length = self.lines[self.offset + y].len();
if line_length > 0 {
x = cmp::min(line_length, self.width() - 1);
}
let n = self.lines[self.dy + y].len();
let w = self.width();
x = n % w;
self.dx = w * (n / w);
self.print_screen();
},
'\x08' => { // Backspace
if x > 0 { // Remove char from line
let line = self.lines[self.offset + y].clone();
let (before, mut after) = line.split_at(x - 1);
if self.dx + x > 0 { // Remove char from line
let line = self.lines[self.dy + y].clone();
let pos = self.dx + x - 1;
let (before, mut after) = line.split_at(pos);
if after.len() > 0 {
after = &after[1..];
}
self.lines[self.offset + y].clear();
self.lines[self.offset + y].push_str(before);
self.lines[self.offset + y].push_str(after);
self.lines[self.dy + y].clear();
self.lines[self.dy + y].push_str(before);
self.lines[self.dy + y].push_str(after);
let mut line = self.lines[self.offset + y].clone();
line.truncate(self.width());
kernel::vga::clear_row();
print!("{}", line);
x -= 1;
} else { // Remove newline char from previous line
if y == 0 && self.offset == 0 {
if x == 0 {
self.dx -= self.width();
x = self.width() - 1;
self.print_screen();
} else {
x -= 1;
let line = self.render_line(self.dy + y);
kernel::vga::clear_row();
print!("{}", line);
}
} else { // Remove newline from previous line
if y == 0 && self.dy == 0 {
continue;
}
x = self.lines[self.offset + y - 1].len();
let line = self.lines.remove(self.offset + y);
self.lines[self.offset + y - 1].push_str(&line);
// Move cursor below the end of the previous line
let n = self.lines[self.dy + y - 1].len();
let w = self.width();
x = n % w;
self.dx = w * (n / w);
// Move line to the end of the previous line
let line = self.lines.remove(self.dy + y);
self.lines[self.dy + y - 1].push_str(&line);
// Move cursor up to the previous line
if y > 0 {
y -= 1;
} else {
self.offset -= 1;
self.dy -= 1;
}
self.print_screen();
}
},
c => {
if !c.is_ascii() || !kernel::vga::is_printable(c as u8) {
continue;
'\x7f' => { // Delete
let n = self.lines[self.dy + y].len();
if self.dx + x >= n { // Remove newline from line
let line = self.lines.remove(self.dy + y + 1);
self.lines[self.dy + y].push_str(&line);
self.print_screen();
} else { // Remove char from line
self.lines[self.dy + y].remove(self.dx + x);
let line = self.render_line(self.dy + y);
kernel::vga::clear_row();
print!("{}", line);
}
},
c => {
if let Some(s) = self.render_char(c) {
let line = self.lines[self.dy + y].clone();
let (before, after) = line.split_at(self.dx + x);
self.lines[self.dy + y].clear();
self.lines[self.dy + y].push_str(before);
self.lines[self.dy + y].push_str(&s);
self.lines[self.dy + y].push_str(after);
let line = self.lines[self.offset + y].clone();
let (before_cursor, after_cursor) = line.split_at(x);
self.lines[self.offset + y].clear();
self.lines[self.offset + y].push_str(before_cursor);
self.lines[self.offset + y].push(c);
self.lines[self.offset + y].push_str(after_cursor);
let mut line = self.lines[self.offset + y].clone();
line.truncate(self.width());
kernel::vga::clear_row();
print!("{}", line);
if x < self.width() - 1 {
x += 1;
x += s.len();
if x >= self.width() {
self.dx += self.width();
x -= self.dx;
self.print_screen();
} else {
let line = self.render_line(self.dy + y);
kernel::vga::clear_row();
print!("{}", line);
}
}
},
}
@ -249,6 +326,20 @@ impl Editor {
user::shell::ExitCode::CommandSuccessful
}
// Move cursor past end of line to end of line or left of the screen
fn next_pos(&self, x: usize, y: usize) -> usize {
let eol = self.lines[self.dy + y].len();
if eol <= self.dx + x {
if eol <= self.dx {
0
} else {
eol - 1
}
} else {
x
}
}
fn height(&self) -> usize {
kernel::vga::screen_height() - 1 // Leave out one line for status line
}
@ -257,3 +348,9 @@ impl Editor {
kernel::vga::screen_width()
}
}
fn truncated_line_indicator() -> String {
let color = Style::color("Black").with_background("LightGray");
let reset = Style::reset();
format!("{}>{}", color, reset)
}

View File

@ -57,7 +57,7 @@ fn help_summary() -> user::shell::ExitCode {
fn help_edit() -> user::shell::ExitCode {
let csi_color = Style::color("Yellow");
let csi_reset = Style::reset();
print!("MOROS text editor is somewhat inspired by nano, but with an even smaller range\n");
print!("MOROS text editor is somewhat inspired by Pico, but with an even smaller range\n");
print!("of features.\n");
print!("\n");
print!("{}Shortcuts:{}\n", csi_color, csi_reset);

View File

@ -183,6 +183,25 @@ impl Shell {
}
}
},
'\x7f' => { // Delete
self.update_history();
self.update_autocomplete();
if self.cmd.len() > 0 {
if kernel::console::has_cursor() {
let cmd = self.cmd.clone();
let (before_cursor, mut after_cursor) = cmd.split_at(x - self.prompt.len());
if after_cursor.len() > 0 {
after_cursor = &after_cursor[1..];
}
self.cmd.clear();
self.cmd.push_str(before_cursor);
self.cmd.push_str(after_cursor);
print!("{} ", after_cursor);
kernel::vga::set_cursor_position(x, y);
kernel::vga::set_writer_position(x, y);
}
}
},
c => {
self.update_history();
self.update_autocomplete();