├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md └── src ├── editor ├── cursor.rs ├── key.rs ├── mod.rs ├── row.rs ├── search_state.rs └── syntax.rs ├── main.rs ├── terminal.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "kilo" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)", 6 | ] 7 | 8 | [[package]] 9 | name = "libc" 10 | version = "0.2.21" 11 | source = "registry+https://github.com/rust-lang/crates.io-index" 12 | 13 | [metadata] 14 | "checksum libc 0.2.21 (registry+https://github.com/rust-lang/crates.io-index)" = "88ee81885f9f04bff991e306fea7c1c60a5f0f9e409e99f6b40e3311a3363135" 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "kilo" 3 | version = "0.1.0" 4 | authors = ["Nathan Bouscal "] 5 | 6 | [dependencies] 7 | libc = "0.2" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kilo.rs 2 | 3 | An implementation of antirez's [kilo](https://github.com/antirez/kilo) text editor in [Rust](https://www.rust-lang.org/). 4 | 5 | Inspired by the [snaptoken tutorial](http://viewsourcecode.org/snaptoken/kilo). 6 | -------------------------------------------------------------------------------- /src/editor/cursor.rs: -------------------------------------------------------------------------------- 1 | #[derive(Clone, Copy)] 2 | pub struct Cursor { 3 | pub x: usize, 4 | pub y: usize, 5 | } 6 | 7 | impl Cursor { 8 | pub fn new() -> Self { 9 | Cursor { x: 0, y: 0 } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/editor/key.rs: -------------------------------------------------------------------------------- 1 | pub enum Key { 2 | Character(char), 3 | Control(char), 4 | Arrow(ArrowKey), 5 | Escape, 6 | Backspace, 7 | Delete, 8 | Home, 9 | End, 10 | PageUp, 11 | PageDown, 12 | } 13 | 14 | pub enum ArrowKey { 15 | Left, 16 | Right, 17 | Up, 18 | Down, 19 | } 20 | 21 | impl Key { 22 | pub fn from_byte(byte: u8) -> Option { 23 | match byte { 24 | 0 => None, 25 | b'\x1b' => Some(Key::Escape), 26 | 8 | 127 => Some(Key::Backspace), 27 | 1...31 => Some(Key::Control((byte | 0x40) as char)), 28 | _ => Some(Key::Character(byte as char)) 29 | } 30 | } 31 | 32 | pub fn from_escape_sequence(bytes: &[u8]) -> Self { 33 | match bytes { 34 | b"[A" => Key::Arrow(ArrowKey::Up), 35 | b"[B" => Key::Arrow(ArrowKey::Down), 36 | b"[C" => Key::Arrow(ArrowKey::Right), 37 | b"[D" => Key::Arrow(ArrowKey::Left), 38 | b"[3~" => Key::Delete, 39 | b"[1~" | b"[7~" | b"[H" | b"OH" => Key::Home, 40 | b"[4~" | b"[8~" | b"[F" | b"OF" => Key::End, 41 | b"[5~" => Key::PageUp, 42 | b"[6~" => Key::PageDown, 43 | _ => Key::Escape, 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/editor/mod.rs: -------------------------------------------------------------------------------- 1 | mod cursor; 2 | mod key; 3 | mod row; 4 | mod search_state; 5 | mod syntax; 6 | 7 | use self::cursor::Cursor; 8 | use self::key::{Key, ArrowKey}; 9 | use self::row::{Row, Highlight}; 10 | use self::search_state::{Direction, Match, SearchState}; 11 | use self::syntax::Syntax; 12 | use terminal; 13 | use util; 14 | 15 | use std::cmp; 16 | use std::io::{self, Read, BufRead, BufReader, Write}; 17 | use std::fs::File; 18 | use std::process; 19 | use std::rc::Rc; 20 | use std::time::{Duration, SystemTime}; 21 | 22 | const KILO_VERSION: &'static str = "0.0.1"; 23 | const KILO_QUIT_TIMES: u8 = 3; 24 | 25 | pub struct Editor { 26 | cursor: Cursor, 27 | row_offset: usize, 28 | col_offset: usize, 29 | screen_rows: u16, 30 | screen_cols: u16, 31 | write_buffer: String, 32 | rows: Vec, 33 | dirty: bool, 34 | quit_times: u8, 35 | filename: String, 36 | status_msg: String, 37 | status_time: SystemTime, 38 | syntax: Option>, 39 | search_state: SearchState, 40 | } 41 | 42 | impl Editor { 43 | pub fn new() -> Self { 44 | // TODO: Default to 24x80 if None? 45 | let (rows, cols) = terminal::get_window_size().unwrap(); 46 | Editor { 47 | cursor: Cursor::new(), 48 | row_offset: 0, 49 | col_offset: 0, 50 | screen_rows: rows - 2, // Leave space for status and message bars 51 | screen_cols: cols, 52 | write_buffer: String::new(), 53 | rows: Vec::new(), 54 | dirty: false, 55 | quit_times: KILO_QUIT_TIMES, 56 | filename: String::new(), 57 | status_msg: String::new(), 58 | status_time: SystemTime::now(), 59 | syntax: None, 60 | search_state: SearchState::new(), 61 | } 62 | } 63 | 64 | pub fn set_filename(&mut self, filename: String) { 65 | self.set_syntax(Syntax::for_filename(&filename)); 66 | self.filename = filename; 67 | } 68 | 69 | pub fn set_syntax(&mut self, syntax: Option) { 70 | self.syntax = syntax.map(Rc::new); 71 | for row in self.rows.iter_mut() { row.set_syntax(self.syntax.clone()) } 72 | } 73 | 74 | pub fn check_mlcomments(&mut self) { 75 | let mut in_mlcomment = false; 76 | for row in self.rows.iter_mut() { 77 | let old_open_comment = row.open_comment; 78 | row.open_comment = in_mlcomment; 79 | if row.ends_mlcomment() { in_mlcomment = false } 80 | if row.starts_mlcomment() { in_mlcomment = true } 81 | if row.open_comment != old_open_comment { row.update_syntax() } 82 | } 83 | } 84 | 85 | pub fn insert_char(&mut self, c: char) { 86 | if self.cursor_past_end() { 87 | let mut row = Row::new(); 88 | row.set_syntax(self.syntax.clone()); 89 | self.rows.push(row); 90 | } 91 | let cursor_x = self.cursor.x; 92 | { 93 | let mut current_row = self.current_row_mut().unwrap(); 94 | current_row.insert_char(cursor_x, c); 95 | } 96 | self.cursor.x += 1; 97 | self.dirty = true; 98 | self.check_mlcomments(); 99 | } 100 | 101 | pub fn insert_newline(&mut self) { 102 | if self.cursor.x == 0 { 103 | let cursor_y = self.cursor.y; 104 | self.insert_row(cursor_y, String::new()); 105 | } else { 106 | let cursor_x = self.cursor.x; 107 | let remainder = self.current_row_mut().unwrap().split_off(cursor_x); 108 | let cursor_y = self.cursor.y; 109 | self.insert_row(cursor_y + 1, remainder); 110 | } 111 | self.cursor.y += 1; 112 | self.cursor.x = 0; 113 | self.dirty = true; 114 | self.check_mlcomments(); 115 | } 116 | 117 | pub fn delete_char(&mut self) { 118 | if self.cursor_past_end() { return }; 119 | if self.cursor.x == 0 && self.cursor.y == 0 { return }; 120 | let cursor_x = self.cursor.x; 121 | if cursor_x == 0 { 122 | let cursor_y = self.cursor.y; 123 | self.cursor.x = self.rows[cursor_y - 1].contents.len(); 124 | // Is there a way to avoid this clone? 125 | let s = self.current_row().unwrap().contents.clone(); 126 | self.rows[cursor_y - 1].append_string(&s); 127 | self.delete_row(cursor_y); 128 | self.cursor.y -= 1; 129 | } else { 130 | self.current_row_mut().unwrap().delete_char(cursor_x - 1); 131 | self.cursor.x -= 1; 132 | } 133 | self.dirty = true; 134 | self.check_mlcomments(); 135 | } 136 | 137 | fn insert_row(&mut self, at: usize, s: String) { 138 | if at <= self.rows.len() { 139 | let mut row = Row::from_string(s); 140 | row.set_syntax(self.syntax.clone()); 141 | self.rows.insert(at, row); 142 | }; 143 | } 144 | 145 | fn delete_row(&mut self, at: usize) { 146 | if at >= self.rows.len() { return } 147 | self.rows.remove(at); 148 | } 149 | 150 | fn rows_to_string(&self) -> String { 151 | self.rows.iter() 152 | .map(|row| row.contents.clone()) 153 | .collect::>() 154 | .join("\n") 155 | } 156 | 157 | pub fn open_file(&mut self, filename: &str) { 158 | let f = File::open(filename).unwrap(); // TODO: Handle error 159 | let reader = BufReader::new(f); 160 | self.rows = reader.lines() 161 | .map(|line| line.unwrap_or(String::new())) 162 | .map(Row::from_string).collect(); 163 | self.set_filename(filename.to_string()); 164 | self.dirty = false; 165 | self.check_mlcomments(); 166 | for row in self.rows.iter_mut() { row.update_syntax() } 167 | } 168 | 169 | pub fn save_file(&mut self) { 170 | if self.filename.is_empty() { 171 | match self.prompt(&|buf| format!("Save as: {}", buf), &|_, _, _|()) { 172 | Some(name) => { 173 | self.set_filename(name); 174 | }, 175 | None => { 176 | self.set_status_message("Save aborted"); 177 | return; 178 | }, 179 | } 180 | } 181 | let mut f = File::create(&self.filename).unwrap(); // TODO: Handle error 182 | let bytes = f.write(&self.rows_to_string().as_bytes()).unwrap(); 183 | self.set_status_message(&format!("{} bytes written to disk", bytes)); 184 | self.dirty = false; 185 | } 186 | 187 | pub fn find(&mut self) { 188 | let saved_cursor = self.cursor; 189 | let saved_col_offset = self.col_offset; 190 | let saved_row_offset = self.row_offset; 191 | self.search_state = SearchState::new(); 192 | 193 | let query = self.prompt(&|buf| format!("Search: {} (Use ESC/Arrows/Enter)", buf), 194 | &Self::find_callback); 195 | if query.is_none() { 196 | self.cursor = saved_cursor; 197 | self.col_offset = saved_col_offset; 198 | self.row_offset = saved_row_offset; 199 | } 200 | self.search_state = SearchState::new(); 201 | } 202 | 203 | fn find_callback(&mut self, query: &str, key: Key) { 204 | let mut current = match self.search_state.last_match { 205 | Some(Match { cursor, ref highlight }) => { 206 | self.rows[cursor.y].highlight = highlight.clone(); 207 | cursor.y 208 | }, 209 | None => 0, 210 | }; 211 | 212 | match key { 213 | Key::Control('M') | Key::Escape => return, 214 | Key::Arrow(ak) => { 215 | match ak { 216 | ArrowKey::Left | ArrowKey::Up => { 217 | current -= 1; 218 | self.search_state.direction = Direction::Backward; 219 | }, 220 | ArrowKey::Right | ArrowKey::Down => { 221 | current += 1; 222 | self.search_state.direction = Direction::Forward; 223 | }, 224 | }; 225 | }, 226 | _ => (), 227 | } 228 | 229 | if query.is_empty() { return } 230 | 231 | let num_rows = self.rows.len(); 232 | 233 | let res = match self.search_state.direction { 234 | Direction::Forward => { 235 | let iter = self.rows.iter().enumerate() 236 | .cycle().skip(current).take(num_rows); 237 | Self::find_in_rows(iter, query) 238 | }, 239 | Direction::Backward => { 240 | let iter = self.rows.iter().enumerate().rev() 241 | .cycle().skip(num_rows - current - 1).take(num_rows); 242 | Self::find_in_rows(iter, query) 243 | }, 244 | }; 245 | match res { 246 | Some(cursor) => { 247 | self.search_state.last_match = Some(Match { 248 | cursor: cursor, 249 | highlight: self.rows[cursor.y].highlight.clone() 250 | }); 251 | self.cursor = cursor; 252 | self.row_offset = self.rows.len(); 253 | 254 | for i in cursor.x..cursor.x + query.len() { 255 | self.rows[cursor.y].highlight[i] = Highlight::Match; 256 | } 257 | }, 258 | _ => (), 259 | } 260 | } 261 | 262 | fn find_in_rows<'a, T: Iterator>(iter: T, query: &str) -> Option { 263 | let res = iter.map(|(y, ref row)| { 264 | let x = row.render.find(&query) 265 | .map(|x| row.raw_cursor_x(x)); 266 | (x, y) 267 | }) 268 | .find(|&(option_x, _)| option_x.is_some()); 269 | res.map(|(option_x, y)| Cursor { x: option_x.unwrap(), y: y }) 270 | } 271 | 272 | fn rendered_cursor_x(&self) -> usize { 273 | self.current_row() 274 | .map_or(0, |row| row.rendered_cursor_x(self.cursor.x)) 275 | } 276 | 277 | pub fn refresh_screen(&mut self) { 278 | self.scroll(); 279 | self.write_buffer.push_str("\x1b[?25l"); 280 | self.write_buffer.push_str("\x1b[H"); 281 | self.draw_rows(); 282 | self.draw_status_bar(); 283 | self.draw_message_bar(); 284 | let cursor_y = self.cursor.y - self.row_offset + 1; 285 | let cursor_x = self.rendered_cursor_x() - self.col_offset + 1; 286 | let set_cursor = format!("\x1b[{};{}H", cursor_y, cursor_x); 287 | self.write_buffer.push_str(&set_cursor); 288 | self.write_buffer.push_str("\x1b[?25h"); 289 | let _ = io::stdout().write(self.write_buffer.as_bytes()); 290 | let _ = io::stdout().flush(); 291 | self.write_buffer.clear(); 292 | } 293 | 294 | fn scroll(&mut self) { 295 | let rx = self.rendered_cursor_x(); 296 | if self.cursor.y < self.row_offset { 297 | self.row_offset = self.cursor.y; 298 | } else if self.cursor.y >= self.row_offset + (self.screen_rows as usize) { 299 | self.row_offset = self.cursor.y - (self.screen_rows as usize) + 1; 300 | } 301 | if rx < self.col_offset { 302 | self.col_offset = rx; 303 | } else if rx >= self.col_offset + (self.screen_cols as usize) { 304 | self.col_offset = rx - (self.screen_cols as usize) + 1; 305 | } 306 | } 307 | 308 | fn draw_rows(&mut self) { 309 | for i in 0..self.screen_rows as usize { 310 | let file_row = i + self.row_offset; 311 | if file_row >= self.rows.len() { 312 | if self.rows.is_empty() && i == (self.screen_rows as usize) / 3 { 313 | let mut welcome = format!("Kilo editor -- version {}", KILO_VERSION); 314 | util::safe_truncate(&mut welcome, self.screen_cols as usize); 315 | 316 | let padding = (self.screen_cols as usize - welcome.len()) / 2; 317 | if padding > 0 { 318 | self.write_buffer.push_str("~"); 319 | let spaces = " ".repeat(padding - 1); 320 | self.write_buffer.push_str(&spaces); 321 | } 322 | 323 | self.write_buffer.push_str(&welcome); 324 | } else { 325 | self.write_buffer.push_str("~"); 326 | } 327 | } else { 328 | let ref row = self.rows[file_row]; 329 | let mut current_color = 0; 330 | let render = row.render.char_indices() 331 | .skip(self.col_offset).take(self.screen_cols as usize) 332 | .map(|(i, c)| { 333 | if c.is_control() { 334 | let sym = if c as u8 <= 26 { 335 | ('@' as u8 + c as u8) as char 336 | } else { 337 | '?' 338 | }; 339 | let reset = if current_color > 0 { 340 | format!("\x1b[{}m", current_color) 341 | } else { 342 | String::new() 343 | }; 344 | format!("\x1b[7m{}\x1b[m{}", sym, reset) 345 | } else { 346 | let color = row.highlight[i].to_color(); 347 | if color != current_color { 348 | current_color = color; 349 | format!("\x1b[{}m{}", color, c) 350 | } else { 351 | c.to_string() 352 | } 353 | } 354 | }) 355 | .collect::(); 356 | self.write_buffer.push_str(&render); 357 | self.write_buffer.push_str("\x1b[39m"); 358 | } 359 | 360 | self.write_buffer.push_str("\x1b[K"); 361 | self.write_buffer.push_str("\r\n"); 362 | } 363 | } 364 | 365 | fn draw_status_bar(&mut self) { 366 | self.write_buffer.push_str("\x1b[7m"); 367 | 368 | let mut filename = self.filename.clone(); 369 | if filename.is_empty() { 370 | filename.push_str("[No Name]") 371 | } else { 372 | util::safe_truncate(&mut filename, 20); 373 | } 374 | let modified = if self.dirty { "(modified)" } else { "" }; 375 | let mut status = format!("{} - {} lines {}", filename, self.rows.len(), modified); 376 | let syntax = match self.syntax { 377 | Some(ref s) => s.filetype, 378 | None => "no ft", 379 | }; 380 | let rstatus = format!("{} | {}/{}", syntax, self.cursor.y + 1, self.rows.len()); 381 | if self.screen_cols as usize > status.len() + rstatus.len() { 382 | let padding = self.screen_cols as usize - status.len() - rstatus.len(); 383 | status.push_str(&" ".repeat(padding)); 384 | } 385 | status.push_str(&rstatus); 386 | util::safe_truncate(&mut status, self.screen_cols as usize); 387 | self.write_buffer.push_str(&status); 388 | 389 | self.write_buffer.push_str("\x1b[m"); 390 | self.write_buffer.push_str("\r\n"); 391 | } 392 | 393 | pub fn set_status_message(&mut self, msg: &str) { 394 | self.status_msg = msg.to_string(); 395 | self.status_time = SystemTime::now(); 396 | } 397 | 398 | fn draw_message_bar(&mut self) { 399 | self.write_buffer.push_str("\x1b[K"); 400 | let mut message = self.status_msg.clone(); 401 | util::safe_truncate(&mut message, self.screen_cols as usize); 402 | if self.status_time.elapsed().unwrap() < Duration::from_secs(5) { 403 | self.write_buffer.push_str(&message); 404 | } 405 | } 406 | 407 | fn read_key() -> Option { 408 | let stdin = io::stdin(); 409 | let c = stdin.lock().bytes().next().and_then(|res| res.ok()); 410 | if c.is_none() { return None } 411 | if c != Some(b'\x1b') { return Key::from_byte(c.unwrap()) } 412 | let mut seq: Vec = stdin.lock().bytes().take(2).map(|res| res.ok().unwrap()).collect(); 413 | if seq[0] == b'[' && seq[1] >= b'0' && seq[1] <= b'9' { 414 | seq.push(stdin.lock().bytes().next().and_then(|res| res.ok()).unwrap()); 415 | } 416 | Some(Key::from_escape_sequence(&seq)) 417 | } 418 | 419 | fn cursor_past_end(&self) -> bool { 420 | self.cursor.y >= self.rows.len() 421 | } 422 | 423 | fn current_row(&self) -> Option<&Row> { 424 | if self.cursor_past_end() { 425 | None 426 | } else { 427 | Some(&self.rows[self.cursor.y]) 428 | } 429 | } 430 | 431 | fn current_row_mut(&mut self) -> Option<&mut Row> { 432 | if self.cursor_past_end() { 433 | None 434 | } else { 435 | Some(&mut self.rows[self.cursor.y]) 436 | } 437 | } 438 | 439 | fn current_row_size(&self) -> Option { 440 | self.current_row().map(|row| row.contents.len()) 441 | } 442 | 443 | fn prompt(&mut self, prompt: (&Fn(&str) -> String), 444 | callback: (&Fn(&mut Self, &str, Key))) -> Option { 445 | let mut buffer = String::new(); 446 | loop { 447 | self.set_status_message(&prompt(&buffer)); 448 | self.refresh_screen(); 449 | let key = Self::read_key(); 450 | if key.is_none() { continue } 451 | let key = key.unwrap(); 452 | match key { 453 | Key::Character(c) => buffer.push(c), 454 | Key::Control('M') => { 455 | if buffer.len() > 0 { 456 | callback(self, &buffer, key); 457 | break 458 | } 459 | }, 460 | Key::Escape => { 461 | callback(self, &buffer, key); 462 | return None 463 | }, 464 | Key::Backspace => { buffer.pop(); }, 465 | _ => () 466 | } 467 | callback(self, &buffer, key); 468 | } 469 | Some(buffer) 470 | } 471 | 472 | fn move_cursor(&mut self, key: ArrowKey) { 473 | match key { 474 | ArrowKey::Left => { 475 | if self.cursor.x > 0 { 476 | self.cursor.x -= 1 477 | } else if self.cursor.y > 0 { 478 | self.cursor.y -= 1; 479 | self.cursor.x = self.current_row_size().unwrap(); 480 | } 481 | }, 482 | ArrowKey::Right => { 483 | match self.current_row_size() { 484 | Some(current_row_size) => { 485 | if self.cursor.x < current_row_size { 486 | self.cursor.x += 1 487 | } else if self.cursor.x == current_row_size { 488 | self.cursor.y += 1; 489 | self.cursor.x = 0; 490 | } 491 | }, 492 | None => () 493 | } 494 | }, 495 | ArrowKey::Up => { 496 | if self.cursor.y > 0 { self.cursor.y -= 1 } 497 | }, 498 | ArrowKey::Down => { 499 | if self.cursor.y < self.rows.len() { 500 | self.cursor.y += 1 501 | } 502 | }, 503 | } 504 | let current_row_size = self.current_row_size().unwrap_or(0); 505 | if (self.cursor.x) > current_row_size { 506 | self.cursor.x = current_row_size; 507 | } 508 | } 509 | 510 | pub fn process_keypress(&mut self) { 511 | let key = Self::read_key(); 512 | if key.is_none() { return } 513 | match key.unwrap() { 514 | Key::Character(c) => self.insert_char(c), 515 | Key::Control('F') => self.find(), 516 | Key::Control('M') => self.insert_newline(), 517 | Key::Control('S') => self.save_file(), 518 | Key::Control('Q') => { 519 | self.exit(); 520 | return; 521 | }, 522 | Key::Control(_) => (), 523 | Key::Arrow(a) => self.move_cursor(a), 524 | Key::Escape => (), 525 | Key::Backspace => self.delete_char(), 526 | Key::Delete => { 527 | self.move_cursor(ArrowKey::Right); 528 | self.delete_char(); 529 | }, 530 | Key::Home => self.cursor.x = 0, 531 | Key::End => self.cursor.x = self.current_row_size().unwrap_or(0), 532 | Key::PageUp => self.page_up(), 533 | Key::PageDown => self.page_down(), 534 | } 535 | self.quit_times = KILO_QUIT_TIMES; 536 | } 537 | 538 | fn exit(&mut self) { 539 | if self.dirty && self.quit_times > 0 { 540 | let quit_times = self.quit_times; 541 | self.set_status_message(&format!("WARNING!!! File has unsaved changes. Press Ctrl-Q {} more times to quit.", quit_times)); 542 | self.quit_times -= 1; 543 | } else { 544 | let _ = io::stdout().write(b"\x1b[2J"); 545 | let _ = io::stdout().write(b"\x1b[H"); 546 | let _ = io::stdout().flush(); 547 | process::exit(0) 548 | } 549 | } 550 | 551 | fn page_up(&mut self) { 552 | self.cursor.y = self.row_offset; 553 | for _ in 0..self.screen_rows { 554 | self.move_cursor(ArrowKey::Up) 555 | } 556 | } 557 | 558 | fn page_down(&mut self) { 559 | self.cursor.y = cmp::min(self.rows.len(), self.row_offset + (self.screen_rows as usize) - 1); 560 | for _ in 0..self.screen_rows { 561 | self.move_cursor(ArrowKey::Down) 562 | } 563 | } 564 | } 565 | -------------------------------------------------------------------------------- /src/editor/row.rs: -------------------------------------------------------------------------------- 1 | use editor::syntax::{Flag, Keyword, Syntax}; 2 | use util; 3 | 4 | use std::iter; 5 | use std::rc::Rc; 6 | 7 | const KILO_TAB_STOP: usize = 8; 8 | 9 | pub struct Row { 10 | pub contents: String, 11 | pub render: String, 12 | pub highlight: Vec, 13 | syntax: Option>, 14 | pub open_comment: bool, 15 | } 16 | 17 | #[derive(PartialEq, Eq, Clone, Copy)] 18 | pub enum Highlight { 19 | Normal, 20 | Comment, 21 | MLComment, 22 | Keyword1, 23 | Keyword2, 24 | String, 25 | Number, 26 | Match, 27 | } 28 | 29 | impl Highlight { 30 | pub fn to_color(&self) -> u8 { 31 | match *self { 32 | Highlight::Normal => 37, 33 | Highlight::Comment | Highlight::MLComment => 36, 34 | Highlight::Keyword1 => 33, 35 | Highlight::Keyword2 => 32, 36 | Highlight::String => 35, 37 | Highlight::Number => 31, 38 | Highlight::Match => 34, 39 | } 40 | } 41 | 42 | pub fn from_keyword(kw: &Keyword) -> Self { 43 | match kw { 44 | &Keyword::One(_) => Highlight::Keyword1, 45 | &Keyword::Two(_) => Highlight::Keyword2, 46 | } 47 | } 48 | } 49 | 50 | enum InString { 51 | SingleQuoted, 52 | DoubleQuoted, 53 | } 54 | 55 | impl InString { 56 | fn to_char(&self) -> char { 57 | match self { 58 | &InString::SingleQuoted => '\'', 59 | &InString::DoubleQuoted => '"', 60 | } 61 | } 62 | 63 | fn from_char(c: char) -> Option { 64 | match c { 65 | '\'' => Some(InString::SingleQuoted), 66 | '"' => Some(InString::DoubleQuoted), 67 | _ => None, 68 | } 69 | } 70 | } 71 | 72 | fn is_separator(c: char) -> bool { 73 | c.is_whitespace() || 74 | c == '\0' || 75 | ",.()+-/*=~%<>[];".contains(c) 76 | } 77 | 78 | impl Row { 79 | pub fn new() -> Self { 80 | Row { 81 | contents: String::new(), 82 | render: String::new(), 83 | highlight: Vec::new(), 84 | syntax: None, 85 | open_comment: false, 86 | } 87 | } 88 | 89 | pub fn from_string(s: String) -> Self { 90 | let mut row = Self::new(); 91 | row.append_string(&s); 92 | row 93 | } 94 | 95 | pub fn set_syntax(&mut self, syntax: Option>) { 96 | self.syntax = syntax; 97 | self.update_syntax(); 98 | } 99 | 100 | fn update(&mut self) { 101 | self.update_render(); 102 | self.update_syntax(); 103 | } 104 | 105 | pub fn starts_mlcomment(&self) -> bool { 106 | match self.highlight.last() { 107 | None => false, 108 | Some(hl) => { 109 | if hl != &Highlight::MLComment { return false } 110 | let mce = self.syntax.as_ref().unwrap().multiline_comment_end; 111 | if self.render.ends_with(mce) { return false } 112 | let mcs = self.syntax.as_ref().unwrap().multiline_comment_start; 113 | self.highlight[0] != Highlight::MLComment || 114 | self.render.starts_with(mcs) 115 | }, 116 | } 117 | } 118 | 119 | pub fn ends_mlcomment(&self) -> bool { 120 | match self.highlight.last() { 121 | None => false, 122 | Some(hl) => { 123 | if self.highlight[0] != Highlight::MLComment { return false } 124 | let mce = self.syntax.as_ref().unwrap().multiline_comment_end; 125 | hl != &Highlight::MLComment || 126 | self.render.ends_with(mce) 127 | }, 128 | } 129 | } 130 | 131 | pub fn update_syntax(&mut self) { 132 | self.highlight = iter::repeat(Highlight::Normal) 133 | .take(self.render.chars().count()).collect(); 134 | 135 | if self.syntax.is_none() { return } 136 | let syntax = self.syntax.as_ref().unwrap(); 137 | 138 | let scs = syntax.singleline_comment_start; 139 | let mcs = syntax.multiline_comment_start; 140 | let mce = syntax.multiline_comment_end; 141 | 142 | let mut prev_sep = true; 143 | let mut in_string = None; 144 | let mut in_mlcomment = self.open_comment; 145 | let opens_comment = (!scs.is_empty() && self.render.contains(scs)) || 146 | (!mcs.is_empty() && self.render.contains(mcs)); 147 | 148 | let mut iter = self.render.chars().enumerate(); 149 | 150 | let render = self.render.clone(); 151 | let keyword_matches = syntax.keywords.iter().flat_map(|&kw| { 152 | render.match_indices(kw.as_str()) 153 | .map(|pair| pair.0) 154 | .zip(iter::repeat(kw)) 155 | }).collect::>(); 156 | 157 | while let Some((i, c)) = iter.next() { 158 | let prev_hl = if i > 0 { 159 | self.highlight[i - 1] 160 | } else { 161 | Highlight::Normal 162 | }; 163 | 164 | if in_string.is_none() && !in_mlcomment && opens_comment { 165 | if self.render.chars().skip(i).collect::().starts_with(scs) { 166 | for j in i..self.highlight.len() { 167 | self.highlight[j] = Highlight::Comment; 168 | } 169 | break; 170 | } 171 | } 172 | 173 | if in_string.is_none() && !mcs.is_empty() && !mce.is_empty() { 174 | if in_mlcomment { 175 | match self.render.chars().skip(i).collect::().find(mce) { 176 | Some(j) => { 177 | self.highlight[i] = Highlight::MLComment; 178 | for k in 1..j + mce.len() { 179 | self.highlight[i + k] = Highlight::MLComment; 180 | iter.next(); 181 | } 182 | in_mlcomment = false; 183 | continue; 184 | } 185 | None => { 186 | for j in i..self.highlight.len() { 187 | self.highlight[j] = Highlight::MLComment; 188 | } 189 | break; 190 | }, 191 | } 192 | } else if opens_comment { 193 | if self.render.chars().skip(i).collect::().starts_with(mcs) { 194 | self.highlight[i] = Highlight::MLComment; 195 | for j in 1..mcs.len() { 196 | self.highlight[i + j] = Highlight::MLComment; 197 | iter.next(); 198 | } 199 | in_mlcomment = true; 200 | continue; 201 | } 202 | } 203 | } 204 | 205 | if syntax.flags.contains(&Flag::HighlightStrings) { 206 | match in_string.as_ref().map(|is: &InString| is.to_char()) { 207 | None => { 208 | let is = InString::from_char(c); 209 | if is.is_some() { 210 | in_string = is; 211 | self.highlight[i] = Highlight::String; 212 | continue; 213 | } 214 | }, 215 | Some(quote) => { 216 | self.highlight[i] = Highlight::String; 217 | 218 | if c == '\\' { 219 | match iter.next() { 220 | Some((j, _)) => { 221 | self.highlight[j] = Highlight::String; 222 | continue; 223 | }, 224 | None => (), 225 | } 226 | } 227 | 228 | if c == quote { in_string = None } 229 | prev_sep = true; 230 | continue; 231 | }, 232 | } 233 | } 234 | 235 | if syntax.flags.contains(&Flag::HighlightNumbers) { 236 | if (c.is_digit(10) && (prev_sep || prev_hl == Highlight::Number)) || (c == '.' && prev_hl == Highlight::Number) { 237 | prev_sep = false; 238 | self.highlight[i] = Highlight::Number; 239 | continue; 240 | } 241 | } 242 | 243 | if prev_sep { 244 | match keyword_matches.iter().find(|&&(j, _)| { i == j }) { 245 | None => (), 246 | Some(&(_, kw)) => { 247 | let s = kw.as_str(); 248 | let chars = self.render.chars().collect::>(); 249 | let separated = i + s.len() == self.render.len() || 250 | is_separator(chars[i + s.len()]); 251 | if !separated { continue } 252 | let hl = Highlight::from_keyword(&kw); 253 | self.highlight[i] = hl; 254 | for j in 1..s.len() { 255 | self.highlight[i + j] = hl; 256 | iter.next(); 257 | } 258 | continue; 259 | }, 260 | }; 261 | } 262 | 263 | prev_sep = is_separator(c); 264 | } 265 | } 266 | 267 | pub fn insert_char(&mut self, at: usize, c: char) { 268 | self.contents.insert(at, c); 269 | self.update(); 270 | } 271 | 272 | pub fn delete_char(&mut self, at: usize) { 273 | if at >= self.contents.len() { return } 274 | self.contents.remove(at); 275 | self.update(); 276 | } 277 | 278 | pub fn append_string(&mut self, s: &str) { 279 | self.contents.push_str(s); 280 | self.update(); 281 | } 282 | 283 | pub fn split_off(&mut self, at: usize) -> String { 284 | let remainder = util::safe_split_off(&mut self.contents, at); 285 | self.update(); 286 | remainder 287 | } 288 | 289 | pub fn rendered_cursor_x(&self, cursor_x: usize) -> usize { 290 | self.contents.chars() 291 | .take(cursor_x) 292 | .fold(0, |acc, c| { 293 | if c == '\t' { 294 | acc + KILO_TAB_STOP - (acc % KILO_TAB_STOP) 295 | } else { 296 | acc + 1 297 | } 298 | }) 299 | } 300 | 301 | pub fn raw_cursor_x(&self, rendered_x: usize) -> usize { 302 | self.contents.chars() 303 | .scan(0, |acc, c| { 304 | if c == '\t' { 305 | *acc = *acc + KILO_TAB_STOP - (*acc % KILO_TAB_STOP) 306 | } else { 307 | *acc += 1 308 | }; 309 | Some(*acc) 310 | }).position(|rx| rx > rendered_x).unwrap() 311 | } 312 | 313 | fn update_render(&mut self) { 314 | self.render = Self::render_string(self.contents.clone()); 315 | } 316 | 317 | fn render_string(s: String) -> String { 318 | let mut idx = 0; 319 | let renderer = |c| 320 | if c == '\t' { 321 | let n = KILO_TAB_STOP - (idx % KILO_TAB_STOP); 322 | idx += n; 323 | iter::repeat(' ').take(n) 324 | } else { 325 | idx += 1; 326 | // This is the same as iter::once(c), but the types of 327 | // the branches of the conditional have to line up. 328 | iter::repeat(c).take(1) 329 | }; 330 | s.chars().flat_map(renderer).collect() 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/editor/search_state.rs: -------------------------------------------------------------------------------- 1 | use editor::cursor::Cursor; 2 | use editor::row::Highlight; 3 | 4 | #[derive(PartialEq, Eq, Clone, Copy)] 5 | pub enum Direction { 6 | Forward, 7 | Backward, 8 | } 9 | 10 | pub struct SearchState { 11 | pub last_match: Option, 12 | pub direction: Direction, 13 | } 14 | 15 | pub struct Match { 16 | pub cursor: Cursor, 17 | pub highlight: Vec, 18 | } 19 | 20 | impl SearchState { 21 | pub fn new() -> Self { 22 | SearchState { 23 | last_match: None, 24 | direction: Direction::Forward, 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/editor/syntax.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | #[derive(PartialEq, Eq, Clone, Hash)] 4 | pub enum Flag { 5 | HighlightNumbers, 6 | HighlightStrings, 7 | } 8 | 9 | pub type Flags = HashSet; 10 | 11 | #[derive(Clone)] 12 | pub struct Syntax { 13 | pub filetype: &'static str, 14 | pub filematch: Vec<&'static str>, 15 | pub keywords: Vec, 16 | pub singleline_comment_start: &'static str, 17 | pub multiline_comment_start: &'static str, 18 | pub multiline_comment_end: &'static str, 19 | pub flags: Flags, 20 | } 21 | 22 | #[derive(Clone, Copy)] 23 | pub enum Keyword { 24 | One(&'static str), 25 | Two(&'static str), 26 | } 27 | 28 | impl Keyword { 29 | pub fn as_str(&self) -> &'static str { 30 | match self { 31 | &Keyword::One(s) => s, 32 | &Keyword::Two(s) => s, 33 | } 34 | } 35 | } 36 | 37 | impl Syntax { 38 | fn database() -> Vec { 39 | let mut db = Vec::new(); 40 | db.push(Syntax { 41 | filetype: "c", 42 | filematch: vec![".c", ".h", ".cpp"], 43 | keywords: vec![ 44 | Keyword::One("switch"), Keyword::One("if"), Keyword::One("while"), 45 | Keyword::One("for"), Keyword::One("break"), Keyword::One("continue"), 46 | Keyword::One("return"), Keyword::One("else"), Keyword::One("struct"), 47 | Keyword::One("union"), Keyword::One("typedef"), Keyword::One("static"), 48 | Keyword::One("enum"), Keyword::One("class"), Keyword::One("case"), 49 | 50 | Keyword::Two("int"), Keyword::Two("long"), Keyword::Two("double"), 51 | Keyword::Two("float"), Keyword::Two("char"), Keyword::Two("unsigned"), 52 | Keyword::Two("signed"), Keyword::Two("void"), 53 | ], 54 | singleline_comment_start: "//", 55 | multiline_comment_start: "/*", 56 | multiline_comment_end: "*/", 57 | flags: [ 58 | Flag::HighlightNumbers, 59 | Flag::HighlightStrings, 60 | ].iter().cloned().collect(), 61 | }); 62 | db.push(Syntax { 63 | filetype: "rust", 64 | filematch: vec![".rs"], 65 | keywords: vec![ 66 | Keyword::One("match"), Keyword::One("if"), Keyword::One("while"), 67 | Keyword::One("for"), Keyword::One("break"), Keyword::One("continue"), 68 | Keyword::One("return"), Keyword::One("else"), Keyword::One("struct"), 69 | Keyword::One("pub"), Keyword::One("const"), Keyword::One("static"), 70 | Keyword::One("enum"), Keyword::One("impl"), Keyword::One("use"), 71 | Keyword::One("fn"), Keyword::One("mod"), Keyword::One("let"), 72 | Keyword::One("mut"), Keyword::One("self"), 73 | 74 | Keyword::Two("usize"), Keyword::Two("isize"), Keyword::Two("str"), 75 | Keyword::Two("bool"), Keyword::Two("char"), Keyword::Two("String"), 76 | Keyword::Two("Option"), Keyword::Two("Vec"), Keyword::Two("Self"), 77 | Keyword::Two("u8"), Keyword::Two("u16"), Keyword::Two("u32"), 78 | Keyword::Two("i8"), Keyword::Two("i16"), Keyword::Two("i32"), 79 | ], 80 | singleline_comment_start: "//", 81 | multiline_comment_start: "/*", 82 | multiline_comment_end: "*/", 83 | flags: [ 84 | Flag::HighlightNumbers, 85 | Flag::HighlightStrings, 86 | ].iter().cloned().collect(), 87 | }); 88 | db 89 | } 90 | 91 | pub fn for_filename(filename: &str) -> Option { 92 | for s in Self::database().into_iter() { 93 | let res = s.filematch.iter() 94 | .map(|ext| filename.rfind(ext)) 95 | .enumerate() 96 | .find(|&(_, opt)| opt.is_some()); 97 | match res { 98 | Some((match_idx, Some(name_idx))) => { 99 | let matched = s.filematch[match_idx]; 100 | if matched.chars().next().unwrap() == '.' || 101 | name_idx + matched.len() == filename.len() { 102 | return Some(s) 103 | } 104 | }, 105 | Some((_, None)) => unreachable!(), 106 | None => continue, 107 | } 108 | } 109 | None 110 | } 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | extern crate libc; 2 | 3 | mod editor; 4 | mod terminal; 5 | mod util; 6 | 7 | use std::env; 8 | 9 | fn main() { 10 | terminal::enable_raw_mode(); 11 | let mut editor = editor::Editor::new(); 12 | 13 | let mut args = env::args(); 14 | if args.len() >= 2 { 15 | let filename = args.nth(1).unwrap(); 16 | editor.open_file(&filename); 17 | } 18 | 19 | editor.set_status_message("HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find"); 20 | 21 | loop { 22 | editor.refresh_screen(); 23 | editor.process_keypress(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/terminal.rs: -------------------------------------------------------------------------------- 1 | use libc; 2 | 3 | use std::io::{self, Read, Write}; 4 | use std::mem; 5 | use std::str; 6 | 7 | static mut ORIG_TERMIOS: Option = None; 8 | 9 | extern "C" fn disable_raw_mode() { 10 | unsafe { 11 | let mut termios = ORIG_TERMIOS.unwrap(); 12 | let errno = libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, &mut termios); 13 | if errno == -1 { panic!("tcsetattr") } 14 | } 15 | } 16 | 17 | pub fn enable_raw_mode() { 18 | unsafe { 19 | let mut termios: libc::termios = mem::zeroed(); 20 | 21 | let errno = libc::tcgetattr(libc::STDIN_FILENO, &mut termios as *mut libc::termios); 22 | if errno == -1 { panic!("tcgetattr") } 23 | 24 | ORIG_TERMIOS = Some(termios); 25 | libc::atexit(disable_raw_mode); 26 | 27 | termios.c_iflag &= !(libc::BRKINT | libc::ICRNL | libc::INPCK | libc::ISTRIP | libc::IXON); 28 | termios.c_oflag &= !libc::OPOST; 29 | termios.c_cflag |= libc::CS8; 30 | termios.c_lflag &= !(libc::ECHO | libc::ICANON | libc::IEXTEN | libc::ISIG); 31 | termios.c_cc[libc::VMIN] = 0; 32 | termios.c_cc[libc::VTIME] = 1; 33 | 34 | let errno = libc::tcsetattr(libc::STDIN_FILENO, libc::TCSAFLUSH, &mut termios); 35 | if errno == -1 { panic!("tcsetattr") } 36 | } 37 | } 38 | 39 | fn get_cursor_position() -> Option<(u16, u16)> { 40 | let _ = io::stdout().write(b"\x1b[6n"); 41 | let _ = io::stdout().flush(); 42 | let mut buffer = [0;32]; 43 | let _ = io::stdin().read(&mut buffer); 44 | let mut iter = str::from_utf8(&buffer[2..]).unwrap().split(|c| c == ';'); 45 | let rows: u16 = iter.next().unwrap() 46 | .parse().unwrap(); 47 | let cols = iter.next().unwrap() 48 | .split(|c| c == 'R') 49 | .next().unwrap() 50 | .parse().unwrap(); 51 | Some((rows, cols)) 52 | } 53 | 54 | pub fn get_window_size() -> Option<(u16, u16)> { 55 | unsafe { 56 | let mut ws: libc::winsize = mem::zeroed(); 57 | let errno = libc::ioctl(libc::STDOUT_FILENO, libc::TIOCGWINSZ, &mut ws); 58 | if errno == -1 || ws.ws_col == 0 { 59 | let bytes_written = io::stdout().write(b"\x1b[999C\x1b[999B"); 60 | if let Ok(12) = bytes_written { 61 | get_cursor_position() 62 | } else { 63 | None 64 | } 65 | } else { 66 | Some((ws.ws_row, ws.ws_col)) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | pub fn safe_truncate(string: &mut String, i: usize) { 2 | if string.len() <= i { 3 | return 4 | } else if string.is_char_boundary(i) { 5 | string.truncate(i) 6 | } else { 7 | safe_truncate(string, i - 1) 8 | } 9 | } 10 | 11 | pub fn safe_split_off(string: &mut String, i: usize) -> String { 12 | if string.len() <= i { 13 | String::new() 14 | } else if string.is_char_boundary(i) { 15 | string.split_off(i) 16 | } else { 17 | safe_split_off(string, i - 1) 18 | } 19 | } 20 | --------------------------------------------------------------------------------