├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── src ├── buffer │ ├── Cargo.toml │ ├── benches │ │ ├── bench_grapheme_iter.rs │ │ └── bench_paragraph_iter.rs │ ├── buffer.rs │ ├── char_iter.rs │ ├── cursor.rs │ ├── display_width.rs │ ├── extras │ │ ├── comment_out.rs │ │ ├── duplicate_lines.rs │ │ ├── edit_words.rs │ │ ├── expand_selections.rs │ │ ├── indent.rs │ │ ├── matching_brackets.rs │ │ ├── mod.rs │ │ ├── move_lines.rs │ │ ├── select_lines.rs │ │ └── truncate.rs │ ├── find.rs │ ├── grapheme_iter.rs │ ├── lib.rs │ ├── mut_raw_buffer.rs │ ├── paragraph_iter.rs │ ├── raw_buffer.rs │ ├── reflow_iter.rs │ ├── scroll.rs │ ├── syntax.rs │ └── word_iter.rs ├── common │ ├── Cargo.toml │ ├── dirs.rs │ ├── lib.rs │ └── logger.rs ├── compositor │ ├── Cargo.toml │ ├── canvas.rs │ ├── compositor.rs │ ├── lib.rs │ ├── surface.rs │ └── terminal.rs ├── editorconfig │ ├── Cargo.toml │ ├── detect_indent.rs │ └── lib.rs ├── languages │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ ├── languages.rs │ └── lib.rs └── noa │ ├── Cargo.toml │ ├── actions │ ├── basic_editing.rs │ ├── change_case.rs │ ├── goto.rs │ ├── linemap.rs │ ├── mod.rs │ └── scrolling.rs │ ├── clipboard.rs │ ├── config.rs │ ├── defaults.toml │ ├── document.rs │ ├── editor.rs │ ├── main.rs │ ├── notification.rs │ └── views │ ├── buffer_view.rs │ ├── metaline_view.rs │ ├── mod.rs │ └── snapshots │ └── noa__views__buffer_view__tests__with_softwrap.snap └── test.c /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | [{*.rs,*.md}] 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | build: 11 | name: ${{ matrix.job }} ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | timeout-minutes: 30 14 | strategy: 15 | matrix: 16 | include: 17 | - os: macos-latest 18 | - os: ubuntu-latest 19 | steps: 20 | - name: Clone the repository 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 1 24 | 25 | - name: Build (release) 26 | uses: actions-rs/cargo@v1 27 | with: 28 | command: build 29 | args: --release 30 | 31 | lint: 32 | name: Lint 33 | runs-on: ubuntu-latest 34 | timeout-minutes: 30 35 | steps: 36 | - name: Clone the repository 37 | uses: actions/checkout@v3 38 | with: 39 | fetch-depth: 1 40 | 41 | - name: Install Rust toolchain 42 | uses: actions-rs/toolchain@v1 43 | with: 44 | toolchain: nightly 45 | override: true 46 | components: rustfmt, clippy 47 | 48 | - name: Create a dummy file 49 | run: touch src/languages/tree_sitter.rs 50 | 51 | - name: Formatting check (cargo fmt) 52 | uses: actions-rs/cargo@v1 53 | with: 54 | command: fmt 55 | args: --all -- --check 56 | 57 | - name: Compiler checks (cargo check) 58 | uses: actions-rs/cargo@v1 59 | with: 60 | command: check 61 | 62 | - name: Clippy checks 63 | uses: actions-rs/cargo@v1 64 | with: 65 | command: clippy 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /perf.data* 3 | /flamegraph.svg 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "src/*", 4 | ] 5 | 6 | [profile.release] 7 | debug = true 8 | opt-level = 3 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # noa 2 | [![CI](https://github.com/nuta/noa/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/nuta/noa/actions/workflows/ci.yml) 3 | 4 | A minimalistic terminal text editor. Aims to be my daily driver and be a good alternative to GNU nano. 5 | 6 | ## Features 7 | 8 | - Grapheme-aware text editing with multiple cursors. 9 | - No distraction: let you focus on coding. 10 | 11 | ## TODOs 12 | 13 | - [ ] Fix TODOs 14 | - [ ] Backup 15 | - [ ] Copy and paste 16 | - [ ] Search file paths 17 | - [ ] Search file contents 18 | - [ ] Ask before quitting if the buffer is dirty 19 | - [ ] Mouse support 20 | - [ ] Tree-sitter support 21 | - [ ] auto complete 22 | 23 | ## License 24 | 25 | MIT or Apache 2.0. Choose whichever you prefer. 26 | -------------------------------------------------------------------------------- /src/buffer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noa_buffer" 3 | version = "0.0.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | path = "lib.rs" 8 | 9 | [dependencies] 10 | log = "0" 11 | ropey = "^1.3.2" 12 | unicode-width = "0" 13 | unicode-segmentation = "1" 14 | tempfile = "3" 15 | 16 | noa_editorconfig = { path = "../editorconfig" } 17 | noa_languages = { path = "../languages" } 18 | 19 | [dev-dependencies] 20 | pretty_assertions = "1" 21 | -------------------------------------------------------------------------------- /src/buffer/benches/bench_grapheme_iter.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use noa_buffer::cursor::*; 6 | use noa_buffer::raw_buffer::*; 7 | 8 | #[bench] 9 | fn bench_iterate_next_graphemes(b: &mut test::Bencher) { 10 | // A grapheme ("u" with some marks), consists of U+0075 U+0308 U+0304. 11 | let buffer = RawBuffer::from_text(&"\u{0075}\u{0308}\u{0304}".repeat(128)); 12 | 13 | b.iter(|| { 14 | let mut iter = buffer.grapheme_iter(Position::new(0, 0)); 15 | for grapheme in iter.by_ref() { 16 | test::black_box(grapheme); 17 | } 18 | }); 19 | } 20 | 21 | #[bench] 22 | fn bench_iterate_next_graphemes_biredictional(b: &mut test::Bencher) { 23 | // A grapheme ("u" with some marks), consists of U+0075 U+0308 U+0304. 24 | let buffer = RawBuffer::from_text(&"\u{0075}\u{0308}\u{0304}".repeat(128)); 25 | 26 | b.iter(|| { 27 | let mut iter = buffer.bidirectional_grapheme_iter(Position::new(0, 0)); 28 | for grapheme in iter.by_ref() { 29 | test::black_box(grapheme); 30 | } 31 | }); 32 | } 33 | 34 | #[bench] 35 | fn bench_iterate_prev_graphemes_biredictional(b: &mut test::Bencher) { 36 | // A grapheme ("u" with some marks), consists of U+0075 U+0308 U+0304. 37 | let buffer = RawBuffer::from_text(&"\u{0075}\u{0308}\u{0304}".repeat(128)); 38 | 39 | b.iter(|| { 40 | let mut iter = buffer.bidirectional_grapheme_iter(Position::new(0, 3 * 128)); 41 | while let Some(grapheme) = iter.prev() { 42 | test::black_box(grapheme); 43 | } 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/buffer/benches/bench_paragraph_iter.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | 5 | use noa_buffer::cursor::*; 6 | use noa_buffer::raw_buffer::*; 7 | 8 | #[bench] 9 | fn bench_iterate_next_paragraph(b: &mut test::Bencher) { 10 | let buffer = RawBuffer::from_text( 11 | &"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n" 12 | .repeat(256), 13 | ); 14 | 15 | b.iter(|| { 16 | let mut iter = buffer.paragraph_iter(Position::new(0, 0), 60, 4); 17 | for paragraph in iter.by_ref() { 18 | test::black_box(paragraph); 19 | } 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/buffer/char_iter.rs: -------------------------------------------------------------------------------- 1 | use crate::{cursor::Position, raw_buffer::RawBuffer}; 2 | 3 | #[derive(Clone)] 4 | pub struct CharIter<'a> { 5 | iter: ropey::iter::Chars<'a>, 6 | buf: &'a RawBuffer, 7 | next_pos: Position, 8 | last_pos: Position, 9 | } 10 | 11 | impl<'a> CharIter<'a> { 12 | pub fn new(iter: ropey::iter::Chars<'a>, buf: &'a RawBuffer, pos: Position) -> CharIter<'a> { 13 | CharIter { 14 | iter, 15 | buf, 16 | next_pos: pos, 17 | last_pos: pos, 18 | } 19 | } 20 | 21 | pub fn next_position(&self) -> Position { 22 | self.next_pos 23 | } 24 | 25 | pub fn last_position(&self) -> Position { 26 | self.last_pos 27 | } 28 | 29 | pub fn buffer(&self) -> &'a RawBuffer { 30 | self.buf 31 | } 32 | 33 | /// Returns the previous character. 34 | /// 35 | /// # Complexity 36 | /// 37 | /// From ropey's documentation: 38 | /// 39 | /// > Runs in amortized O(1) time and worst-case O(log N) time. 40 | pub fn prev(&mut self) -> Option { 41 | let ch = self.iter.prev(); 42 | match ch { 43 | Some('\n') => { 44 | self.next_pos.y -= 1; 45 | self.next_pos.x = self.buf.line_len(self.next_pos.y); 46 | } 47 | Some('\r') => { 48 | // Do nothing. 49 | } 50 | Some(_) => { 51 | self.next_pos.x = self.next_pos.x.saturating_sub(1); 52 | } 53 | None => { 54 | // Do nothing. 55 | } 56 | } 57 | self.last_pos = self.next_pos; 58 | ch 59 | } 60 | } 61 | 62 | impl Iterator for CharIter<'_> { 63 | type Item = char; 64 | 65 | /// Returns the next character. 66 | /// 67 | /// # Complexity 68 | /// 69 | /// From ropey's documentation: 70 | /// 71 | /// > Runs in amortized O(1) time and worst-case O(log N) time. 72 | fn next(&mut self) -> Option { 73 | let ch = self.iter.next(); 74 | self.last_pos = self.next_pos; 75 | match ch { 76 | Some('\n') => { 77 | self.next_pos.y += 1; 78 | self.next_pos.x = 0; 79 | } 80 | Some('\r') => { 81 | // Do nothing. 82 | } 83 | Some(_) => { 84 | self.next_pos.x += 1; 85 | } 86 | None => { 87 | // Do nothing. 88 | } 89 | } 90 | ch 91 | } 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | use pretty_assertions::assert_eq; 97 | 98 | use super::*; 99 | 100 | #[test] 101 | fn test_char_iter() { 102 | let buffer = RawBuffer::from_text("ABC"); 103 | let mut iter = buffer.char_iter(Position::new(0, 1)); 104 | assert_eq!(iter.last_position(), Position::new(0, 1)); 105 | assert_eq!(iter.next(), Some('B')); 106 | assert_eq!(iter.last_position(), Position::new(0, 1)); 107 | assert_eq!(iter.next(), Some('C')); 108 | assert_eq!(iter.last_position(), Position::new(0, 2)); 109 | assert_eq!(iter.next(), None); 110 | assert_eq!(iter.last_position(), Position::new(0, 3)); 111 | assert_eq!(iter.prev(), Some('C')); 112 | assert_eq!(iter.last_position(), Position::new(0, 2)); 113 | assert_eq!(iter.prev(), Some('B')); 114 | assert_eq!(iter.last_position(), Position::new(0, 1)); 115 | assert_eq!(iter.prev(), Some('A')); 116 | assert_eq!(iter.last_position(), Position::new(0, 0)); 117 | assert_eq!(iter.next(), Some('A')); 118 | assert_eq!(iter.last_position(), Position::new(0, 0)); 119 | assert_eq!(iter.next(), Some('B')); 120 | assert_eq!(iter.last_position(), Position::new(0, 1)); 121 | } 122 | 123 | #[test] 124 | fn newline() { 125 | let buffer = RawBuffer::from_text("A\nB"); 126 | let mut iter = buffer.char_iter(Position::new(0, 0)); 127 | assert_eq!(iter.next(), Some('A')); 128 | assert_eq!(iter.last_position(), Position::new(0, 0)); 129 | assert_eq!(iter.next(), Some('\n')); 130 | assert_eq!(iter.last_position(), Position::new(0, 1)); 131 | assert_eq!(iter.next(), Some('B')); 132 | assert_eq!(iter.last_position(), Position::new(1, 0)); 133 | assert_eq!(iter.prev(), Some('B')); 134 | assert_eq!(iter.last_position(), Position::new(1, 0)); 135 | assert_eq!(iter.prev(), Some('\n')); 136 | assert_eq!(iter.last_position(), Position::new(0, 1)); 137 | assert_eq!(iter.prev(), Some('A')); 138 | assert_eq!(iter.last_position(), Position::new(0, 0)); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/buffer/display_width.rs: -------------------------------------------------------------------------------- 1 | pub trait DisplayWidth { 2 | fn display_width(&self) -> usize; 3 | } 4 | 5 | impl DisplayWidth for str { 6 | fn display_width(&self) -> usize { 7 | unicode_width::UnicodeWidthStr::width_cjk(self) 8 | } 9 | } 10 | 11 | impl DisplayWidth for char { 12 | fn display_width(&self) -> usize { 13 | unicode_width::UnicodeWidthChar::width_cjk(*self).unwrap_or(1) 14 | } 15 | } 16 | 17 | impl DisplayWidth for usize { 18 | fn display_width(&self) -> usize { 19 | let mut n = *self; 20 | match n { 21 | 0..=9 => 1, 22 | 10..=99 => 2, 23 | 100..=999 => 3, 24 | 1000..=9999 => 4, 25 | 10000..=99999 => 5, 26 | _ => { 27 | let mut num = 1; 28 | loop { 29 | n /= 10; 30 | if n == 0 { 31 | break; 32 | } 33 | num += 1; 34 | } 35 | num 36 | } 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | use super::*; 44 | 45 | #[test] 46 | fn display_width() { 47 | assert_eq!(0.display_width(), 1); 48 | assert_eq!(10.display_width(), 2); 49 | assert_eq!(101.display_width(), 3); 50 | assert_eq!('a'.display_width(), 1); 51 | assert_eq!("a ".display_width(), 4); 52 | assert_eq!("aあb".display_width(), 4); 53 | assert_eq!("こんにちは".display_width(), 2 * 5); 54 | assert_eq!("こんにちは".display_width(), 2 * 5); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/buffer/extras/comment_out.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::min, collections::HashMap}; 2 | 3 | use crate::{ 4 | buffer::Buffer, 5 | cursor::{Position, Range}, 6 | }; 7 | 8 | impl Buffer { 9 | pub fn toggle_line_comment_out(&mut self) { 10 | let keyword_without_whitespace = match self.language().line_comment.as_ref() { 11 | Some(keyword) => *keyword, 12 | None => return, 13 | }; 14 | let keyword_with_whitespace = format!("{} ", keyword_without_whitespace); 15 | let keyword_without_whitespace_len = keyword_without_whitespace.chars().count(); 16 | let keyword_with_whitespace_len = keyword_with_whitespace.chars().count(); 17 | 18 | let mut target_lines = Vec::new(); 19 | for c in self.cursors() { 20 | let ys = c.selection().overlapped_lines(); 21 | if ys.is_empty() { 22 | target_lines.push(c.front().y); 23 | } else { 24 | for y in ys { 25 | target_lines.push(y); 26 | } 27 | } 28 | } 29 | 30 | let increment_comment = target_lines.iter().any(|y| { 31 | !self 32 | .buf 33 | .line_text(*y) 34 | .trim_start() 35 | .starts_with(keyword_without_whitespace) 36 | }); 37 | 38 | // Add/remove comment outs. 39 | let mut x_diffs = HashMap::new(); 40 | for y in target_lines { 41 | let current_indent_len = self.buf.line_indent_len(y); 42 | let pos_after_indent = Position::new(y, current_indent_len); 43 | let eol = Position::new(y, self.buf.line_len(y)); 44 | let stripped_line_text = self.substr(Range::from_positions(pos_after_indent, eol)); 45 | 46 | if increment_comment { 47 | self.buf.edit( 48 | Range::from_positions(pos_after_indent, pos_after_indent), 49 | &keyword_with_whitespace, 50 | ); 51 | } else if stripped_line_text.starts_with(&keyword_with_whitespace) { 52 | let end = Position::new(y, current_indent_len + keyword_with_whitespace_len); 53 | self.buf 54 | .edit(Range::from_positions(pos_after_indent, end), ""); 55 | x_diffs.insert(y, keyword_with_whitespace_len); 56 | } else if stripped_line_text.starts_with(keyword_without_whitespace) { 57 | let end = Position::new(y, current_indent_len + keyword_without_whitespace_len); 58 | self.buf 59 | .edit(Range::from_positions(pos_after_indent, end), ""); 60 | x_diffs.insert(y, keyword_without_whitespace_len); 61 | } 62 | } 63 | 64 | // Adjust cursors. 65 | self.cursors.foreach(|c, _| { 66 | let range = c.selection_mut(); 67 | let x_diff = x_diffs.get(&range.start.y).copied().unwrap_or(0); 68 | if increment_comment { 69 | range.start.x = min( 70 | range.start.x.saturating_sub(x_diff), 71 | self.buf.line_len(range.start.y), 72 | ); 73 | range.end.x = min( 74 | range.end.x.saturating_sub(x_diff), 75 | self.buf.line_len(range.end.y), 76 | ); 77 | } else { 78 | range.start.x = min(range.start.x + (x_diff), self.buf.line_len(range.start.y)); 79 | range.end.x = min(range.end.x + (x_diff), self.buf.line_len(range.end.y)); 80 | } 81 | }); 82 | } 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use crate::cursor::Cursor; 88 | use noa_languages::get_language_by_name; 89 | use pretty_assertions::assert_eq; 90 | 91 | use super::*; 92 | 93 | #[test] 94 | fn test_comment_out() { 95 | let lang = get_language_by_name("rust").unwrap(); 96 | 97 | let mut buffer = Buffer::from_text(""); 98 | buffer.set_language(lang).unwrap(); 99 | buffer.set_cursors_for_test(&[Cursor::new(0, 0)]); 100 | buffer.toggle_line_comment_out(); 101 | assert_eq!(buffer.text(), "// "); 102 | 103 | let mut buffer = Buffer::from_text("abc"); 104 | buffer.set_language(lang).unwrap(); 105 | buffer.set_cursors_for_test(&[Cursor::new(0, 0)]); 106 | buffer.toggle_line_comment_out(); 107 | assert_eq!(buffer.text(), "// abc"); 108 | 109 | let mut buffer = Buffer::from_text(" abc"); 110 | buffer.set_language(lang).unwrap(); 111 | buffer.set_cursors_for_test(&[Cursor::new(0, 0)]); 112 | buffer.toggle_line_comment_out(); 113 | assert_eq!(buffer.text(), " // abc"); 114 | 115 | let mut buffer = Buffer::from_text(" abc\n def"); 116 | buffer.set_language(lang).unwrap(); 117 | buffer.set_cursors_for_test(&[Cursor::new_selection(0, 0, 2, 0)]); 118 | buffer.toggle_line_comment_out(); 119 | assert_eq!(buffer.text(), " // abc\n // def"); 120 | 121 | let mut buffer = Buffer::from_text(" abc\n // def"); 122 | buffer.set_language(lang).unwrap(); 123 | buffer.set_cursors_for_test(&[Cursor::new_selection(0, 0, 2, 0)]); 124 | buffer.toggle_line_comment_out(); 125 | assert_eq!(buffer.text(), " // abc\n // // def"); 126 | } 127 | 128 | #[test] 129 | fn test_uncomment_out() { 130 | let lang = get_language_by_name("rust").unwrap(); 131 | 132 | let mut buffer = Buffer::from_text("//"); 133 | buffer.set_language(lang).unwrap(); 134 | buffer.set_cursors_for_test(&[Cursor::new(0, 0)]); 135 | buffer.toggle_line_comment_out(); 136 | assert_eq!(buffer.text(), ""); 137 | 138 | let mut buffer = Buffer::from_text("// abc"); 139 | buffer.set_language(lang).unwrap(); 140 | buffer.set_cursors_for_test(&[Cursor::new(0, 6)]); 141 | buffer.toggle_line_comment_out(); 142 | assert_eq!(buffer.text(), "abc"); 143 | assert_eq!(buffer.cursors(), &[Cursor::new(0, 3)]); 144 | 145 | let mut buffer = Buffer::from_text(" // abc"); 146 | buffer.set_language(lang).unwrap(); 147 | buffer.set_cursors_for_test(&[Cursor::new(0, 10)]); 148 | buffer.toggle_line_comment_out(); 149 | assert_eq!(buffer.text(), " abc"); 150 | assert_eq!(buffer.cursors(), &[Cursor::new(0, 7)]); 151 | 152 | let mut buffer = Buffer::from_text(" // abc\n // def"); 153 | buffer.set_language(lang).unwrap(); 154 | buffer.set_cursors_for_test(&[Cursor::new_selection(0, 0, 2, 0)]); 155 | buffer.toggle_line_comment_out(); 156 | assert_eq!(buffer.text(), " abc\n def"); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/buffer/extras/duplicate_lines.rs: -------------------------------------------------------------------------------- 1 | use crate::buffer::Buffer; 2 | 3 | impl Buffer { 4 | pub fn duplicate_lines_up(&mut self) { 5 | self.cursors.foreach(|c, past_cursors| { 6 | let s = c.selection(); 7 | 8 | c.select_overlapped_lines(); 9 | let mut text = self.buf.substr(c.selection()); 10 | if !text.ends_with('\n') { 11 | text.push('\n'); 12 | } 13 | 14 | c.move_to(s.front().y, 0); 15 | self.buf.edit_at_cursor(c, past_cursors, &text); 16 | c.select_range(s); 17 | }); 18 | } 19 | 20 | pub fn duplicate_lines_down(&mut self) { 21 | self.cursors.foreach(|c, past_cursors| { 22 | let s = c.selection(); 23 | 24 | c.select_overlapped_lines(); 25 | let mut text = self.buf.substr(c.selection()); 26 | let num_lines = text.trim_end().matches('\n').count() + 1; 27 | if !text.ends_with('\n') { 28 | text.push('\n'); 29 | } 30 | 31 | c.move_to(c.front().y, 0); 32 | self.buf.edit_at_cursor(c, past_cursors, &text); 33 | c.select( 34 | s.start.y + num_lines, 35 | s.start.x, 36 | s.end.y + num_lines, 37 | s.end.x, 38 | ); 39 | }); 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use crate::cursor::Cursor; 46 | 47 | use super::*; 48 | use pretty_assertions::assert_eq; 49 | 50 | #[test] 51 | fn duplicate_a_line_up() { 52 | let mut b = Buffer::from_text(""); 53 | b.set_cursors_for_test(&[Cursor::new(0, 0)]); 54 | b.duplicate_lines_up(); 55 | assert_eq!(b.text(), "\n"); 56 | assert_eq!(b.cursors(), &[Cursor::new(0, 0)]); 57 | 58 | let mut b = Buffer::from_text("abcd"); 59 | b.set_cursors_for_test(&[Cursor::new(0, 2)]); 60 | b.duplicate_lines_up(); 61 | assert_eq!(b.text(), "abcd\nabcd"); 62 | assert_eq!(b.cursors(), &[Cursor::new(0, 2)]); 63 | 64 | let mut b = Buffer::from_text("abcd\nxyz"); 65 | b.set_cursors_for_test(&[Cursor::new(1, 2)]); 66 | b.duplicate_lines_up(); 67 | assert_eq!(b.text(), "abcd\nxyz\nxyz"); 68 | assert_eq!(b.cursors(), &[Cursor::new(1, 2)]); 69 | } 70 | 71 | #[test] 72 | fn duplicate_multiple_lines_up() { 73 | // ABCD 74 | // EFGH 75 | // ---- 76 | let mut b = Buffer::from_text("ABCD\nEFGH\n----"); 77 | b.set_cursors_for_test(&[Cursor::new_selection(0, 1, 1, 0)]); 78 | b.duplicate_lines_up(); 79 | assert_eq!(b.text(), "ABCD\nABCD\nEFGH\n----"); 80 | assert_eq!(b.cursors(), &[Cursor::new_selection(0, 1, 1, 0)]); 81 | 82 | // ABCD 83 | // EFGH 84 | // ---- 85 | let mut b = Buffer::from_text("ABCD\nEFGH\n----"); 86 | b.set_cursors_for_test(&[Cursor::new_selection(0, 1, 2, 4)]); 87 | b.duplicate_lines_up(); 88 | assert_eq!(b.text(), "ABCD\nEFGH\n----\nABCD\nEFGH\n----"); 89 | assert_eq!(b.cursors(), &[Cursor::new_selection(0, 1, 2, 4)]); 90 | } 91 | 92 | #[test] 93 | fn duplicate_a_line_down() { 94 | let mut b = Buffer::from_text(""); 95 | b.set_cursors_for_test(&[Cursor::new(0, 0)]); 96 | b.duplicate_lines_down(); 97 | assert_eq!(b.text(), "\n"); 98 | assert_eq!(b.cursors(), &[Cursor::new(1, 0)]); 99 | 100 | let mut b = Buffer::from_text("abcd"); 101 | b.set_cursors_for_test(&[Cursor::new(0, 2)]); 102 | b.duplicate_lines_down(); 103 | assert_eq!(b.text(), "abcd\nabcd"); 104 | assert_eq!(b.cursors(), &[Cursor::new(1, 2)]); 105 | 106 | let mut b = Buffer::from_text("abcd\nxyz"); 107 | b.set_cursors_for_test(&[Cursor::new(1, 2)]); 108 | b.duplicate_lines_down(); 109 | assert_eq!(b.text(), "abcd\nxyz\nxyz"); 110 | assert_eq!(b.cursors(), &[Cursor::new(2, 2)]); 111 | } 112 | 113 | #[test] 114 | fn duplicate_multiple_lines_down() { 115 | // ABCD 116 | // EFGH 117 | // ---- 118 | let mut b = Buffer::from_text("ABCD\nEFGH\n----"); 119 | b.set_cursors_for_test(&[Cursor::new_selection(0, 1, 1, 0)]); 120 | b.duplicate_lines_down(); 121 | assert_eq!(b.text(), "ABCD\nABCD\nEFGH\n----"); 122 | assert_eq!(b.cursors(), &[Cursor::new_selection(1, 1, 2, 0)]); 123 | 124 | // ABCD 125 | // EFGH 126 | // ---- 127 | let mut b = Buffer::from_text("ABCD\nEFGH\n----"); 128 | b.set_cursors_for_test(&[Cursor::new_selection(0, 1, 2, 4)]); 129 | b.duplicate_lines_down(); 130 | assert_eq!(b.text(), "ABCD\nEFGH\n----\nABCD\nEFGH\n----"); 131 | assert_eq!(b.cursors(), &[Cursor::new_selection(3, 1, 5, 4)]); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/buffer/extras/edit_words.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | buffer::Buffer, 3 | cursor::{Cursor, Range}, 4 | word_iter::Word, 5 | }; 6 | 7 | impl Buffer { 8 | pub fn current_word_str(&self) -> Option { 9 | let c = self.main_cursor(); 10 | if c.is_selection() { 11 | return None; 12 | } 13 | 14 | self.current_word(c.moving_position()) 15 | .map(|range| self.substr(range)) 16 | } 17 | 18 | pub fn delete_current_word(&mut self) { 19 | self.select_current_word(); 20 | self.delete_if_not_empty(); 21 | } 22 | 23 | pub fn select_current_word(&mut self) { 24 | self.update_cursors_with(|c, buffer| { 25 | if let Some(selection) = buffer.current_word(c.moving_position()) { 26 | c.select_range(selection); 27 | } 28 | }); 29 | } 30 | 31 | pub fn select_next_word(&mut self) { 32 | self.update_cursors_with_next_word(|c, word| { 33 | c.move_moving_position_to(word.range().back()) 34 | }); 35 | } 36 | 37 | pub fn select_prev_word(&mut self) { 38 | self.update_cursors_with_prev_word(|c, word| { 39 | c.move_moving_position_to(word.range().front()) 40 | }); 41 | } 42 | 43 | pub fn move_to_next_word(&mut self) { 44 | self.update_cursors_with_next_word(|c, word| c.move_to_pos(word.range().back())); 45 | } 46 | 47 | pub fn move_to_prev_word(&mut self) { 48 | self.update_cursors_with_prev_word(|c, word| c.move_to_pos(word.range().front())); 49 | } 50 | 51 | pub fn backspace_word(&mut self) { 52 | self.cursors.foreach(|c, past_cursors| { 53 | if c.selection().is_empty() { 54 | // Select the previous word. 55 | let back_pos = c.moving_position(); 56 | let mut char_iter = self.buf.char_iter(back_pos); 57 | 58 | fn is_whitespace_pred(c: char) -> bool { 59 | matches!(c, ' ' | '\t' | '\n') 60 | } 61 | 62 | fn is_symbols_pred(c: char) -> bool { 63 | "!@#$%^&*()-=+[]{}\\|;:'\",.<>/?".contains(c) 64 | } 65 | 66 | fn others_pred(c: char) -> bool { 67 | !is_whitespace_pred(c) && !is_symbols_pred(c) 68 | } 69 | 70 | let pred = match char_iter.prev() { 71 | Some(c) if is_whitespace_pred(c) => is_whitespace_pred, 72 | Some(c) if is_symbols_pred(c) => is_symbols_pred, 73 | _ => others_pred, 74 | }; 75 | 76 | let front_pos = loop { 77 | let pos = char_iter.last_position(); 78 | match char_iter.prev() { 79 | Some(c) if pred(c) => continue, 80 | _ => break pos, 81 | } 82 | }; 83 | 84 | c.select_range(Range::from_positions(front_pos, back_pos)); 85 | } 86 | 87 | self.buf.edit_at_cursor(c, past_cursors, ""); 88 | }); 89 | } 90 | 91 | fn update_cursors_with_next_word(&mut self, callback: F) 92 | where 93 | F: Fn(&mut Cursor, Word), 94 | { 95 | self.update_cursors_with(|c, buffer| { 96 | let pos = c.moving_position(); 97 | let mut word_iter = buffer.word_iter_from_beginning_of_word(pos); 98 | if let Some(word) = word_iter.next() { 99 | if pos == word.range().back() { 100 | // Move to the next word. 101 | if let Some(next_word) = word_iter.next() { 102 | callback(c, next_word); 103 | } 104 | } else { 105 | // Move to the end of the current word. 106 | callback(c, word); 107 | } 108 | } 109 | }); 110 | } 111 | 112 | fn update_cursors_with_prev_word(&mut self, callback: F) 113 | where 114 | F: Fn(&mut Cursor, Word), 115 | { 116 | self.update_cursors_with(|c, buffer| { 117 | let pos = c.moving_position(); 118 | let mut word_iter = buffer.word_iter_from_end_of_word(pos); 119 | if let Some(word) = word_iter.prev() { 120 | if pos == word.range().front() { 121 | // Move to the next word. 122 | if let Some(prev_word) = word_iter.prev() { 123 | callback(c, prev_word); 124 | } 125 | } else { 126 | // Move to the beginning of the current word. 127 | callback(c, word); 128 | } 129 | } 130 | }); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/buffer/extras/expand_selections.rs: -------------------------------------------------------------------------------- 1 | use noa_languages::tree_sitter; 2 | 3 | use crate::{buffer::Buffer, cursor::Range, syntax::TsNodeExt}; 4 | 5 | impl Buffer { 6 | pub fn expand_selections(&mut self) { 7 | self.update_cursors_with(|c, buf| { 8 | if let Some(syntax) = buf.syntax() { 9 | let root = syntax.tree().root_node(); 10 | let new_selection = walk_ts_node(root, &mut root.walk(), c.selection()); 11 | c.select_range(new_selection); 12 | } 13 | }); 14 | } 15 | } 16 | 17 | fn walk_ts_node<'tree>( 18 | parent: tree_sitter::Node<'tree>, 19 | cursor: &mut tree_sitter::TreeCursor<'tree>, 20 | selection: Range, 21 | ) -> Range { 22 | for node in parent.children(cursor) { 23 | let range = node.buffer_range(); 24 | if range.contains_range(selection) && range != selection { 25 | // A child node may contain narrower selection than `range`. 26 | return walk_ts_node(node, &mut node.walk(), selection); 27 | } 28 | } 29 | 30 | parent.buffer_range() 31 | } 32 | 33 | #[cfg(test)] 34 | mod tests { 35 | use std::borrow::Cow; 36 | 37 | use crate::{cursor::Cursor, syntax::SyntaxParser}; 38 | 39 | use super::*; 40 | use noa_languages::get_language_by_name; 41 | use pretty_assertions::assert_eq; 42 | 43 | fn selected_str(buf: &Buffer) -> Cow<'_, str> { 44 | Cow::from(buf.substr(buf.cursors()[0].selection())) 45 | } 46 | 47 | #[test] 48 | fn expand_selections() { 49 | let mut b = Buffer::from_text(""); 50 | let lang = get_language_by_name("rust").unwrap(); 51 | b.set_language(lang).unwrap(); 52 | let mut parser = SyntaxParser::new(lang).unwrap(); 53 | parser.parse_fully(b.raw_buffer()); 54 | b.set_syntax_tree(parser.tree().clone()); 55 | b.set_cursors_for_test(&[Cursor::new(0, 0)]); 56 | b.expand_selections(); 57 | assert_eq!(selected_str(&b), ""); 58 | 59 | // source_file [0, 0] - [5, 0] 60 | // function_item [0, 0] - [4, 1] 61 | // visibility_modifier [0, 0] - [0, 3] 62 | // name: identifier [0, 7] - [0, 11] 63 | // parameters: parameters [0, 11] - [0, 13] 64 | // body: block [0, 14] - [4, 1] 65 | // if_expression [1, 4] - [3, 5] 66 | // condition: boolean_literal [1, 7] - [1, 11] 67 | // consequence: block [1, 12] - [3, 5] 68 | // macro_invocation [2, 8] - [2, 32] 69 | // macro: identifier [2, 8] - [2, 11] 70 | // token_tree [2, 12] - [2, 32] 71 | // identifier [2, 13] - [2, 16] 72 | // token_tree [2, 17] - [2, 31] 73 | // integer_literal [2, 18] - [2, 21] 74 | // integer_literal [2, 24] - [2, 25] 75 | // integer_literal [2, 27] - [2, 30] 76 | let mut b = Buffer::from_text(concat!( 77 | "pub fn main() {\n", 78 | " if true {\n", 79 | " dbg!(vec![123 + 0, 456]);\n", 80 | " }\n", 81 | "}\n", 82 | )); 83 | let lang = get_language_by_name("rust").unwrap(); 84 | b.set_language(lang).unwrap(); 85 | let mut parser = SyntaxParser::new(lang).unwrap(); 86 | parser.parse_fully(b.raw_buffer()); 87 | b.set_syntax_tree(parser.tree().clone()); 88 | 89 | // The cursor is located in "123". 90 | b.set_cursors_for_test(&[Cursor::new(2, 21)]); 91 | 92 | b.expand_selections(); 93 | assert_eq!(selected_str(&b), "123"); 94 | b.expand_selections(); 95 | assert_eq!(selected_str(&b), "[123 + 0, 456]"); 96 | b.expand_selections(); 97 | assert_eq!(selected_str(&b), "(vec![123 + 0, 456])"); 98 | b.expand_selections(); 99 | assert_eq!(selected_str(&b), "dbg!(vec![123 + 0, 456])"); 100 | b.expand_selections(); 101 | assert_eq!(selected_str(&b), "dbg!(vec![123 + 0, 456]);"); 102 | b.expand_selections(); 103 | assert_eq!( 104 | selected_str(&b), 105 | "{\n dbg!(vec![123 + 0, 456]);\n }" 106 | ); 107 | b.expand_selections(); 108 | assert_eq!( 109 | selected_str(&b), 110 | "if true {\n dbg!(vec![123 + 0, 456]);\n }" 111 | ); 112 | b.expand_selections(); 113 | assert_eq!( 114 | selected_str(&b), 115 | "{\n if true {\n dbg!(vec![123 + 0, 456]);\n }\n}" 116 | ); 117 | b.expand_selections(); 118 | assert_eq!( 119 | selected_str(&b), 120 | "pub fn main() {\n if true {\n dbg!(vec![123 + 0, 456]);\n }\n}" 121 | ); 122 | b.expand_selections(); 123 | assert_eq!( 124 | selected_str(&b), 125 | "pub fn main() {\n if true {\n dbg!(vec![123 + 0, 456]);\n }\n}\n" 126 | ); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/buffer/extras/matching_brackets.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | buffer::Buffer, 3 | char_iter::CharIter, 4 | cursor::{Position, Range}, 5 | }; 6 | 7 | impl Buffer { 8 | pub fn matching_bracket(&mut self, pos: Position) -> Option { 9 | let (mut char_iter, opening, start_ch, end_ch) = self.find_bracket_nearby(pos)?; 10 | debug_assert_ne!(start_ch, end_ch); 11 | 12 | let start_pos = char_iter.last_position(); 13 | let mut nested = 0; 14 | while let Some(ch) = if opening { 15 | char_iter.next() 16 | } else { 17 | char_iter.prev() 18 | } { 19 | if start_pos == char_iter.last_position() { 20 | continue; 21 | } 22 | 23 | if ch == start_ch { 24 | nested += 1; 25 | } 26 | 27 | if ch == end_ch { 28 | if nested == 0 { 29 | let start = char_iter.last_position(); 30 | let end = Position::new(start.y, start.x + 1); 31 | return Some(Range::from_positions(start, end)); 32 | } else { 33 | nested -= 1; 34 | } 35 | } 36 | } 37 | 38 | None 39 | } 40 | 41 | pub fn find_bracket_nearby(&mut self, pos: Position) -> Option<(CharIter, bool, char, char)> { 42 | let get_corresponding_char = |c: char| match c { 43 | '(' => Some((true, ')')), 44 | '{' => Some((true, '}')), 45 | '[' => Some((true, ']')), 46 | '<' => Some((true, '>')), 47 | ')' => Some((false, '(')), 48 | '}' => Some((false, '{')), 49 | ']' => Some((false, '[')), 50 | '>' => Some((false, '<')), 51 | _ => None, 52 | }; 53 | 54 | // Try the next character. 55 | let mut char_iter = self.char_iter(pos); 56 | if let Some(c) = char_iter.next() { 57 | if let Some((opening, correspond_ch)) = get_corresponding_char(c) { 58 | return Some((char_iter, opening, c, correspond_ch)); 59 | } 60 | } 61 | 62 | // Try the previous character. 63 | let mut char_iter = self.char_iter(pos); 64 | if let Some(c) = char_iter.prev() { 65 | if let Some((opening, correspond_ch)) = get_corresponding_char(c) { 66 | return Some((char_iter, opening, c, correspond_ch)); 67 | } 68 | } 69 | 70 | None 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | use pretty_assertions::assert_eq; 78 | 79 | #[test] 80 | fn matching_bracket() { 81 | let mut b = Buffer::from_text(""); 82 | assert_eq!(b.matching_bracket(Position::new(0, 0)), None); 83 | 84 | let mut b = Buffer::from_text("{}"); 85 | assert_eq!( 86 | b.matching_bracket(Position::new(0, 0)), 87 | Some(Range::new(0, 1, 0, 2)) 88 | ); 89 | assert_eq!( 90 | b.matching_bracket(Position::new(0, 1)), 91 | Some(Range::new(0, 0, 0, 1)) 92 | ); 93 | 94 | let mut b = Buffer::from_text("{{{}}}"); 95 | assert_eq!( 96 | b.matching_bracket(Position::new(0, 0)), 97 | Some(Range::new(0, 5, 0, 6)) 98 | ); 99 | assert_eq!( 100 | b.matching_bracket(Position::new(0, 1)), 101 | Some(Range::new(0, 4, 0, 5)) 102 | ); 103 | 104 | let mut b = Buffer::from_text("{abc}"); 105 | assert_eq!( 106 | b.matching_bracket(Position::new(0, 0)), 107 | Some(Range::new(0, 4, 0, 5)) 108 | ); 109 | assert_eq!( 110 | b.matching_bracket(Position::new(0, 1)), 111 | Some(Range::new(0, 4, 0, 5)) 112 | ); 113 | assert_eq!( 114 | b.matching_bracket(Position::new(0, 4)), 115 | Some(Range::new(0, 0, 0, 1)) 116 | ); 117 | assert_eq!( 118 | b.matching_bracket(Position::new(0, 5)), 119 | Some(Range::new(0, 0, 0, 1)) 120 | ); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/buffer/extras/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod comment_out; 2 | pub mod duplicate_lines; 3 | pub mod edit_words; 4 | pub mod expand_selections; 5 | pub mod indent; 6 | pub mod matching_brackets; 7 | pub mod move_lines; 8 | pub mod select_lines; 9 | pub mod truncate; 10 | -------------------------------------------------------------------------------- /src/buffer/extras/move_lines.rs: -------------------------------------------------------------------------------- 1 | use crate::{buffer::Buffer, cursor::Range}; 2 | 3 | impl Buffer { 4 | pub fn move_lines_up(&mut self) { 5 | self.cursors.foreach(|c, past_cursors| { 6 | if c.front().y == 0 { 7 | return; 8 | } 9 | 10 | let s = c.selection(); 11 | 12 | c.select_overlapped_lines(); 13 | let mut text = self.buf.substr(c.selection()); 14 | let should_trim = !text.ends_with('\n'); 15 | if !text.ends_with('\n') { 16 | text.push('\n'); 17 | } 18 | 19 | let prev_line = self 20 | .buf 21 | .substr(Range::new(c.front().y - 1, 0, c.front().y, 0)); 22 | text.push_str(&prev_line); 23 | if should_trim && text.ends_with('\n') { 24 | text.pop(); 25 | } 26 | 27 | c.select(s.front().y - 1, 0, s.back().y + 1, 0); 28 | self.buf.edit_at_cursor(c, past_cursors, &text); 29 | c.select(s.start.y - 1, s.start.x, s.end.y - 1, s.end.x); 30 | }); 31 | } 32 | 33 | pub fn move_lines_down(&mut self) { 34 | self.cursors.foreach(|c, past_cursors| { 35 | if c.back().y >= self.buf.num_lines() - 1 { 36 | return; 37 | } 38 | 39 | let s = c.selection(); 40 | 41 | c.select_overlapped_lines(); 42 | let mut text = self.buf.substr(c.selection()); 43 | 44 | let mut next_line = self 45 | .buf 46 | .substr(Range::new(c.back().y, 0, c.back().y + 1, 0)); 47 | let should_trim = !next_line.ends_with('\n'); 48 | if !next_line.ends_with('\n') { 49 | next_line.push('\n'); 50 | } 51 | 52 | text.insert_str(0, &next_line); 53 | if should_trim && text.ends_with('\n') { 54 | text.pop(); 55 | } 56 | 57 | c.select(c.front().y, 0, c.back().y + 1, 0); 58 | self.buf.edit_at_cursor(c, past_cursors, &text); 59 | c.select(s.start.y + 1, s.start.x, s.end.y + 1, s.end.x); 60 | }); 61 | } 62 | 63 | pub fn move_to_end_of_line(&mut self) { 64 | self.cursors.foreach(|c, _past_cursors| { 65 | let y = c.moving_position().y; 66 | c.move_to(y, self.buf.line_len(y)); 67 | }); 68 | } 69 | 70 | pub fn move_to_beginning_of_line(&mut self) { 71 | self.cursors.foreach(|c, _past_cursors| { 72 | c.move_to(c.moving_position().y, 0); 73 | }); 74 | } 75 | 76 | pub fn move_to_top(&mut self) { 77 | self.cursors.save_undo_state(); 78 | self.select_main_cursor(0, 0, 0, 0); 79 | } 80 | } 81 | 82 | #[cfg(test)] 83 | mod tests { 84 | use crate::cursor::Cursor; 85 | 86 | use super::*; 87 | use pretty_assertions::assert_eq; 88 | 89 | #[test] 90 | fn move_a_line_up() { 91 | let mut b = Buffer::from_text(""); 92 | b.set_cursors_for_test(&[Cursor::new(0, 0)]); 93 | b.move_lines_up(); 94 | assert_eq!(b.text(), ""); 95 | assert_eq!(b.cursors(), &[Cursor::new(0, 0)]); 96 | 97 | // abcd 98 | let mut b = Buffer::from_text("abcd"); 99 | b.set_cursors_for_test(&[Cursor::new(0, 2)]); 100 | b.move_lines_up(); 101 | assert_eq!(b.text(), "abcd"); 102 | assert_eq!(b.cursors(), &[Cursor::new(0, 2)]); 103 | 104 | // 105 | // abcd 106 | let mut b = Buffer::from_text("\nabcd"); 107 | b.set_cursors_for_test(&[Cursor::new(1, 2)]); 108 | b.move_lines_up(); 109 | assert_eq!(b.text(), "abcd\n"); 110 | assert_eq!(b.cursors(), &[Cursor::new(0, 2)]); 111 | 112 | // abcd 113 | // xyz 114 | let mut b = Buffer::from_text("abcd\nxyz"); 115 | b.set_cursors_for_test(&[Cursor::new(1, 2)]); 116 | b.move_lines_up(); 117 | assert_eq!(b.text(), "xyz\nabcd"); 118 | assert_eq!(b.cursors(), &[Cursor::new(0, 2)]); 119 | 120 | // abcd 121 | // xyz 122 | // 123 | let mut b = Buffer::from_text("abcd\nxyz\n"); 124 | b.set_cursors_for_test(&[Cursor::new(1, 2)]); 125 | b.move_lines_up(); 126 | assert_eq!(b.text(), "xyz\nabcd\n"); 127 | assert_eq!(b.cursors(), &[Cursor::new(0, 2)]); 128 | } 129 | 130 | #[test] 131 | fn move_multiple_lines_up() { 132 | // abcd 133 | // efgh 134 | // xyz 135 | // 136 | let mut b = Buffer::from_text("abcd\nefgh\nxyz\n"); 137 | b.set_cursors_for_test(&[Cursor::new_selection(0, 2, 2, 1)]); 138 | b.move_lines_up(); 139 | assert_eq!(b.text(), "abcd\nefgh\nxyz\n"); 140 | assert_eq!(b.cursors(), &[Cursor::new_selection(0, 2, 2, 1)]); 141 | 142 | // 143 | // abcd 144 | // efgh 145 | // xyz 146 | // 147 | let mut b = Buffer::from_text("\nabcd\nefgh\nxyz\n"); 148 | b.set_cursors_for_test(&[Cursor::new_selection(1, 2, 3, 1)]); 149 | b.move_lines_up(); 150 | assert_eq!(b.text(), "abcd\nefgh\nxyz\n\n"); 151 | assert_eq!(b.cursors(), &[Cursor::new_selection(0, 2, 2, 1)]); 152 | 153 | // ---- 154 | // abcd 155 | // xyz 156 | let mut b = Buffer::from_text("----\nabcd\nxyz"); 157 | b.set_cursors_for_test(&[Cursor::new_selection(1, 2, 2, 1)]); 158 | b.move_lines_up(); 159 | assert_eq!(b.text(), "abcd\nxyz\n----"); 160 | assert_eq!(b.cursors(), &[Cursor::new_selection(0, 2, 1, 1)]); 161 | } 162 | 163 | #[test] 164 | fn move_a_line_down() { 165 | let mut b = Buffer::from_text(""); 166 | b.set_cursors_for_test(&[Cursor::new(0, 0)]); 167 | b.move_lines_down(); 168 | assert_eq!(b.text(), ""); 169 | assert_eq!(b.cursors(), &[Cursor::new(0, 0)]); 170 | 171 | // abcd 172 | let mut b = Buffer::from_text("abcd"); 173 | b.set_cursors_for_test(&[Cursor::new(0, 2)]); 174 | b.move_lines_down(); 175 | assert_eq!(b.text(), "abcd"); 176 | assert_eq!(b.cursors(), &[Cursor::new(0, 2)]); 177 | 178 | // abcd 179 | // 180 | let mut b = Buffer::from_text("abcd\n"); 181 | b.set_cursors_for_test(&[Cursor::new(0, 2)]); 182 | b.move_lines_down(); 183 | assert_eq!(b.text(), "\nabcd"); 184 | assert_eq!(b.cursors(), &[Cursor::new(1, 2)]); 185 | 186 | // abcd 187 | // xyz 188 | let mut b = Buffer::from_text("abcd\nxyz"); 189 | b.set_cursors_for_test(&[Cursor::new(0, 2)]); 190 | b.move_lines_down(); 191 | assert_eq!(b.text(), "xyz\nabcd"); 192 | assert_eq!(b.cursors(), &[Cursor::new(1, 2)]); 193 | 194 | // abcd 195 | // xyz 196 | // 197 | let mut b = Buffer::from_text("abcd\nxyz\n"); 198 | b.set_cursors_for_test(&[Cursor::new(1, 2)]); 199 | b.move_lines_up(); 200 | assert_eq!(b.text(), "xyz\nabcd\n"); 201 | assert_eq!(b.cursors(), &[Cursor::new(0, 2)]); 202 | } 203 | 204 | #[test] 205 | fn move_multiple_lines_down() { 206 | // 207 | // abcd 208 | // efgh 209 | // xyz 210 | let mut b = Buffer::from_text("\nabcd\nefgh\nxyz"); 211 | b.set_cursors_for_test(&[Cursor::new_selection(1, 2, 3, 1)]); 212 | b.move_lines_down(); 213 | assert_eq!(b.text(), "\nabcd\nefgh\nxyz"); 214 | assert_eq!(b.cursors(), &[Cursor::new_selection(1, 2, 3, 1)]); 215 | 216 | // 217 | // abcd 218 | // efgh 219 | // xyz 220 | // 221 | let mut b = Buffer::from_text("\nabcd\nefgh\nxyz\n"); 222 | b.set_cursors_for_test(&[Cursor::new_selection(1, 2, 3, 1)]); 223 | b.move_lines_down(); 224 | assert_eq!(b.text(), "\n\nabcd\nefgh\nxyz"); 225 | assert_eq!(b.cursors(), &[Cursor::new_selection(2, 2, 3, 1)]); 226 | 227 | // abcd 228 | // xyz 229 | // ---- 230 | let mut b = Buffer::from_text("abcd\nxyz\n----"); 231 | b.set_cursors_for_test(&[Cursor::new_selection(0, 2, 1, 1)]); 232 | b.move_lines_down(); 233 | assert_eq!(b.text(), "----\nabcd\nxyz"); 234 | assert_eq!(b.cursors(), &[Cursor::new_selection(1, 2, 2, 1)]); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/buffer/extras/select_lines.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | buffer::Buffer, 3 | cursor::{Position, Range}, 4 | }; 5 | 6 | impl Buffer { 7 | pub fn select_whole_line(&mut self, pos: Position) { 8 | let range = Range::new(pos.y, 0, pos.y + 1, 0); 9 | self.select_main_cursor_range(range); 10 | } 11 | 12 | pub fn select_whole_buffer(&mut self) { 13 | let end_y = self.num_lines() - 1; 14 | let range = Range::new(0, 0, end_y, self.line_len(end_y)); 15 | self.select_main_cursor_range(range); 16 | } 17 | } 18 | 19 | #[cfg(test)] 20 | mod tests { 21 | use crate::cursor::Cursor; 22 | 23 | use super::*; 24 | 25 | #[test] 26 | fn test_select_whole_buffer() { 27 | let mut buf = Buffer::from_text(""); 28 | buf.select_whole_buffer(); 29 | assert_eq!(buf.cursors(), &[Cursor::new(0, 0)]); 30 | 31 | let mut buf = Buffer::from_text("hello world"); 32 | buf.select_whole_buffer(); 33 | assert_eq!(buf.cursors(), &[Cursor::new_selection(0, 0, 11, 0)]); 34 | 35 | let mut buf = Buffer::from_text("hello\n"); 36 | buf.select_whole_buffer(); 37 | assert_eq!(buf.cursors(), &[Cursor::new_selection(0, 0, 1, 0)]); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/buffer/extras/truncate.rs: -------------------------------------------------------------------------------- 1 | use crate::buffer::Buffer; 2 | 3 | impl Buffer { 4 | pub fn truncate(&mut self) { 5 | self.cursors.foreach(|c, past_cursors| { 6 | if c.selection().is_empty() { 7 | // Select until the end of line. 8 | let pos = c.moving_position(); 9 | let eol = self.buf.line_len(pos.y); 10 | if pos.x == eol { 11 | // The cursor is already at the end of line, remove the 12 | // following newline instead. 13 | c.select(pos.y, pos.x, pos.y + 1, 0); 14 | } else { 15 | c.select(pos.y, pos.x, pos.y, eol); 16 | } 17 | } 18 | 19 | self.buf.edit_at_cursor(c, past_cursors, ""); 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/buffer/find.rs: -------------------------------------------------------------------------------- 1 | use crate::{char_iter::CharIter, cursor::Range}; 2 | 3 | pub struct FindIter<'a, 'b> { 4 | chars: CharIter<'a>, 5 | query: &'b str, 6 | } 7 | 8 | impl<'a, 'b> FindIter<'a, 'b> { 9 | pub fn new(chars: CharIter<'a>, query: &'b str) -> FindIter<'a, 'b> { 10 | FindIter { chars, query } 11 | } 12 | 13 | pub fn prev(&mut self) -> Option { 14 | if self.query.is_empty() { 15 | return None; 16 | } 17 | 18 | loop { 19 | let mut query_iter = self.query.chars().rev(); 20 | let mut buf_iter = self.chars.clone(); 21 | 22 | let first_pos = self.chars.last_position(); 23 | self.chars.prev(); 24 | 25 | let mut n = 0; 26 | loop { 27 | let last_pos = buf_iter.next_position(); 28 | match (buf_iter.prev(), query_iter.next()) { 29 | (Some(a), Some(b)) if a != b => { 30 | break; 31 | } 32 | (None, Some(_)) => { 33 | // Reached to EOF. 34 | return None; 35 | } 36 | (_, None) => { 37 | for _ in 0..n - 1 { 38 | self.chars.prev(); 39 | } 40 | 41 | return Some(Range::from_positions(last_pos, first_pos)); 42 | } 43 | (Some(_), Some(_)) => { 44 | // Continue comparing the next characters... 45 | n += 1; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | impl<'a, 'b> Iterator for FindIter<'a, 'b> { 54 | type Item = Range; 55 | 56 | fn next(&mut self) -> Option { 57 | if self.query.is_empty() { 58 | return None; 59 | } 60 | 61 | loop { 62 | let mut query_iter = self.query.chars(); 63 | let mut buf_iter = self.chars.clone(); 64 | let first_pos = buf_iter.next_position(); 65 | 66 | self.chars.next(); 67 | 68 | let mut n = 0; 69 | loop { 70 | match (buf_iter.next(), query_iter.next()) { 71 | (Some(a), Some(b)) if a != b => { 72 | break; 73 | } 74 | (None, Some(_)) => { 75 | // Reached to EOF. 76 | return None; 77 | } 78 | (_, None) => { 79 | for _ in 0..n - 1 { 80 | self.chars.next(); 81 | } 82 | 83 | let last_pos = buf_iter.last_position(); 84 | return Some(Range::from_positions(first_pos, last_pos)); 85 | } 86 | (Some(_), Some(_)) => { 87 | // Continue comparing the next characters... 88 | n += 1; 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | 96 | #[cfg(test)] 97 | mod tests { 98 | use crate::{buffer::Buffer, cursor::Position}; 99 | 100 | use super::*; 101 | use pretty_assertions::assert_eq; 102 | 103 | #[test] 104 | fn test_find_next() { 105 | let b = Buffer::from_text(""); 106 | let mut iter = b.find_iter("A", Position::new(0, 0)); 107 | assert_eq!(iter.next(), None); 108 | 109 | let b = Buffer::from_text("AAAA"); 110 | let mut iter = b.find_iter("", Position::new(0, 0)); 111 | assert_eq!(iter.next(), None); 112 | let mut iter = b.find_iter("B", Position::new(0, 0)); 113 | assert_eq!(iter.next(), None); 114 | let mut iter = b.find_iter("A", Position::new(0, 0)); 115 | assert_eq!(iter.next(), Some(Range::new(0, 0, 0, 1))); 116 | assert_eq!(iter.next(), Some(Range::new(0, 1, 0, 2))); 117 | assert_eq!(iter.next(), Some(Range::new(0, 2, 0, 3))); 118 | assert_eq!(iter.next(), Some(Range::new(0, 3, 0, 4))); 119 | assert_eq!(iter.next(), None); 120 | let mut iter = b.find_iter("A", Position::new(0, 2)); 121 | assert_eq!(iter.next(), Some(Range::new(0, 2, 0, 3))); 122 | assert_eq!(iter.next(), Some(Range::new(0, 3, 0, 4))); 123 | assert_eq!(iter.next(), None); 124 | let mut iter = b.find_iter("AA", Position::new(0, 0)); 125 | assert_eq!(iter.next(), Some(Range::new(0, 0, 0, 2))); 126 | assert_eq!(iter.next(), Some(Range::new(0, 2, 0, 4))); 127 | assert_eq!(iter.next(), None); 128 | 129 | let b = Buffer::from_text("AxAxA"); 130 | let mut iter = b.find_iter("A", Position::new(0, 0)); 131 | assert_eq!(iter.next(), Some(Range::new(0, 0, 0, 1))); 132 | assert_eq!(iter.next(), Some(Range::new(0, 2, 0, 3))); 133 | assert_eq!(iter.next(), Some(Range::new(0, 4, 0, 5))); 134 | assert_eq!(iter.next(), None); 135 | } 136 | 137 | #[test] 138 | fn test_find_prev() { 139 | let b = Buffer::from_text(""); 140 | let mut iter = b.find_iter("A", Position::new(0, 0)); 141 | assert_eq!(iter.prev(), None); 142 | 143 | let b = Buffer::from_text("AAAA"); 144 | let mut iter = b.find_iter("", Position::new(0, 4)); 145 | assert_eq!(iter.prev(), None); 146 | let mut iter = b.find_iter("B", Position::new(0, 4)); 147 | assert_eq!(iter.prev(), None); 148 | let mut iter = b.find_iter("A", Position::new(0, 4)); 149 | assert_eq!(iter.prev(), Some(Range::new(0, 3, 0, 4))); 150 | assert_eq!(iter.prev(), Some(Range::new(0, 2, 0, 3))); 151 | assert_eq!(iter.prev(), Some(Range::new(0, 1, 0, 2))); 152 | assert_eq!(iter.prev(), Some(Range::new(0, 0, 0, 1))); 153 | assert_eq!(iter.prev(), None); 154 | let mut iter = b.find_iter("A", Position::new(0, 2)); 155 | assert_eq!(iter.prev(), Some(Range::new(0, 1, 0, 2))); 156 | assert_eq!(iter.prev(), Some(Range::new(0, 0, 0, 1))); 157 | assert_eq!(iter.prev(), None); 158 | let mut iter = b.find_iter("AA", Position::new(0, 4)); 159 | assert_eq!(iter.prev(), Some(Range::new(0, 2, 0, 4))); 160 | assert_eq!(iter.prev(), Some(Range::new(0, 0, 0, 2))); 161 | assert_eq!(iter.prev(), None); 162 | 163 | let b = Buffer::from_text("AxAxA"); 164 | let mut iter = b.find_iter("A", Position::new(0, 5)); 165 | assert_eq!(iter.prev(), Some(Range::new(0, 4, 0, 5))); 166 | assert_eq!(iter.prev(), Some(Range::new(0, 2, 0, 3))); 167 | assert_eq!(iter.prev(), Some(Range::new(0, 0, 0, 1))); 168 | assert_eq!(iter.prev(), None); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/buffer/lib.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | extern crate test; 3 | 4 | #[macro_use] 5 | extern crate log; 6 | 7 | pub mod buffer; 8 | pub mod char_iter; 9 | pub mod cursor; 10 | pub mod display_width; 11 | pub mod extras; 12 | pub mod find; 13 | pub mod grapheme_iter; 14 | pub mod mut_raw_buffer; 15 | pub mod paragraph_iter; 16 | pub mod raw_buffer; 17 | pub mod reflow_iter; 18 | pub mod scroll; 19 | pub mod syntax; 20 | pub mod word_iter; 21 | -------------------------------------------------------------------------------- /src/buffer/mut_raw_buffer.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Deref; 2 | 3 | use crate::{ 4 | cursor::{Cursor, Position, Range}, 5 | raw_buffer::RawBuffer, 6 | }; 7 | 8 | #[derive(Clone, PartialEq, Eq, Debug)] 9 | pub struct Change { 10 | pub range: Range, 11 | pub byte_range: std::ops::Range, 12 | pub new_pos: Position, 13 | pub insert_text: String, 14 | } 15 | 16 | /// An internal mutable buffer implementation supporting primitive operations 17 | /// required by the editor. 18 | pub struct MutRawBuffer { 19 | raw: RawBuffer, 20 | changes: Vec, 21 | } 22 | 23 | impl MutRawBuffer { 24 | pub fn new() -> MutRawBuffer { 25 | MutRawBuffer { 26 | raw: RawBuffer::new(), 27 | changes: Vec::new(), 28 | } 29 | } 30 | 31 | pub fn from_raw_buffer(raw_buffer: RawBuffer) -> MutRawBuffer { 32 | MutRawBuffer { 33 | raw: raw_buffer, 34 | changes: Vec::new(), 35 | } 36 | } 37 | 38 | pub fn from_text(text: &str) -> MutRawBuffer { 39 | MutRawBuffer { 40 | raw: RawBuffer::from_text(text), 41 | changes: Vec::new(), 42 | } 43 | } 44 | 45 | pub fn from_reader(reader: T) -> std::io::Result { 46 | Ok(MutRawBuffer { 47 | raw: RawBuffer::from_reader(reader)?, 48 | changes: Vec::new(), 49 | }) 50 | } 51 | 52 | pub fn raw_buffer(&self) -> &RawBuffer { 53 | &self.raw 54 | } 55 | 56 | pub fn clear_changes(&mut self) -> Vec { 57 | let changes = self.changes.drain(..).collect(); 58 | self.changes = Vec::new(); 59 | changes 60 | } 61 | 62 | /// Replaces the text at the `range` with `new_text`. 63 | /// 64 | /// This is the only method that modifies the buffer. 65 | /// 66 | /// # Complexity 67 | /// 68 | /// According to the ropey's documentation: 69 | // 70 | /// Runs in O(M + log N) time, where N is the length of the Rope and M 71 | /// is the length of the range being removed/inserted. 72 | fn edit_without_recording(&mut self, range: Range, new_text: &str) { 73 | let start = self.pos_to_char_index(range.front()); 74 | let end = self.pos_to_char_index(range.back()); 75 | 76 | let mut rope = self.raw.rope().clone(); 77 | if !(start..end).is_empty() { 78 | rope.remove(start..end); 79 | } 80 | 81 | if !new_text.is_empty() { 82 | rope.insert(start, new_text); 83 | } 84 | 85 | self.raw = RawBuffer::from(rope); 86 | } 87 | 88 | pub fn edit(&mut self, range: Range, new_text: &str) -> &Change { 89 | let new_pos = Position::position_after_edit(range, new_text); 90 | self.changes.push(Change { 91 | range, 92 | insert_text: new_text.to_owned(), 93 | new_pos, 94 | byte_range: self.raw.pos_to_byte_index(range.front()) 95 | ..self.raw.pos_to_byte_index(range.back()), 96 | }); 97 | 98 | self.edit_without_recording(range, new_text); 99 | self.changes.last().unwrap() 100 | } 101 | 102 | pub fn edit_at_cursor( 103 | &mut self, 104 | current_cursor: &mut Cursor, 105 | past_cursors: &mut [Cursor], 106 | new_text: &str, 107 | ) { 108 | let range_removed = current_cursor.selection(); 109 | let prev_back_y = current_cursor.selection().back().y; 110 | 111 | let change = self.edit(range_removed, new_text); 112 | 113 | // Move the current cursor. 114 | let new_pos = change.new_pos; 115 | current_cursor.move_to(new_pos.y, new_pos.x); 116 | 117 | // Adjust past cursors. 118 | let y_diff = (new_pos.y as isize) - (prev_back_y as isize); 119 | for c in past_cursors { 120 | let s = c.selection_mut(); 121 | 122 | if s.start.y == range_removed.back().y { 123 | s.start.x = new_pos.x + (s.start.x - range_removed.back().x); 124 | } 125 | if s.end.y == range_removed.back().y { 126 | s.end.x = new_pos.x + (s.end.x - range_removed.back().x); 127 | } 128 | 129 | s.start.y = ((s.start.y as isize) + y_diff) as usize; 130 | s.end.y = ((s.end.y as isize) + y_diff) as usize; 131 | } 132 | } 133 | } 134 | 135 | impl Default for MutRawBuffer { 136 | fn default() -> MutRawBuffer { 137 | MutRawBuffer::new() 138 | } 139 | } 140 | 141 | impl PartialEq for MutRawBuffer { 142 | fn eq(&self, other: &Self) -> bool { 143 | self.raw == other.raw 144 | } 145 | } 146 | 147 | impl Deref for MutRawBuffer { 148 | type Target = RawBuffer; 149 | 150 | fn deref(&self) -> &RawBuffer { 151 | &self.raw 152 | } 153 | } 154 | 155 | #[cfg(test)] 156 | mod tests { 157 | use super::*; 158 | 159 | #[test] 160 | fn test_insertion() { 161 | let mut buffer = MutRawBuffer::new(); 162 | buffer.edit(Range::new(0, 0, 0, 0), "ABG"); 163 | assert_eq!(buffer.text(), "ABG"); 164 | 165 | buffer.edit(Range::new(0, 2, 0, 2), "CDEF"); 166 | assert_eq!(buffer.text(), "ABCDEFG"); 167 | } 168 | 169 | #[test] 170 | fn test_deletion() { 171 | let mut buffer = MutRawBuffer::from_text("ABCDEFG"); 172 | buffer.edit(Range::new(0, 1, 0, 1), ""); 173 | assert_eq!(buffer.text(), "ABCDEFG"); 174 | 175 | buffer.edit(Range::new(0, 1, 0, 3), ""); 176 | assert_eq!(buffer.text(), "ADEFG"); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/buffer/paragraph_iter.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cursor::{Position, Range}, 3 | raw_buffer::RawBuffer, 4 | reflow_iter::ReflowIter, 5 | }; 6 | 7 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 8 | pub struct ParagraphIndex { 9 | pub buffer_y: usize, 10 | } 11 | 12 | impl ParagraphIndex { 13 | pub fn new(_buffer: &RawBuffer, pos: Position) -> Self { 14 | ParagraphIndex { buffer_y: pos.y } 15 | } 16 | 17 | pub fn zeroed() -> ParagraphIndex { 18 | ParagraphIndex { buffer_y: 0 } 19 | } 20 | } 21 | 22 | pub struct Paragraph<'a> { 23 | pub index: ParagraphIndex, 24 | pub reflow_iter: ReflowIter<'a>, 25 | } 26 | 27 | pub struct ParagraphIter<'a> { 28 | pos: Position, 29 | buffer: &'a RawBuffer, 30 | screen_width: usize, 31 | tab_width: usize, 32 | } 33 | 34 | impl<'a> ParagraphIter<'a> { 35 | /// Returns `ParagraphIter` from the paragraph containing the given position. 36 | pub fn new( 37 | buffer: &'a RawBuffer, 38 | pos: Position, 39 | screen_width: usize, 40 | tab_width: usize, 41 | ) -> ParagraphIter<'a> { 42 | let index = ParagraphIndex::new(buffer, pos); 43 | Self::new_at_index(buffer, index, screen_width, tab_width) 44 | } 45 | 46 | pub fn new_at_index( 47 | buffer: &'a RawBuffer, 48 | index: ParagraphIndex, 49 | screen_width: usize, 50 | tab_width: usize, 51 | ) -> ParagraphIter<'a> { 52 | let pos = Position::new(index.buffer_y, 0); 53 | ParagraphIter { 54 | pos, 55 | buffer, 56 | screen_width, 57 | tab_width, 58 | } 59 | } 60 | 61 | pub fn prev(&mut self) -> Option> { 62 | if self.pos.y == 0 { 63 | return None; 64 | } 65 | 66 | // TODO: Support for too long lines: split a line into multiple paragraphs. 67 | let pos_start = Position::new(self.pos.y - 1, 0); 68 | let pos_end = Position::new(self.pos.y, 0); 69 | self.pos = Position::new(self.pos.y - 1, 0); 70 | 71 | let reflow_iter = ReflowIter::new( 72 | self.buffer, 73 | Range::from_positions(pos_start, pos_end), 74 | self.screen_width, 75 | self.tab_width, 76 | ); 77 | Some(Paragraph { 78 | index: ParagraphIndex { 79 | buffer_y: pos_start.y, 80 | }, 81 | reflow_iter, 82 | }) 83 | } 84 | } 85 | 86 | impl<'a> Iterator for ParagraphIter<'a> { 87 | type Item = Paragraph<'a>; 88 | 89 | fn next(&mut self) -> Option { 90 | let pos = self.pos; 91 | // if pos.y > self.buffer.num_lines() { 92 | if !self.buffer.is_valid_position(pos) { 93 | return None; 94 | } 95 | 96 | // TODO: Support for too long lines: split a line into multiple paragraphs. 97 | let pos_start = Position::new(pos.y, 0); 98 | let pos_end = Position::new(pos.y + 1, 0); 99 | self.pos = Position::new(pos_start.y + 1, 0); 100 | 101 | // FIXME: GraphemeIter::new() is slow 102 | let reflow_iter = ReflowIter::new( 103 | self.buffer, 104 | Range::from_positions(pos_start, pos_end), 105 | self.screen_width, 106 | self.tab_width, 107 | ); 108 | Some(Paragraph { 109 | index: ParagraphIndex { 110 | buffer_y: pos_start.y, 111 | }, 112 | reflow_iter, 113 | }) 114 | } 115 | } 116 | 117 | #[cfg(test)] 118 | mod tests { 119 | 120 | #[test] 121 | fn paragraph_iter() {} 122 | } 123 | -------------------------------------------------------------------------------- /src/buffer/raw_buffer.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp::min, fmt}; 2 | 3 | use crate::{ 4 | char_iter::CharIter, 5 | cursor::{Position, Range}, 6 | find::FindIter, 7 | grapheme_iter::{BidirectionalGraphemeIter, GraphemeIter}, 8 | paragraph_iter::{ParagraphIndex, ParagraphIter}, 9 | reflow_iter::ReflowIter, 10 | word_iter::{is_word_char, WordIter}, 11 | }; 12 | 13 | /// An internal immutable buffer, being used as a snapshot. 14 | /// 15 | /// This object offers a cheap `clone`-ing thanks to the underlying data sturcture 16 | /// called *rope*. It makes significantly easy to implement undo/redo operations. 17 | #[derive(Clone)] 18 | pub struct RawBuffer { 19 | /// The inner buffer data structure. 20 | rope: ropey::Rope, 21 | } 22 | 23 | impl RawBuffer { 24 | pub fn new() -> RawBuffer { 25 | RawBuffer { 26 | rope: ropey::Rope::new(), 27 | } 28 | } 29 | 30 | pub fn from_text(text: &str) -> RawBuffer { 31 | RawBuffer { 32 | rope: ropey::Rope::from_str(text), 33 | } 34 | } 35 | 36 | pub fn from_reader(reader: T) -> std::io::Result { 37 | Ok(RawBuffer { 38 | rope: ropey::Rope::from_reader(reader)?, 39 | }) 40 | } 41 | 42 | pub fn rope(&self) -> &ropey::Rope { 43 | &self.rope 44 | } 45 | 46 | pub fn write_to(&self, writer: impl std::io::Write) -> std::io::Result<()> { 47 | self.rope.write_to(writer) 48 | } 49 | 50 | pub fn is_empty(&self) -> bool { 51 | self.rope.len_bytes() == 0 52 | } 53 | 54 | /// Returns the number of lines in the buffer. 55 | /// 56 | /// # Complexity 57 | /// 58 | /// Runs in O(1) time. 59 | pub fn num_lines(&self) -> usize { 60 | self.rope.len_lines() 61 | } 62 | 63 | /// Returns the number of characters in the buffer. 64 | /// 65 | /// # Complexity 66 | /// 67 | /// Runs in O(1) time. 68 | pub fn len_chars(&self) -> usize { 69 | self.rope.len_chars() 70 | } 71 | 72 | /// Returns the number of characters in a line except new line characters. 73 | /// 74 | /// # Complexity 75 | /// 76 | /// Runs in O(log N) time, where N is the length of the buffer. 77 | pub fn line_len(&self, y: usize) -> usize { 78 | if y == self.num_lines() { 79 | 0 80 | } else { 81 | let line = self.rope.line(y); 82 | 83 | // The `line` contains newline characters so we need to subtract them. 84 | let num_newline_chars = line 85 | .chunks() 86 | .last() 87 | .map(|chunk| chunk.matches(|c| c == '\n' || c == '\r').count()) 88 | .unwrap_or(0); 89 | 90 | line.len_chars() - num_newline_chars 91 | } 92 | } 93 | 94 | /// Returns the number of indentation characters in a line. 95 | /// 96 | /// # Complexity 97 | /// 98 | /// Runs in O(M + log N) time, where N is the length of the rope and M is 99 | /// the length of the line. 100 | pub fn line_indent_len(&self, y: usize) -> usize { 101 | self.char_iter(Position::new(y, 0)) 102 | .take_while(|c| *c == ' ' || *c == '\t') 103 | .count() 104 | } 105 | 106 | pub fn clamp_position(&self, pos: Position) -> Position { 107 | Position::new( 108 | min(pos.y, self.num_lines().saturating_sub(1)), 109 | min(pos.x, self.line_len(pos.y)), 110 | ) 111 | } 112 | 113 | pub fn clamp_range(&self, range: Range) -> Range { 114 | let mut r = range; 115 | r.start.y = min(r.start.y, self.num_lines().saturating_sub(1)); 116 | r.end.y = min(r.end.y, self.num_lines().saturating_sub(1)); 117 | r.start.x = min(r.start.x, self.line_len(r.start.y)); 118 | r.end.x = min(r.end.x, self.line_len(r.end.y)); 119 | r 120 | } 121 | 122 | pub fn is_valid_position(&self, pos: Position) -> bool { 123 | self.clamp_position(pos) == pos 124 | } 125 | 126 | pub fn is_valid_range(&self, range: Range) -> bool { 127 | self.is_valid_position(range.start) && self.is_valid_position(range.end) 128 | } 129 | 130 | /// Turns the whole buffer into a string. 131 | /// 132 | /// # Complexity 133 | /// 134 | /// Runs in O(N) time, where N is the length of the buffer. 135 | pub fn text(&self) -> String { 136 | self.rope.to_string() 137 | } 138 | 139 | /// Returns a substring. 140 | /// 141 | /// # Complexity 142 | /// 143 | /// Runs in O(N) time, where N is the length of the buffer. 144 | pub fn substr(&self, range: Range) -> String { 145 | let start = self.pos_to_char_index(range.front()); 146 | let end = self.pos_to_char_index(range.back()); 147 | self.rope.slice(start..end).to_string() 148 | } 149 | 150 | /// Returns the text in a line excluding newline character(s). 151 | /// 152 | /// # Complexity 153 | /// 154 | /// Runs in O(N) time, where N is the length of the line. 155 | pub fn line_text(&self, y: usize) -> String { 156 | self.substr(Range::new(y, 0, y, self.line_len(y))) 157 | } 158 | 159 | /// Returns an iterator at the given position which allows traversing 160 | /// characters (not graphemes) in the buffer back and forth. 161 | pub fn char_iter(&self, pos: Position) -> CharIter<'_> { 162 | CharIter::new(self.rope.chars_at(self.pos_to_char_index(pos)), self, pos) 163 | } 164 | 165 | /// Returns an iterator at the given position which allows traversing 166 | /// graphemes in the buffer. 167 | pub fn grapheme_iter(&self, pos: Position) -> GraphemeIter<'_> { 168 | GraphemeIter::new(self, pos) 169 | } 170 | 171 | /// Returns an iterator at the given position which allows traversing 172 | /// graphemes in the buffer back and forth. 173 | /// 174 | /// Prefer using this method over `grapheme_iter` if you don't 175 | /// need to move a iterator backwards. 176 | pub fn bidirectional_grapheme_iter(&self, pos: Position) -> BidirectionalGraphemeIter<'_> { 177 | BidirectionalGraphemeIter::new(self, pos) 178 | } 179 | 180 | pub fn reflow_iter( 181 | &self, 182 | range: Range, 183 | screen_width: usize, 184 | tab_width: usize, 185 | ) -> ReflowIter<'_> { 186 | ReflowIter::new(self, range, screen_width, tab_width) 187 | } 188 | 189 | pub fn paragraph_iter( 190 | &self, 191 | pos: Position, 192 | screen_width: usize, 193 | tab_width: usize, 194 | ) -> ParagraphIter<'_> { 195 | ParagraphIter::new(self, pos, screen_width, tab_width) 196 | } 197 | 198 | pub fn paragraph_iter_at_index( 199 | &self, 200 | index: ParagraphIndex, 201 | screen_width: usize, 202 | tab_width: usize, 203 | ) -> ParagraphIter<'_> { 204 | ParagraphIter::new_at_index(self, index, screen_width, tab_width) 205 | } 206 | 207 | /// Returns the current word range. 208 | pub fn current_word(&self, pos: Position) -> Option { 209 | let mut start_iter = self.char_iter(pos); 210 | let mut end_iter = self.char_iter(pos); 211 | 212 | let mut start_pos; 213 | loop { 214 | start_pos = start_iter.last_position(); 215 | match start_iter.prev() { 216 | Some(ch) if !is_word_char(ch) => break, 217 | Some(_) => continue, 218 | None => break, 219 | } 220 | } 221 | 222 | for ch in end_iter.by_ref() { 223 | if !is_word_char(ch) { 224 | break; 225 | } 226 | } 227 | 228 | if start_pos == end_iter.last_position() { 229 | return None; 230 | } 231 | 232 | Some(Range::from_positions(start_pos, end_iter.last_position())) 233 | } 234 | 235 | /// Returns an iterator at the given position which allows traversing 236 | /// words in the buffer back and forth. 237 | pub fn word_iter_from_beginning_of_word(&self, pos: Position) -> WordIter<'_> { 238 | WordIter::new_from_beginning_of_word(self.char_iter(pos)) 239 | } 240 | 241 | /// Returns an iterator at the given position which allows traversing 242 | /// words in the buffer back and forth. 243 | pub fn word_iter_from_end_of_word(&self, pos: Position) -> WordIter<'_> { 244 | WordIter::new_from_end_of_word(self.char_iter(pos)) 245 | } 246 | 247 | /// Returns an iterator which returns occurrences of the given string. 248 | pub fn find_iter<'a, 'b>(&'a self, query: &'b str, pos: Position) -> FindIter<'a, 'b> { 249 | FindIter::new(self.char_iter(pos), query) 250 | } 251 | 252 | pub(crate) fn rope_slice(&self, range: Range) -> ropey::RopeSlice<'_> { 253 | let start = self.pos_to_char_index(range.front()); 254 | let end = self.pos_to_char_index(range.back()); 255 | self.rope.slice(start..end) 256 | } 257 | 258 | /// Returns the character index in the rope. 259 | /// 260 | /// # Complexity 261 | /// 262 | /// Runs in O(log N) time, where N is the length of the rope. 263 | pub fn pos_to_char_index(&self, pos: Position) -> usize { 264 | if pos.y == self.num_lines() && pos.x == 0 { 265 | // EOF. 266 | return self.rope.line_to_char(pos.y) + self.line_len(pos.y); 267 | } 268 | 269 | let column = if pos.x == std::usize::MAX { 270 | self.line_len(pos.y) 271 | } else { 272 | pos.x 273 | }; 274 | 275 | self.rope.line_to_char(pos.y) + column 276 | } 277 | 278 | pub fn pos_to_byte_index(&self, pos: Position) -> usize { 279 | self.rope.char_to_byte(self.pos_to_char_index(pos)) 280 | } 281 | 282 | pub fn char_index_to_pos(&self, char_index: usize) -> Position { 283 | let y = self.rope.char_to_line(char_index); 284 | let x = char_index - self.rope.line_to_char(y); 285 | Position { y, x } 286 | } 287 | } 288 | 289 | impl Default for RawBuffer { 290 | fn default() -> RawBuffer { 291 | RawBuffer::new() 292 | } 293 | } 294 | 295 | impl PartialEq for RawBuffer { 296 | fn eq(&self, other: &Self) -> bool { 297 | self.rope == other.rope 298 | } 299 | } 300 | 301 | impl fmt::Debug for RawBuffer { 302 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 303 | write!(f, "RawBuffer {{ num_lines: {} }}", self.num_lines()) 304 | } 305 | } 306 | 307 | impl From for RawBuffer { 308 | fn from(rope: ropey::Rope) -> RawBuffer { 309 | RawBuffer { rope } 310 | } 311 | } 312 | 313 | #[cfg(test)] 314 | mod tests { 315 | use super::*; 316 | 317 | #[test] 318 | fn test_substr() { 319 | let buffer = RawBuffer::from_text("...AB..."); 320 | assert_eq!(buffer.substr(Range::new(0, 3, 0, 5)), "AB"); 321 | 322 | let buffer = RawBuffer::from_text("あいうABえお"); 323 | assert_eq!(buffer.substr(Range::new(0, 3, 0, 5)), "AB"); 324 | } 325 | 326 | #[test] 327 | fn test_current_word() { 328 | let buffer = RawBuffer::from_text(""); 329 | assert_eq!(buffer.current_word(Position::new(0, 0)), None); 330 | 331 | let buffer = RawBuffer::from_text("ABC "); 332 | assert_eq!( 333 | buffer.current_word(Position::new(0, 0)), 334 | Some(Range::new(0, 0, 0, 3)) 335 | ); 336 | assert_eq!( 337 | buffer.current_word(Position::new(0, 1)), 338 | Some(Range::new(0, 0, 0, 3)) 339 | ); 340 | assert_eq!( 341 | buffer.current_word(Position::new(0, 3)), 342 | Some(Range::new(0, 0, 0, 3)) 343 | ); 344 | assert_eq!(buffer.current_word(Position::new(0, 4)), None); 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/buffer/reflow_iter.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cursor::{Position, Range}, 3 | display_width::DisplayWidth, 4 | grapheme_iter::GraphemeIter, 5 | raw_buffer::RawBuffer, 6 | }; 7 | 8 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 9 | pub struct ScreenPosition { 10 | pub y: usize, 11 | pub x: usize, 12 | } 13 | 14 | impl ScreenPosition { 15 | pub fn new(y: usize, x: usize) -> ScreenPosition { 16 | ScreenPosition { y, x } 17 | } 18 | } 19 | 20 | #[derive(Debug, PartialEq, Eq)] 21 | pub enum PrintableGrapheme<'a> { 22 | Eof, 23 | Grapheme(&'a str), 24 | Whitespaces, 25 | ZeroWidth, 26 | /// Contains newline character(s). 27 | Newline(Option<&'a str>), 28 | } 29 | 30 | #[derive(Debug, PartialEq, Eq)] 31 | pub struct ReflowItem<'a> { 32 | pub grapheme: PrintableGrapheme<'a>, 33 | pub grapheme_width: usize, 34 | pub pos_in_buffer: Position, 35 | pub pos_in_screen: ScreenPosition, 36 | } 37 | 38 | #[derive(Clone)] 39 | pub struct ReflowIter<'a> { 40 | buffer: &'a RawBuffer, 41 | iter: GraphemeIter<'a>, 42 | /// The number of columns in the screen. 43 | screen_width: usize, 44 | screen_pos: ScreenPosition, 45 | tab_width: usize, 46 | range: Range, 47 | return_eof: bool, 48 | } 49 | 50 | impl<'a> ReflowIter<'a> { 51 | pub fn new( 52 | buffer: &'a RawBuffer, 53 | range: Range, 54 | screen_width: usize, 55 | tab_width: usize, 56 | ) -> ReflowIter<'a> { 57 | ReflowIter { 58 | buffer, 59 | iter: buffer.grapheme_iter(range.front()), 60 | screen_width, 61 | screen_pos: ScreenPosition { y: 0, x: 0 }, 62 | tab_width, 63 | range, 64 | return_eof: false, 65 | } 66 | } 67 | 68 | pub fn range(&self) -> Range { 69 | self.range 70 | } 71 | 72 | pub fn enable_eof(&mut self, enable: bool) { 73 | self.return_eof = enable; 74 | } 75 | } 76 | 77 | impl<'a> Iterator for ReflowIter<'a> { 78 | type Item = ReflowItem<'a>; 79 | 80 | fn next(&mut self) -> Option { 81 | let (pos_in_buffer, grapheme) = match self.iter.next() { 82 | Some((pos_in_buffer, grapheme)) => (pos_in_buffer, grapheme), 83 | None if self.return_eof => { 84 | self.return_eof = false; 85 | return Some(ReflowItem { 86 | grapheme: PrintableGrapheme::Eof, 87 | grapheme_width: 0, 88 | // What if it's not eof? 89 | pos_in_buffer: Position::new( 90 | self.buffer.num_lines() - 1, 91 | self.buffer.line_len(self.buffer.num_lines() - 1), 92 | ), 93 | pos_in_screen: self.screen_pos, 94 | }); 95 | } 96 | None => { 97 | return None; 98 | } 99 | }; 100 | 101 | if pos_in_buffer >= self.range.back() { 102 | return None; 103 | } 104 | 105 | let (printable, grapheme_width) = match grapheme { 106 | "\n" => (PrintableGrapheme::Newline(Some(grapheme)), 1), 107 | "\t" => { 108 | let n = width_to_next_tab_stop(self.screen_pos.x, self.tab_width); 109 | (PrintableGrapheme::Whitespaces, n) 110 | } 111 | _ => { 112 | let w = grapheme.display_width(); 113 | if w == 0 { 114 | // We treat a zero-width character as a single character otherwise it'll be 115 | // very confusing. 116 | (PrintableGrapheme::ZeroWidth, 1) 117 | } else { 118 | (PrintableGrapheme::Grapheme(grapheme), w) 119 | } 120 | } 121 | }; 122 | 123 | if self.screen_pos.x + grapheme_width > self.screen_width { 124 | self.screen_pos.y += 1; 125 | self.screen_pos.x = 0; 126 | } 127 | 128 | let pos_in_screen = self.screen_pos; 129 | 130 | if matches!(printable, PrintableGrapheme::Newline(_)) { 131 | self.screen_pos.y += 1; 132 | self.screen_pos.x = 0; 133 | } else { 134 | self.screen_pos.x += grapheme_width; 135 | } 136 | 137 | Some(ReflowItem { 138 | grapheme: printable, 139 | grapheme_width, 140 | pos_in_buffer, 141 | pos_in_screen, 142 | }) 143 | } 144 | } 145 | 146 | fn width_to_next_tab_stop(x: usize, tab_width: usize) -> usize { 147 | let level = x / tab_width + 1; 148 | tab_width * level - x 149 | } 150 | 151 | #[cfg(test)] 152 | mod tests { 153 | use super::*; 154 | use pretty_assertions::assert_eq; 155 | 156 | #[test] 157 | fn reflow_iter() { 158 | // abc 159 | // d 160 | let buf = RawBuffer::from_text("abc\nd"); 161 | let mut iter = ReflowIter::new(&buf, Range::new(0, 0, usize::MAX, 0), 4, 4); 162 | assert_eq!( 163 | iter.next(), 164 | Some(ReflowItem { 165 | grapheme: PrintableGrapheme::Grapheme("a"), 166 | grapheme_width: 1, 167 | pos_in_buffer: Position::new(0, 0), 168 | pos_in_screen: ScreenPosition { y: 0, x: 0 }, 169 | }) 170 | ); 171 | assert_eq!( 172 | iter.next(), 173 | Some(ReflowItem { 174 | grapheme: PrintableGrapheme::Grapheme("b"), 175 | grapheme_width: 1, 176 | pos_in_buffer: Position::new(0, 1), 177 | pos_in_screen: ScreenPosition { y: 0, x: 1 }, 178 | }) 179 | ); 180 | assert_eq!( 181 | iter.next(), 182 | Some(ReflowItem { 183 | grapheme: PrintableGrapheme::Grapheme("c"), 184 | grapheme_width: 1, 185 | pos_in_buffer: Position::new(0, 2), 186 | pos_in_screen: ScreenPosition { y: 0, x: 2 }, 187 | }) 188 | ); 189 | assert_eq!( 190 | iter.next(), 191 | Some(ReflowItem { 192 | grapheme: PrintableGrapheme::Newline(Some("\n")), 193 | grapheme_width: 1, 194 | pos_in_buffer: Position::new(0, 3), 195 | pos_in_screen: ScreenPosition { y: 0, x: 3 }, 196 | }) 197 | ); 198 | assert_eq!( 199 | iter.next(), 200 | Some(ReflowItem { 201 | grapheme: PrintableGrapheme::Grapheme("d"), 202 | grapheme_width: 1, 203 | pos_in_buffer: Position::new(1, 0), 204 | pos_in_screen: ScreenPosition { y: 1, x: 0 }, 205 | }) 206 | ); 207 | } 208 | 209 | #[test] 210 | fn reflow_iter_wrapped() { 211 | // ab 212 | // c 213 | let buf = RawBuffer::from_text("abc"); 214 | let mut iter = ReflowIter::new(&buf, Range::new(0, 0, usize::MAX, 0), 2, 4); 215 | assert_eq!( 216 | iter.next(), 217 | Some(ReflowItem { 218 | grapheme: PrintableGrapheme::Grapheme("a"), 219 | grapheme_width: 1, 220 | pos_in_buffer: Position::new(0, 0), 221 | pos_in_screen: ScreenPosition { y: 0, x: 0 }, 222 | }) 223 | ); 224 | assert_eq!( 225 | iter.next(), 226 | Some(ReflowItem { 227 | grapheme: PrintableGrapheme::Grapheme("b"), 228 | grapheme_width: 1, 229 | pos_in_buffer: Position::new(0, 1), 230 | pos_in_screen: ScreenPosition { y: 0, x: 1 }, 231 | }) 232 | ); 233 | assert_eq!( 234 | iter.next(), 235 | Some(ReflowItem { 236 | grapheme: PrintableGrapheme::Grapheme("c"), 237 | grapheme_width: 1, 238 | pos_in_buffer: Position::new(0, 2), 239 | pos_in_screen: ScreenPosition { y: 1, x: 0 }, 240 | }) 241 | ); 242 | } 243 | 244 | #[test] 245 | fn test_width_to_next_tab_stop() { 246 | assert_eq!(width_to_next_tab_stop(0, 4), 4); 247 | assert_eq!(width_to_next_tab_stop(1, 4), 3); 248 | assert_eq!(width_to_next_tab_stop(2, 4), 2); 249 | assert_eq!(width_to_next_tab_stop(3, 4), 1); 250 | assert_eq!(width_to_next_tab_stop(4, 4), 4); 251 | assert_eq!(width_to_next_tab_stop(5, 4), 3); 252 | assert_eq!(width_to_next_tab_stop(6, 4), 2); 253 | assert_eq!(width_to_next_tab_stop(7, 4), 1); 254 | assert_eq!(width_to_next_tab_stop(8, 4), 4); 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/buffer/scroll.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cursor::Position, 3 | paragraph_iter::{Paragraph, ParagraphIndex}, 4 | raw_buffer::RawBuffer, 5 | reflow_iter::ScreenPosition, 6 | }; 7 | 8 | #[derive(Debug, PartialEq, Eq)] 9 | pub struct Scroll { 10 | pub paragraph_index: ParagraphIndex, 11 | pub y_in_paragraph: usize, 12 | // Non-zero only if soft wrap is disabled. 13 | pub x_in_paragraph: usize, 14 | } 15 | 16 | impl Scroll { 17 | pub fn zeroed() -> Scroll { 18 | Scroll { 19 | paragraph_index: ParagraphIndex::zeroed(), 20 | y_in_paragraph: 0, 21 | x_in_paragraph: 0, 22 | } 23 | } 24 | 25 | pub fn scroll_down( 26 | &mut self, 27 | buffer: &RawBuffer, 28 | screen_width: usize, 29 | tab_width: usize, 30 | n: usize, 31 | ) { 32 | for _ in 0..n { 33 | let mut paragraph_iter = 34 | buffer.paragraph_iter_at_index(self.paragraph_index, screen_width, tab_width); 35 | let mut current_paragraph_reflow = paragraph_iter 36 | .next() 37 | .unwrap() 38 | .reflow_iter 39 | .skip_while(|item| item.pos_in_screen.y <= self.y_in_paragraph); 40 | 41 | if current_paragraph_reflow.next().is_some() { 42 | // Scroll within the current paragraph. 43 | self.y_in_paragraph += 1; 44 | continue; 45 | } 46 | 47 | match paragraph_iter.next() { 48 | Some(Paragraph { index, .. }) => { 49 | // Scroll to the next paragraph. 50 | self.paragraph_index = index; 51 | self.y_in_paragraph = 0; 52 | } 53 | None => { 54 | // No more paragraph: at EOF. 55 | return; 56 | } 57 | } 58 | } 59 | } 60 | 61 | pub fn scroll_up( 62 | &mut self, 63 | buffer: &RawBuffer, 64 | screen_width: usize, 65 | tab_width: usize, 66 | n: usize, 67 | ) { 68 | for _ in 0..n { 69 | if self.y_in_paragraph > 0 { 70 | // Scroll within the current paragraph. 71 | self.y_in_paragraph -= 1; 72 | } else { 73 | // Scroll to the previous paragraph. 74 | let mut paragraph_iter = 75 | buffer.paragraph_iter_at_index(self.paragraph_index, screen_width, tab_width); 76 | 77 | if let Some(prev) = paragraph_iter.prev() { 78 | self.paragraph_index = prev.index; 79 | self.y_in_paragraph = prev 80 | .reflow_iter 81 | .map(|item| item.pos_in_screen.y) 82 | .max() 83 | .unwrap_or(0); 84 | } 85 | } 86 | } 87 | } 88 | 89 | #[allow(clippy::too_many_arguments)] 90 | pub fn adjust_scroll( 91 | &mut self, 92 | buffer: &RawBuffer, 93 | virtual_screen_width: usize, 94 | screen_width: usize, 95 | screen_height: usize, 96 | tab_width: usize, 97 | first_visible_pos: Position, 98 | last_visible_pos: Position, 99 | pos: Position, 100 | ) { 101 | if let Some((paragraph_index, pos_in_screen)) = 102 | locate_row(buffer, virtual_screen_width, tab_width, pos) 103 | { 104 | // Scroll vertically. 105 | if pos < first_visible_pos || pos > last_visible_pos { 106 | self.paragraph_index = paragraph_index; 107 | self.y_in_paragraph = pos_in_screen.y; 108 | 109 | if pos > last_visible_pos { 110 | self.scroll_up( 111 | buffer, 112 | virtual_screen_width, 113 | tab_width, 114 | screen_height.saturating_sub(1), 115 | ); 116 | } 117 | } 118 | 119 | // Scroll horizontally (no softwrap). 120 | if virtual_screen_width != usize::MAX { 121 | self.x_in_paragraph = 0; 122 | } 123 | 124 | if pos_in_screen.x >= self.x_in_paragraph + screen_width { 125 | self.x_in_paragraph = pos_in_screen.x - screen_width + 1; 126 | } else if pos_in_screen.x < self.x_in_paragraph { 127 | self.x_in_paragraph = pos_in_screen.x; 128 | } 129 | } 130 | } 131 | } 132 | 133 | fn locate_row( 134 | buffer: &RawBuffer, 135 | screen_width: usize, 136 | tab_width: usize, 137 | pos: Position, 138 | ) -> Option<(ParagraphIndex, ScreenPosition)> { 139 | let mut paragraph = buffer 140 | .paragraph_iter(pos, screen_width, tab_width) 141 | .next() 142 | .unwrap(); 143 | 144 | paragraph 145 | .reflow_iter 146 | .find(|item| item.pos_in_buffer >= pos) 147 | .map(|item| (paragraph.index, item.pos_in_screen)) 148 | } 149 | 150 | #[cfg(test)] 151 | mod tests { 152 | use crate::cursor::Position; 153 | 154 | use super::*; 155 | use pretty_assertions::assert_eq; 156 | 157 | #[test] 158 | fn scroll_down() { 159 | // abc 160 | // xyz 161 | let buf = RawBuffer::from_text("abc\nxyz"); 162 | let mut scroll = Scroll { 163 | paragraph_index: ParagraphIndex::new(&buf, Position::new(0, 0)), 164 | x_in_paragraph: 0, 165 | y_in_paragraph: 0, 166 | }; 167 | 168 | scroll.scroll_down(&buf, 5, 4, 1); 169 | assert_eq!( 170 | scroll, 171 | Scroll { 172 | paragraph_index: ParagraphIndex { buffer_y: 1 }, 173 | x_in_paragraph: 0, 174 | y_in_paragraph: 0, 175 | } 176 | ); 177 | 178 | // Scroll at EOF. No changes. 179 | scroll.scroll_down(&buf, 5, 4, 1); 180 | assert_eq!( 181 | scroll, 182 | Scroll { 183 | paragraph_index: ParagraphIndex { buffer_y: 1 }, 184 | x_in_paragraph: 0, 185 | y_in_paragraph: 0, 186 | } 187 | ); 188 | } 189 | 190 | #[test] 191 | fn scroll_down_soft_wrapped() { 192 | // abcde 193 | // xyz 194 | // 195 | let buf = RawBuffer::from_text("abcdexyz\n"); 196 | let mut scroll = Scroll { 197 | paragraph_index: ParagraphIndex::new(&buf, Position::new(0, 0)), 198 | x_in_paragraph: 0, 199 | y_in_paragraph: 0, 200 | }; 201 | 202 | scroll.scroll_down(&buf, 5, 4, 1); 203 | assert_eq!( 204 | scroll, 205 | Scroll { 206 | paragraph_index: ParagraphIndex { buffer_y: 0 }, 207 | x_in_paragraph: 0, 208 | y_in_paragraph: 1, 209 | } 210 | ); 211 | 212 | scroll.scroll_down(&buf, 5, 4, 1); 213 | assert_eq!( 214 | scroll, 215 | Scroll { 216 | paragraph_index: ParagraphIndex { buffer_y: 1 }, 217 | x_in_paragraph: 0, 218 | y_in_paragraph: 0, 219 | } 220 | ); 221 | } 222 | 223 | #[test] 224 | fn scroll_up() { 225 | // abc 226 | // xyz 227 | let buf = RawBuffer::from_text("abc\nxyz"); 228 | let mut scroll = Scroll { 229 | paragraph_index: ParagraphIndex::new(&buf, Position::new(1, 0)), 230 | x_in_paragraph: 0, 231 | y_in_paragraph: 0, 232 | }; 233 | 234 | scroll.scroll_up(&buf, 5, 4, 1); 235 | assert_eq!( 236 | scroll, 237 | Scroll { 238 | paragraph_index: ParagraphIndex { buffer_y: 0 }, 239 | x_in_paragraph: 0, 240 | y_in_paragraph: 0, 241 | } 242 | ); 243 | 244 | // Scroll at the top. No changes. 245 | scroll.scroll_up(&buf, 5, 4, 1); 246 | assert_eq!( 247 | scroll, 248 | Scroll { 249 | paragraph_index: ParagraphIndex { buffer_y: 0 }, 250 | x_in_paragraph: 0, 251 | y_in_paragraph: 0, 252 | } 253 | ); 254 | } 255 | 256 | #[test] 257 | fn scroll_up_soft_wrapped() { 258 | // abcde 259 | // xyz 260 | let buf = RawBuffer::from_text("abcdexyz"); 261 | let mut scroll = Scroll { 262 | paragraph_index: ParagraphIndex::new(&buf, Position::new(0, 0)), 263 | x_in_paragraph: 0, 264 | y_in_paragraph: 1, 265 | }; 266 | 267 | scroll.scroll_up(&buf, 5, 4, 1); 268 | assert_eq!( 269 | scroll, 270 | Scroll { 271 | paragraph_index: ParagraphIndex { buffer_y: 0 }, 272 | x_in_paragraph: 0, 273 | y_in_paragraph: 0, 274 | } 275 | ); 276 | } 277 | 278 | #[test] 279 | fn test_locate_row() { 280 | // abcde 281 | // xyz 282 | // 123 283 | let buf = RawBuffer::from_text("abcdexyz\n123"); 284 | 285 | assert_eq!( 286 | locate_row(&buf, 5, 4, Position::new(0, 0)), 287 | Some(( 288 | ParagraphIndex { buffer_y: 0 }, 289 | ScreenPosition { y: 0, x: 0 } 290 | )) 291 | ); 292 | assert_eq!( 293 | locate_row(&buf, 5, 4, Position::new(0, 3)), 294 | Some(( 295 | ParagraphIndex { buffer_y: 0 }, 296 | ScreenPosition { y: 0, x: 3 } 297 | )) 298 | ); 299 | assert_eq!( 300 | locate_row(&buf, 5, 4, Position::new(0, 5)), 301 | Some(( 302 | ParagraphIndex { buffer_y: 0 }, 303 | ScreenPosition { y: 1, x: 0 } 304 | )) 305 | ); 306 | assert_eq!( 307 | locate_row(&buf, 5, 4, Position::new(1, 2)), 308 | Some(( 309 | ParagraphIndex { buffer_y: 1 }, 310 | ScreenPosition { y: 0, x: 2 } 311 | )) 312 | ); 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /src/buffer/syntax.rs: -------------------------------------------------------------------------------- 1 | use std::ops::ControlFlow; 2 | 3 | use crate::{ 4 | cursor::{Position, Range}, 5 | mut_raw_buffer::Change, 6 | raw_buffer::RawBuffer, 7 | }; 8 | 9 | use noa_languages::{ 10 | tree_sitter::{ 11 | self, get_highlights_query, get_tree_sitter_parser, InputEdit, Node, QueryCursor, 12 | TextProvider, 13 | }, 14 | Language, 15 | }; 16 | 17 | struct RopeByteChunks<'a>(ropey::iter::Chunks<'a>); 18 | 19 | impl<'a> Iterator for RopeByteChunks<'a> { 20 | type Item = &'a [u8]; 21 | 22 | fn next(&mut self) -> Option { 23 | self.0.next().map(str::as_bytes) 24 | } 25 | } 26 | 27 | struct RopeTextProvider<'a>(&'a RawBuffer); 28 | 29 | impl<'a> TextProvider<'a> for RopeTextProvider<'a> { 30 | type I = RopeByteChunks<'a>; 31 | 32 | fn text(&mut self, node: Node) -> Self::I { 33 | RopeByteChunks(self.0.rope_slice(node.buffer_range()).chunks()) 34 | } 35 | } 36 | 37 | pub struct Query { 38 | raw_query: tree_sitter::Query, 39 | } 40 | 41 | impl Query { 42 | pub fn new( 43 | ts_lang: tree_sitter::Language, 44 | query_str: &str, 45 | ) -> Result { 46 | let raw_query = tree_sitter::Query::new(ts_lang, query_str)?; 47 | 48 | Ok(Query { raw_query }) 49 | } 50 | 51 | pub fn query( 52 | &self, 53 | tree: &tree_sitter::Tree, 54 | buffer: &RawBuffer, 55 | query_range: Option, 56 | mut callback: F, 57 | ) where 58 | F: FnMut(Range, &str), 59 | { 60 | let mut cursor = QueryCursor::new(); 61 | if let Some(range) = query_range { 62 | cursor.set_point_range(range.into()); 63 | } 64 | 65 | let matches = cursor.matches(&self.raw_query, tree.root_node(), RopeTextProvider(buffer)); 66 | for m in matches { 67 | for cap in m.captures { 68 | if let Some(span) = self.raw_query.capture_names().get(cap.index as usize) { 69 | callback(cap.node.buffer_range(), span); 70 | } 71 | } 72 | } 73 | } 74 | 75 | pub fn captures( 76 | &self, 77 | tree: &tree_sitter::Tree, 78 | buffer: &RawBuffer, 79 | query_range: Option, 80 | mut callback: F, 81 | ) where 82 | F: FnMut(Range, &str), 83 | { 84 | let mut cursor = QueryCursor::new(); 85 | if let Some(range) = query_range { 86 | cursor.set_point_range(range.into()); 87 | } 88 | 89 | let captures = cursor.captures(&self.raw_query, tree.root_node(), RopeTextProvider(buffer)); 90 | for (m, _) in captures { 91 | for cap in m.captures { 92 | if let Some(span) = self.raw_query.capture_names().get(cap.index as usize) { 93 | callback(cap.node.buffer_range(), span); 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | #[derive(Debug, PartialEq, Eq)] 101 | pub enum ParserError { 102 | NotSupportedLanguage, 103 | LanguageError(tree_sitter::LanguageError), 104 | QueryError(tree_sitter::QueryError), 105 | ParseError, 106 | } 107 | 108 | pub struct SyntaxParser { 109 | parser: tree_sitter::Parser, 110 | ts_lang: tree_sitter::Language, 111 | tree: tree_sitter::Tree, 112 | } 113 | 114 | impl SyntaxParser { 115 | pub fn new(lang: &Language) -> Result { 116 | let mut parser = tree_sitter::Parser::new(); 117 | let ts_lang = get_tree_sitter_parser(lang.name).ok_or(ParserError::NotSupportedLanguage)?; 118 | parser 119 | .set_language(ts_lang) 120 | .map_err(ParserError::LanguageError)?; 121 | 122 | Ok(SyntaxParser { 123 | tree: parser.parse("", None).ok_or(ParserError::ParseError)?, 124 | ts_lang, 125 | parser, 126 | }) 127 | } 128 | 129 | pub fn tree(&self) -> &tree_sitter::Tree { 130 | &self.tree 131 | } 132 | 133 | pub fn parse_fully(&mut self, buffer: &RawBuffer) { 134 | self.parse(buffer, None); 135 | } 136 | 137 | pub fn parse_incrementally(&mut self, buffer: &RawBuffer, changes: &[Change]) { 138 | self.parse(buffer, Some(changes)); 139 | } 140 | 141 | fn parse(&mut self, buffer: &RawBuffer, changes: Option<&[Change]>) { 142 | let rope = buffer.rope(); 143 | let mut callback = |i, _| { 144 | if i > rope.len_bytes() { 145 | return &[] as &[u8]; 146 | } 147 | 148 | let (chunk, start, _, _) = rope.chunk_at_byte(i); 149 | chunk[i - start..].as_bytes() 150 | }; 151 | 152 | let old_tree = if let Some(changes) = changes { 153 | // Tell tree-sitter about the changes we made since the last parsing. 154 | for change in changes { 155 | self.tree.edit(&InputEdit { 156 | start_byte: change.byte_range.start, 157 | old_end_byte: change.byte_range.end, 158 | new_end_byte: change.byte_range.start + change.insert_text.len(), 159 | start_position: change.range.front().into(), 160 | old_end_position: change.range.back().into(), 161 | new_end_position: change.new_pos.into(), 162 | }); 163 | } 164 | 165 | Some(&self.tree) 166 | } else { 167 | None 168 | }; 169 | 170 | if let Some(new_tree) = self.parser.parse_with(&mut callback, old_tree) { 171 | self.tree = new_tree; 172 | } 173 | } 174 | } 175 | 176 | pub struct Syntax { 177 | tree: tree_sitter::Tree, 178 | highlight_query: Query, 179 | } 180 | 181 | impl Syntax { 182 | pub fn new(lang: &'static Language) -> Result { 183 | let parser = SyntaxParser::new(lang)?; 184 | let highlight_query = Query::new( 185 | parser.ts_lang, 186 | get_highlights_query(lang.name).unwrap_or(""), 187 | ) 188 | .map_err(ParserError::QueryError)?; 189 | 190 | Ok(Syntax { 191 | tree: parser.tree, 192 | highlight_query, 193 | }) 194 | } 195 | 196 | pub fn tree(&self) -> &tree_sitter::Tree { 197 | &self.tree 198 | } 199 | 200 | pub fn set_tree(&mut self, tree: tree_sitter::Tree) { 201 | self.tree = tree; 202 | } 203 | 204 | pub fn query_highlight(&self, buffer: &RawBuffer, range: Range, mut callback: F) 205 | where 206 | F: FnMut(Range, &str), 207 | { 208 | self.highlight_query 209 | .query(self.tree(), buffer, Some(range), &mut callback); 210 | } 211 | 212 | pub fn words(&self, mut callback: F) 213 | where 214 | F: FnMut(Range) -> ControlFlow<()>, 215 | { 216 | const WORD_LEN_MAX: usize = 32; 217 | 218 | self.visit_all_nodes(|node, range| { 219 | if range.start.y != range.end.y { 220 | return ControlFlow::Continue(()); 221 | } 222 | 223 | if range.start.x.abs_diff(range.end.x) > WORD_LEN_MAX { 224 | return ControlFlow::Continue(()); 225 | } 226 | 227 | if !node.kind().ends_with("identifier") { 228 | return ControlFlow::Continue(()); 229 | } 230 | 231 | callback(range) 232 | }); 233 | } 234 | 235 | pub fn visit_all_nodes(&self, mut callback: F) 236 | where 237 | F: FnMut(&tree_sitter::Node<'_>, Range) -> ControlFlow<()>, 238 | { 239 | let root = self.tree.root_node(); 240 | self.visit_ts_node(root, &mut root.walk(), &mut callback); 241 | } 242 | 243 | fn visit_ts_node<'a, 'b, 'tree, F>( 244 | &self, 245 | parent: tree_sitter::Node<'tree>, 246 | cursor: &'b mut tree_sitter::TreeCursor<'tree>, 247 | callback: &mut F, 248 | ) -> ControlFlow<()> 249 | where 250 | F: FnMut(&tree_sitter::Node<'tree>, Range) -> ControlFlow<()>, 251 | { 252 | for node in parent.children(cursor) { 253 | let node_start = node.start_position(); 254 | let node_end = node.end_position(); 255 | let start_pos = Position::new(node_start.row, node_start.column); 256 | let end_pos = Position::new(node_end.row, node_end.column); 257 | let range = Range::from_positions(start_pos, end_pos); 258 | 259 | if callback(&node, range) == ControlFlow::Break(()) { 260 | return ControlFlow::Break(()); 261 | } 262 | 263 | let mut node_cursor = node.walk(); 264 | if node.child_count() > 0 265 | && self.visit_ts_node(node, &mut node_cursor, callback) == ControlFlow::Break(()) 266 | { 267 | return ControlFlow::Break(()); 268 | } 269 | } 270 | 271 | ControlFlow::Continue(()) 272 | } 273 | } 274 | 275 | impl From for tree_sitter::Point { 276 | fn from(pos: Position) -> Self { 277 | tree_sitter::Point { 278 | row: pos.y, 279 | column: pos.x, 280 | } 281 | } 282 | } 283 | 284 | impl From for std::ops::Range { 285 | fn from(range: Range) -> Self { 286 | range.front().into()..range.back().into() 287 | } 288 | } 289 | 290 | pub trait TsNodeExt { 291 | fn buffer_range(&self) -> Range; 292 | } 293 | 294 | impl<'tree> TsNodeExt for tree_sitter::Node<'tree> { 295 | fn buffer_range(&self) -> Range { 296 | let node_start = self.start_position(); 297 | let node_end = self.end_position(); 298 | let start_pos = Position::new(node_start.row, node_start.column); 299 | let end_pos = Position::new(node_end.row, node_end.column); 300 | Range::from_positions(start_pos, end_pos) 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/buffer/word_iter.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | char_iter::CharIter, 3 | cursor::{Position, Range}, 4 | raw_buffer::RawBuffer, 5 | }; 6 | 7 | pub fn is_word_char(c: char) -> bool { 8 | c.is_ascii_alphanumeric() || c == '_' 9 | } 10 | 11 | #[derive(PartialEq)] 12 | pub struct Word<'a> { 13 | buf: &'a RawBuffer, 14 | range: Range, 15 | } 16 | 17 | impl<'a> Word<'a> { 18 | pub fn range(&self) -> Range { 19 | self.range 20 | } 21 | 22 | pub fn text(&self) -> String { 23 | self.buf.substr(self.range) 24 | } 25 | } 26 | 27 | #[derive(Clone)] 28 | pub struct WordIter<'a> { 29 | iter: CharIter<'a>, 30 | } 31 | 32 | impl<'a> WordIter<'a> { 33 | pub fn new(iter: CharIter<'a>) -> WordIter<'a> { 34 | WordIter { iter } 35 | } 36 | 37 | pub fn new_from_beginning_of_word(mut iter: CharIter<'a>) -> WordIter<'a> { 38 | while let Some(ch) = iter.prev() { 39 | if !is_word_char(ch) { 40 | break; 41 | } 42 | } 43 | 44 | WordIter { iter } 45 | } 46 | 47 | pub fn new_from_end_of_word(mut iter: CharIter<'a>) -> WordIter<'a> { 48 | for ch in iter.by_ref() { 49 | if !is_word_char(ch) { 50 | break; 51 | } 52 | } 53 | 54 | WordIter { iter } 55 | } 56 | 57 | pub fn position(&self) -> Position { 58 | self.iter.last_position() 59 | } 60 | 61 | pub fn prev(&mut self) -> Option> { 62 | // Skip until the end of the previous word. 63 | let mut end_pos; 64 | loop { 65 | end_pos = self.iter.last_position(); 66 | match self.iter.prev() { 67 | Some(ch) if is_word_char(ch) => { 68 | break; 69 | } 70 | Some(_) => { 71 | continue; 72 | } 73 | None => { 74 | return None; 75 | } 76 | } 77 | } 78 | 79 | // Find the beginning of the word. 80 | let mut start_pos; 81 | loop { 82 | start_pos = self.iter.last_position(); 83 | match self.iter.prev() { 84 | Some(ch) if !is_word_char(ch) => { 85 | break; 86 | } 87 | None => break, 88 | _ => continue, 89 | } 90 | } 91 | 92 | Some(Word { 93 | buf: self.iter.buffer(), 94 | range: Range::from_positions(start_pos, end_pos), 95 | }) 96 | } 97 | } 98 | 99 | impl<'a> Iterator for WordIter<'a> { 100 | type Item = Word<'a>; 101 | 102 | fn next(&mut self) -> Option { 103 | // Skip until the start of the next word. 104 | loop { 105 | match self.iter.next() { 106 | Some(ch) if is_word_char(ch) => { 107 | break; 108 | } 109 | Some(_) => { 110 | continue; 111 | } 112 | None => { 113 | return None; 114 | } 115 | } 116 | } 117 | 118 | let start_pos = self.iter.last_position(); 119 | 120 | // Find the end of the word. 121 | for ch in self.iter.by_ref() { 122 | if !is_word_char(ch) { 123 | break; 124 | } 125 | } 126 | 127 | let end_pos = self.iter.last_position(); 128 | Some(Word { 129 | buf: self.iter.buffer(), 130 | range: Range::from_positions(start_pos, end_pos), 131 | }) 132 | } 133 | } 134 | 135 | #[cfg(test)] 136 | mod tests { 137 | use pretty_assertions::assert_eq; 138 | 139 | use super::*; 140 | 141 | fn next_word(iter: &mut WordIter) -> Option { 142 | iter.next().map(|w| w.range()) 143 | } 144 | 145 | fn prev_word(iter: &mut WordIter) -> Option { 146 | iter.prev().map(|w| w.range()) 147 | } 148 | 149 | #[test] 150 | fn word_iter_from_current_word() { 151 | let buffer = RawBuffer::from_text(""); 152 | let mut iter = buffer.word_iter_from_beginning_of_word(Position::new(0, 0)); 153 | assert_eq!(next_word(&mut iter), None); 154 | 155 | let buffer = RawBuffer::from_text("ABC DEF XYZ"); 156 | let mut iter = buffer.word_iter_from_beginning_of_word(Position::new(0, 1)); 157 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 0, 0, 3))); 158 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 4, 0, 7))); 159 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 8, 0, 11))); 160 | assert_eq!(next_word(&mut iter), None); 161 | 162 | let mut iter = buffer.word_iter_from_beginning_of_word(Position::new(0, 3)); 163 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 0, 0, 3))); 164 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 4, 0, 7))); 165 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 8, 0, 11))); 166 | assert_eq!(next_word(&mut iter), None); 167 | 168 | let buffer = RawBuffer::from_text(" foo(bar, baz)"); 169 | let mut iter = buffer.word_iter_from_beginning_of_word(Position::new(0, 0)); 170 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 4, 0, 7))); // "foo" 171 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 8, 0, 11))); // "bar" 172 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 13, 0, 16))); // "baz" 173 | assert_eq!(next_word(&mut iter), None); 174 | 175 | let buffer = RawBuffer::from_text("ABC\nUVW XYZ"); 176 | let mut iter = buffer.word_iter_from_beginning_of_word(Position::new(0, 0)); 177 | assert_eq!(next_word(&mut iter), Some(Range::new(0, 0, 0, 3))); // "ABC" 178 | assert_eq!(next_word(&mut iter), Some(Range::new(1, 0, 1, 3))); // "UVW" 179 | assert_eq!(next_word(&mut iter), Some(Range::new(1, 4, 1, 7))); // "XYZ" 180 | assert_eq!(next_word(&mut iter), None); 181 | 182 | let mut iter = buffer.word_iter_from_beginning_of_word(Position::new(1, 0)); 183 | assert_eq!(next_word(&mut iter), Some(Range::new(1, 0, 1, 3))); 184 | assert_eq!(next_word(&mut iter), Some(Range::new(1, 4, 1, 7))); 185 | assert_eq!(next_word(&mut iter), None); 186 | } 187 | 188 | #[test] 189 | fn iter_prev_words() { 190 | let buffer = RawBuffer::from_text(""); 191 | let mut iter = buffer.word_iter_from_beginning_of_word(Position::new(0, 0)); 192 | assert_eq!(prev_word(&mut iter), None); 193 | 194 | let buffer = RawBuffer::from_text("ABC DEF XYZ"); 195 | let mut iter = buffer.word_iter_from_end_of_word(Position::new(0, 10)); 196 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 8, 0, 11))); 197 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 4, 0, 7))); 198 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 0, 0, 3))); 199 | assert_eq!(prev_word(&mut iter), None); 200 | 201 | let mut iter = buffer.word_iter_from_end_of_word(Position::new(0, 3)); 202 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 0, 0, 3))); 203 | assert_eq!(prev_word(&mut iter), None); 204 | 205 | let buffer = RawBuffer::from_text(" foo(bar, baz)"); 206 | let mut iter = buffer.word_iter_from_end_of_word(Position::new(0, 17)); 207 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 13, 0, 16))); // "baz" 208 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 8, 0, 11))); // "bar" 209 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 4, 0, 7))); // "foo" 210 | assert_eq!(prev_word(&mut iter), None); 211 | 212 | let buffer = RawBuffer::from_text("ABC\nUVW XYZ"); 213 | let mut iter = buffer.word_iter_from_end_of_word(Position::new(1, 7)); 214 | assert_eq!(prev_word(&mut iter), Some(Range::new(1, 4, 1, 7))); // "XYZ" 215 | assert_eq!(prev_word(&mut iter), Some(Range::new(1, 0, 1, 3))); // "UVW" 216 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 0, 0, 3))); // "ABC" 217 | assert_eq!(prev_word(&mut iter), None); 218 | 219 | let mut iter = buffer.word_iter_from_end_of_word(Position::new(1, 0)); 220 | assert_eq!(prev_word(&mut iter), Some(Range::new(1, 0, 1, 3))); 221 | assert_eq!(prev_word(&mut iter), Some(Range::new(0, 0, 0, 3))); 222 | assert_eq!(prev_word(&mut iter), None); 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noa_common" 3 | version = "0.0.0" 4 | authors = ["Seiya Nuta "] 5 | edition = "2021" 6 | 7 | [lib] 8 | path = "lib.rs" 9 | 10 | [dependencies] 11 | backtrace = "0" 12 | log = { version = "0", features = ["std"] } 13 | anyhow = "1" 14 | dirs = "3" 15 | once_cell = "1" 16 | tempfile = "3" 17 | -------------------------------------------------------------------------------- /src/common/dirs.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::create_dir_all, 3 | path::{Path, PathBuf}, 4 | }; 5 | 6 | use anyhow::Context; 7 | 8 | pub fn path_into_dotted_str(path: &Path) -> String { 9 | path.to_str() 10 | .unwrap() 11 | .trim_start_matches('/') 12 | .replace('/', ".") 13 | } 14 | 15 | pub fn noa_dir() -> PathBuf { 16 | let dir = dirs::home_dir() 17 | .expect("where's your home dir?") 18 | .join(".noa"); 19 | 20 | create_dir_all(&dir).expect("failed to create dir"); 21 | dir 22 | } 23 | 24 | pub fn noa_workdir(workdir: &Path) -> PathBuf { 25 | let workdir = workdir 26 | .canonicalize() 27 | .with_context(|| format!("failed to resolve the workspace dir: {}", workdir.display())) 28 | .unwrap(); 29 | 30 | let dir = noa_dir() 31 | .join("workdirs") 32 | .join(Path::new(&path_into_dotted_str(&workdir))); 33 | create_dir_all(&dir).expect("failed to create dir"); 34 | dir 35 | } 36 | 37 | pub fn log_file_path(name: &str) -> PathBuf { 38 | let log_dir = noa_dir().join("log"); 39 | create_dir_all(&log_dir).expect("failed to create dir"); 40 | log_dir.join(&format!("{}.log", name)) 41 | } 42 | 43 | pub fn backup_dir() -> PathBuf { 44 | let backup_dir = noa_dir().join("backup"); 45 | create_dir_all(&backup_dir).expect("failed to create dir"); 46 | backup_dir 47 | } 48 | -------------------------------------------------------------------------------- /src/common/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | #[macro_use] 5 | pub mod logger; 6 | 7 | pub mod dirs; 8 | -------------------------------------------------------------------------------- /src/common/logger.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use backtrace::Backtrace; 3 | use log::{Level, LevelFilter}; 4 | use once_cell::sync::OnceCell; 5 | use std::{ 6 | fmt::Debug, 7 | fs::{create_dir_all, File, OpenOptions}, 8 | io::{self, prelude::*, BufReader, SeekFrom}, 9 | path::Path, 10 | sync::Mutex, 11 | }; 12 | 13 | use crate::dirs::log_file_path; 14 | 15 | const LOG_FILE_LEN_MAX: usize = 256 * 1024; 16 | 17 | struct Logger { 18 | log_file: Mutex, 19 | } 20 | 21 | impl Logger { 22 | pub fn new(file: File) -> Self { 23 | Logger { 24 | log_file: Mutex::new(file), 25 | } 26 | } 27 | } 28 | 29 | impl log::Log for Logger { 30 | fn enabled(&self, _metadata: &log::Metadata) -> bool { 31 | true 32 | } 33 | 34 | fn flush(&self) {} 35 | 36 | fn log(&self, record: &log::Record) { 37 | let color_start = match record.level() { 38 | Level::Error => "\x1b[1;31m", 39 | Level::Warn => "\x1b[1;33m", 40 | _ => "\x1b[34m", 41 | }; 42 | let color_end = match record.level() { 43 | Level::Error => "\x1b[1;31m", 44 | Level::Warn => "\x1b[1;33m", 45 | _ => "\x1b[0m", 46 | }; 47 | let prefix = match record.level() { 48 | Level::Error => " Error:", 49 | Level::Warn => " Warn:", 50 | _ => "", 51 | }; 52 | let filename = record.file().unwrap_or_else(|| record.target()); 53 | let lineno = record.line().unwrap_or(0); 54 | let message = format!( 55 | "{color_start}[{filename}:{lineno}]{prefix}{color_end} {}\n", 56 | record.args() 57 | ); 58 | 59 | let _ = self.log_file.lock().unwrap().write_all(message.as_bytes()); 60 | } 61 | } 62 | 63 | pub fn shrink_file(path: &Path, max_len: usize) -> Result<()> { 64 | let meta = match std::fs::metadata(path) { 65 | Ok(meta) => meta, 66 | Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()), 67 | Err(err) => return Err(err.into()), 68 | }; 69 | 70 | let current_len: usize = meta.len().try_into()?; 71 | if current_len <= max_len { 72 | return Ok(()); 73 | } 74 | 75 | let new_len = current_len - max_len; 76 | 77 | // Look for the nearest newline character. 78 | let mut file = std::fs::OpenOptions::new().read(true).open(path)?; 79 | file.seek(SeekFrom::Current(new_len.try_into()?))?; 80 | let mut reader = BufReader::new(file); 81 | let mut buf = Vec::new(); 82 | reader.read_until(b'\n', &mut buf)?; 83 | 84 | // Copy contents after the newline character and replace the old file. 85 | let mut new_file = tempfile::NamedTempFile::new()?; 86 | std::io::copy(&mut reader, &mut new_file)?; 87 | new_file.persist(path)?; 88 | 89 | Ok(()) 90 | } 91 | 92 | pub fn install_logger(name: &str) { 93 | let log_path = log_file_path(name); 94 | shrink_file(&log_path, LOG_FILE_LEN_MAX).expect("failed to shrink the log file"); 95 | let _ = create_dir_all(log_path.parent().unwrap()); 96 | let log_file = OpenOptions::new() 97 | .append(true) 98 | .create(true) 99 | .open(log_path) 100 | .expect("failed to open the log file"); 101 | 102 | log::set_max_level(if cfg!(debug_assertions) { 103 | LevelFilter::Debug 104 | } else { 105 | LevelFilter::Info 106 | }); 107 | 108 | log::set_boxed_logger(Box::new(Logger::new(log_file))).expect("failed to set the logger"); 109 | 110 | std::panic::set_hook(Box::new(|info| { 111 | error!("panic: {}", info); 112 | prettify_backtrace(backtrace::Backtrace::new()); 113 | })); 114 | } 115 | 116 | pub fn prettify_backtrace(backtrace: Backtrace) { 117 | for (i, frame) in backtrace.frames().iter().enumerate() { 118 | for symbol in frame.symbols() { 119 | if let Some(path) = symbol.filename() { 120 | let filename = path.to_str().unwrap_or("(non-utf8 path)"); 121 | if filename.contains("/.rustup/") 122 | || filename.contains("/.cargo/") 123 | || filename.starts_with("/rustc/") 124 | { 125 | continue; 126 | } 127 | 128 | warn!( 129 | " #{} {}:{}, col {}", 130 | i, 131 | filename, 132 | symbol.lineno().unwrap_or(0), 133 | symbol.colno().unwrap_or(0), 134 | ); 135 | } 136 | } 137 | } 138 | } 139 | 140 | pub fn backtrace() { 141 | prettify_backtrace(backtrace::Backtrace::new()); 142 | } 143 | 144 | pub trait OopsExt: Sized { 145 | fn oops(self); 146 | } 147 | 148 | impl OopsExt for std::result::Result { 149 | fn oops(self) { 150 | match self { 151 | Ok(_) => {} 152 | Err(err) => { 153 | warn!("oops: {:?}", err); 154 | crate::logger::backtrace(); 155 | } 156 | } 157 | } 158 | } 159 | 160 | #[macro_export] 161 | macro_rules! debug_warn { 162 | ($($arg:tt)*) => {{ 163 | #[cfg(debug_assertions)] 164 | { 165 | warn!($($arg)*); 166 | } 167 | }} 168 | } 169 | 170 | #[macro_export] 171 | macro_rules! warn_once { 172 | ($($arg:tt)*) => {{ 173 | static WARN_ONCE: ::std::sync::Once = ::std::sync::Once::new(); 174 | WARN_ONCE.call_once(|| warn!($($arg)*)); 175 | }} 176 | } 177 | 178 | #[macro_export] 179 | macro_rules! debug_warn_once { 180 | ($($arg:tt)*) => {{ 181 | #[cfg(debug_assertions)] 182 | { 183 | $crate::warn_once!($($arg)*); 184 | } 185 | }} 186 | } 187 | 188 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 189 | pub enum TimingTraceMode { 190 | Disabled, 191 | All, 192 | OutliersOnly, 193 | } 194 | 195 | pub static ENABLE_TIMING_TRACE: OnceCell = OnceCell::new(); 196 | 197 | #[macro_export] 198 | macro_rules! trace_timing { 199 | ($title:expr, $threshold_ms:expr, $($block:block)*) => {{ 200 | use $crate::logger::TimingTraceMode; 201 | 202 | let mode = *$crate::logger::ENABLE_TIMING_TRACE.get_or_init(|| { 203 | match std::env::var("TIMING_TRACE") { 204 | Ok(s) if s == "all" => TimingTraceMode::All, 205 | Ok(_) => TimingTraceMode::OutliersOnly, 206 | _ => TimingTraceMode::Disabled, 207 | } 208 | }); 209 | let tracing_start = if mode != TimingTraceMode::Disabled { 210 | Some(::std::time::Instant::now()) 211 | } else { 212 | None 213 | }; 214 | 215 | $($block)*; 216 | 217 | if let Some(tracing_start) = tracing_start { 218 | let elapsed = tracing_start.elapsed(); 219 | match mode { 220 | TimingTraceMode::All => { 221 | info!("{} timing: {:?}", $title, elapsed); 222 | } 223 | TimingTraceMode::OutliersOnly => { 224 | if elapsed.as_millis() > $threshold_ms { 225 | warn!("{} timing: {:?}", $title, elapsed); 226 | } 227 | } 228 | TimingTraceMode::Disabled => { 229 | } 230 | } 231 | } 232 | }}; 233 | } 234 | -------------------------------------------------------------------------------- /src/compositor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noa_compositor" 3 | version = "0.0.0" 4 | authors = ["Seiya Nuta "] 5 | edition = "2021" 6 | 7 | [lib] 8 | path = "lib.rs" 9 | 10 | [dependencies] 11 | log = "0" 12 | tokio = { version = "1", features = ["full"] } 13 | futures = "0" 14 | crossterm = { version = "0", features = ["event-stream"] } 15 | arrayvec = "0" 16 | unicode-segmentation = "1" 17 | 18 | noa_common = { path = "../common" } 19 | noa_buffer = { path = "../buffer" } 20 | 21 | [dev-dependencies] 22 | pretty_assertions = "1" 23 | -------------------------------------------------------------------------------- /src/compositor/compositor.rs: -------------------------------------------------------------------------------- 1 | use std::slice; 2 | 3 | use noa_common::trace_timing; 4 | use tokio::sync::mpsc; 5 | 6 | use crate::{surface::HandledEvent, terminal::InputEvent}; 7 | 8 | use super::{ 9 | canvas::Canvas, 10 | surface::{Layout, RectSize, Surface}, 11 | terminal::{self, Terminal}, 12 | }; 13 | 14 | pub struct Layer { 15 | pub surface: Box + Send>, 16 | pub canvas: Canvas, 17 | pub screen_y: usize, 18 | pub screen_x: usize, 19 | } 20 | 21 | pub struct Compositor { 22 | terminal: Terminal, 23 | term_rx: mpsc::UnboundedReceiver, 24 | screens: [Canvas; 2], 25 | screen_size: RectSize, 26 | active_screen_index: usize, 27 | /// The last element comes foreground. 28 | layers: Vec>, 29 | /// A temporary vec to avoid mutual borrowing of self. 30 | past_layers: Vec>, 31 | } 32 | 33 | impl Compositor { 34 | pub fn new() -> Compositor { 35 | let (term_tx, term_rx) = mpsc::unbounded_channel(); 36 | let terminal = Terminal::new(term_tx); 37 | let screen_size = RectSize { 38 | height: terminal.height(), 39 | width: terminal.width(), 40 | }; 41 | 42 | Compositor { 43 | terminal, 44 | term_rx, 45 | screens: [ 46 | Canvas::new(screen_size.height, screen_size.width), 47 | Canvas::new(screen_size.height, screen_size.width), 48 | ], 49 | screen_size, 50 | active_screen_index: 0, 51 | layers: Vec::new(), 52 | past_layers: Vec::new(), 53 | } 54 | } 55 | 56 | pub fn screen_size(&self) -> RectSize { 57 | self.screen_size 58 | } 59 | 60 | pub async fn run_in_cooked_mode(&mut self, ctx: &mut C, f: F) -> R 61 | where 62 | F: FnOnce() -> R, 63 | { 64 | let result = self.terminal.run_in_cooked_mode(f).await; 65 | self.force_render(ctx); 66 | result 67 | } 68 | 69 | pub async fn receive_event(&mut self) -> Option { 70 | self.term_rx.recv().await 71 | } 72 | 73 | pub fn handle_event(&mut self, ctx: &mut C, ev: terminal::Event) { 74 | match ev { 75 | terminal::Event::Input(input_event) => { 76 | trace_timing!("handle_input_event", 20 /* ms */, { 77 | self.handle_input_event(ctx, input_event); 78 | }); 79 | } 80 | terminal::Event::Resize { height, width } => { 81 | trace_timing!("resize_screen", 10 /* ms */, { 82 | self.resize_screen(height, width); 83 | }); 84 | } 85 | } 86 | } 87 | 88 | pub fn add_frontmost_layer(&mut self, surface: Box + Send>) { 89 | debug_assert!(self 90 | .layers 91 | .iter() 92 | .all(|l| l.surface.name() != surface.name())); 93 | 94 | self.layers.push(Layer { 95 | surface, 96 | canvas: Canvas::new(0, 0), 97 | screen_x: 0, 98 | screen_y: 0, 99 | }); 100 | } 101 | 102 | pub fn contains_surface_with_name(&self, name: &str) -> bool { 103 | self.layers.iter().any(|l| l.surface.name() == name) 104 | } 105 | 106 | pub fn get_mut_surface_by_name(&mut self, name: &str) -> &mut S 107 | where 108 | S: Surface, 109 | { 110 | for layer in self.layers.iter_mut() { 111 | if layer.surface.name() == name { 112 | return layer 113 | .surface 114 | .as_any_mut() 115 | .downcast_mut::() 116 | .expect("surface type mismatch"); 117 | } 118 | } 119 | 120 | for layer in self.past_layers.iter_mut() { 121 | if layer.surface.name() == name { 122 | return layer 123 | .surface 124 | .as_any_mut() 125 | .downcast_mut::() 126 | .expect("surface type mismatch"); 127 | } 128 | } 129 | 130 | unreachable!("surface \"{}\" not found", name); 131 | } 132 | 133 | fn resize_screen(&mut self, height: usize, width: usize) { 134 | self.screen_size = RectSize { height, width }; 135 | self.screens = [Canvas::new(height, width), Canvas::new(height, width)]; 136 | self.terminal.clear(); 137 | } 138 | 139 | pub fn force_render(&mut self, ctx: &mut C) { 140 | self.screens[self.active_screen_index].invalidate(); 141 | self.render(ctx); 142 | } 143 | 144 | pub fn render(&mut self, ctx: &mut C) { 145 | // Re-layout layers. 146 | let mut prev_cursor_pos = None; 147 | for layer in self.layers.iter_mut() { 148 | if !layer.surface.is_active(ctx) { 149 | continue; 150 | } 151 | 152 | let ((screen_y, screen_x), rect_size) = 153 | relayout_layer(ctx, &mut *layer.surface, self.screen_size, prev_cursor_pos); 154 | layer.screen_x = screen_x; 155 | layer.screen_y = screen_y; 156 | layer.canvas = Canvas::new(rect_size.height, rect_size.width); 157 | 158 | if let Some((surface_y, surface_x)) = layer.surface.cursor_position(ctx) { 159 | prev_cursor_pos = Some((screen_y + surface_y, screen_x + surface_x)); 160 | } 161 | } 162 | 163 | let prev_screen_index = self.active_screen_index; 164 | self.active_screen_index = (self.active_screen_index + 1) % self.screens.len(); 165 | let screen_index = self.active_screen_index; 166 | 167 | // Render and composite layers. 168 | compose_layers(ctx, &mut self.screens[screen_index], self.layers.iter_mut()); 169 | 170 | // Get the cursor position. 171 | let mut cursor = None; 172 | for layer in self.layers.iter().rev() { 173 | if layer.surface.is_active(ctx) { 174 | if let Some((y, x)) = layer.surface.cursor_position(ctx) { 175 | cursor = Some((layer.screen_y + y, layer.screen_x + x)); 176 | break; 177 | } 178 | } 179 | } 180 | 181 | // Compute diffs. 182 | let draw_ops = self.screens[screen_index].diff(&self.screens[prev_screen_index]); 183 | 184 | // Write into the terminal. 185 | let mut drawer = self.terminal.drawer(); 186 | drawer.before_drawing(); 187 | 188 | for op in draw_ops { 189 | drawer.draw(&op); 190 | } 191 | 192 | drawer.flush(cursor); 193 | } 194 | 195 | fn handle_input_event(&mut self, ctx: &mut C, input: InputEvent) { 196 | match input { 197 | InputEvent::Key(key) => { 198 | self.past_layers = Vec::new(); 199 | while let Some(mut layer) = self.layers.pop() { 200 | let result = if layer.surface.is_active(ctx) { 201 | layer.surface.handle_key_event(ctx, self, key) 202 | } else { 203 | HandledEvent::Ignored 204 | }; 205 | self.past_layers.push(layer); 206 | if result == HandledEvent::Consumed { 207 | break; 208 | } 209 | } 210 | self.layers.extend(self.past_layers.drain(..).rev()); 211 | } 212 | InputEvent::Mouse(ev) => { 213 | self.past_layers = Vec::new(); 214 | while let Some(mut layer) = self.layers.pop() { 215 | let screen_y = ev.row as usize; 216 | let screen_x = ev.column as usize; 217 | let in_bounds = layer.screen_y <= screen_y 218 | && screen_y < layer.screen_y + layer.canvas.height() 219 | && layer.screen_x <= screen_x 220 | && screen_x < layer.screen_x + layer.canvas.width(); 221 | 222 | let result = if layer.surface.is_active(ctx) && in_bounds { 223 | layer.surface.handle_mouse_event( 224 | ctx, 225 | self, 226 | ev.kind, 227 | ev.modifiers, 228 | screen_y - layer.screen_y, 229 | screen_x - layer.screen_x, 230 | ) 231 | } else { 232 | HandledEvent::Ignored 233 | }; 234 | 235 | self.past_layers.push(layer); 236 | if result == HandledEvent::Consumed { 237 | break; 238 | } 239 | } 240 | self.layers.extend(self.past_layers.drain(..).rev()); 241 | } 242 | InputEvent::KeyBatch(input) => { 243 | self.past_layers = Vec::new(); 244 | while let Some(mut layer) = self.layers.pop() { 245 | let result = if layer.surface.is_active(ctx) { 246 | layer.surface.handle_key_batch_event(ctx, self, &input) 247 | } else { 248 | HandledEvent::Ignored 249 | }; 250 | 251 | self.past_layers.push(layer); 252 | if result == HandledEvent::Consumed { 253 | break; 254 | } 255 | } 256 | self.layers.extend(self.past_layers.drain(..).rev()); 257 | } 258 | } 259 | } 260 | } 261 | 262 | impl Default for Compositor { 263 | fn default() -> Self { 264 | Compositor::new() 265 | } 266 | } 267 | 268 | /// Renders each surfaces and copy the compose into the screen canvas. 269 | fn compose_layers( 270 | ctx: &mut C, 271 | screen: &mut Canvas, 272 | layers: slice::IterMut<'_, Layer>, 273 | ) { 274 | screen.view_mut().clear(); 275 | 276 | for layer in layers { 277 | if !layer.surface.is_active(ctx) { 278 | continue; 279 | } 280 | 281 | // Handle the case when the screen is too small. 282 | let too_small = screen.width() < 10 || screen.height() < 5; 283 | let is_too_small_layer = layer.surface.name() == "too_small"; 284 | match (too_small, is_too_small_layer) { 285 | (true, true) => {} /* render too_small layer */ 286 | (false, false) => {} /* render layers except too_small */ 287 | _ => continue, 288 | } 289 | 290 | layer.surface.render(ctx, &mut layer.canvas.view_mut()); 291 | screen.copy_from_other(layer.screen_y, layer.screen_x, &layer.canvas); 292 | } 293 | } 294 | 295 | fn relayout_layer( 296 | ctx: &mut C, 297 | surface: &mut (impl Surface + ?Sized), 298 | screen_size: RectSize, 299 | prev_cursor_pos: Option<(usize, usize)>, 300 | ) -> ((usize, usize), RectSize) { 301 | let (layout, rect) = surface.layout(ctx, screen_size); 302 | 303 | let (screen_y, screen_x) = match layout { 304 | Layout::Fixed { y, x } => (y, x), 305 | Layout::Center => ( 306 | (screen_size.height / 2).saturating_sub(rect.height / 2), 307 | (screen_size.width / 2).saturating_sub(rect.width / 2), 308 | ), 309 | Layout::AroundCursor => { 310 | let (cursor_y, cursor_x) = prev_cursor_pos.unwrap(); 311 | let y = if cursor_y + rect.height + 1 > screen_size.height { 312 | cursor_y.saturating_sub(rect.height + 1) 313 | } else { 314 | cursor_y + 1 315 | }; 316 | 317 | let x = if cursor_x + rect.width > screen_size.width { 318 | cursor_x.saturating_sub(rect.width) 319 | } else { 320 | cursor_x 321 | }; 322 | 323 | (y, x) 324 | } 325 | }; 326 | 327 | ((screen_y, screen_x), rect) 328 | } 329 | -------------------------------------------------------------------------------- /src/compositor/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate log; 3 | 4 | pub mod canvas; 5 | pub mod compositor; 6 | pub mod surface; 7 | pub mod terminal; 8 | -------------------------------------------------------------------------------- /src/compositor/surface.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | 3 | use crate::compositor::Compositor; 4 | 5 | use super::canvas::CanvasViewMut; 6 | pub use crossterm::event::{KeyEvent, MouseEvent}; 7 | use crossterm::event::{KeyModifiers, MouseEventKind}; 8 | 9 | #[derive(Clone, Copy, Debug)] 10 | pub enum Layout { 11 | Fixed { y: usize, x: usize }, 12 | Center, 13 | AroundCursor, 14 | } 15 | 16 | #[derive(Clone, Copy, Debug)] 17 | pub struct RectSize { 18 | pub height: usize, 19 | pub width: usize, 20 | } 21 | 22 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 23 | pub enum HandledEvent { 24 | Consumed, 25 | Ignored, 26 | } 27 | 28 | pub trait Surface: Any { 29 | type Context; 30 | 31 | fn name(&self) -> &str; 32 | fn as_any_mut(&mut self) -> &mut dyn Any; 33 | fn is_active(&self, ctx: &mut Self::Context) -> bool; 34 | fn layout(&mut self, ctx: &mut Self::Context, screen_size: RectSize) -> (Layout, RectSize); 35 | /// Returns the cursor position in surface-local `(y, x)`. `None` if the cursor 36 | /// is hidden. 37 | fn cursor_position(&self, ctx: &mut Self::Context) -> Option<(usize, usize)>; 38 | /// Render its contents into the canvas. It must fill the whole canvas; the 39 | /// canvas can be the newly created one due to, for example, screen resizing. 40 | fn render(&mut self, ctx: &mut Self::Context, canvas: &mut CanvasViewMut<'_>); 41 | 42 | fn handle_key_event( 43 | &mut self, 44 | _ctx: &mut Self::Context, 45 | _compositor: &mut Compositor, 46 | _key: KeyEvent, 47 | ) -> HandledEvent { 48 | HandledEvent::Ignored 49 | } 50 | 51 | fn handle_mouse_event( 52 | &mut self, 53 | _ctx: &mut Self::Context, 54 | _compositor: &mut Compositor, 55 | _kind: MouseEventKind, 56 | _modifiers: KeyModifiers, 57 | _surface_y: usize, 58 | _surface_x: usize, 59 | ) -> HandledEvent { 60 | HandledEvent::Ignored 61 | } 62 | fn handle_key_batch_event( 63 | &mut self, 64 | _ctx: &mut Self::Context, 65 | _compositor: &mut Compositor, 66 | _input: &str, 67 | ) -> HandledEvent { 68 | HandledEvent::Ignored 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/compositor/terminal.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | io::{stdout, Stdout, Write}, 4 | time::Duration, 5 | }; 6 | 7 | use crossterm::{ 8 | cursor::{self, MoveTo}, 9 | event::{DisableMouseCapture, EnableMouseCapture, Event as TermEvent, EventStream, KeyEvent}, 10 | execute, queue, 11 | style::{Attribute, Print, SetAttribute, SetBackgroundColor, SetForegroundColor}, 12 | terminal::*, 13 | }; 14 | use futures::{channel::oneshot, StreamExt}; 15 | 16 | pub use crossterm::event::{KeyCode, KeyModifiers, MouseButton, MouseEvent}; 17 | use tokio::{sync::mpsc::UnboundedSender, task::JoinHandle}; 18 | 19 | use crate::canvas::DrawOp; 20 | 21 | #[derive(Clone, PartialEq, Eq, Debug)] 22 | pub enum InputEvent { 23 | Key(KeyEvent), 24 | Mouse(MouseEvent), 25 | KeyBatch(String), 26 | } 27 | 28 | #[derive(Clone, PartialEq, Eq, Debug)] 29 | pub enum Event { 30 | Input(InputEvent), 31 | Resize { height: usize, width: usize }, 32 | } 33 | 34 | pub struct Terminal { 35 | height: usize, 36 | width: usize, 37 | event_tx: UnboundedSender, 38 | stdio_listener: Option<(JoinHandle<()>, oneshot::Sender<()>)>, 39 | } 40 | 41 | impl Terminal { 42 | pub fn new(event_tx: UnboundedSender) -> Terminal { 43 | enable_raw_mode().expect("failed to enable the raw mode"); 44 | 45 | let mut stdout = stdout(); 46 | queue!(stdout, EnterAlternateScreen, EnableMouseCapture).ok(); 47 | stdout.flush().ok(); 48 | 49 | let (event_abort_tx, event_abort_rx) = oneshot::channel(); 50 | let stdio_listener = listen_events(event_tx.clone(), event_abort_rx); 51 | 52 | let (cols, rows) = size().expect("failed to get the terminal size"); 53 | Terminal { 54 | height: rows as usize, 55 | width: cols as usize, 56 | stdio_listener: Some((stdio_listener, event_abort_tx)), 57 | event_tx, 58 | } 59 | } 60 | 61 | pub fn height(&self) -> usize { 62 | self.height 63 | } 64 | 65 | pub fn width(&self) -> usize { 66 | self.width 67 | } 68 | 69 | pub async fn run_in_cooked_mode(&mut self, f: F) -> R 70 | where 71 | F: FnOnce() -> R, 72 | { 73 | let (join_handle, abort) = self.stdio_listener.take().unwrap(); 74 | abort.send(()).unwrap(); 75 | join_handle.await.unwrap(); 76 | execute!(stdout(), DisableMouseCapture).ok(); 77 | disable_raw_mode().unwrap(); 78 | 79 | let result = f(); 80 | 81 | let (event_abort_tx, event_abort_rx) = oneshot::channel(); 82 | let stdio_listener = listen_events(self.event_tx.clone(), event_abort_rx); 83 | self.stdio_listener = Some((stdio_listener, event_abort_tx)); 84 | execute!(stdout(), DisableMouseCapture, EnterAlternateScreen).ok(); 85 | enable_raw_mode().unwrap(); 86 | 87 | result 88 | } 89 | 90 | pub fn clear(&mut self) { 91 | execute!( 92 | stdout(), 93 | SetAttribute(Attribute::Reset), 94 | Clear(ClearType::All) 95 | ) 96 | .ok(); 97 | } 98 | 99 | pub fn drawer(&mut self) -> Drawer<'_> { 100 | Drawer { 101 | stdout: stdout(), 102 | _terminal: self, 103 | } 104 | } 105 | } 106 | 107 | impl Drop for Terminal { 108 | fn drop(&mut self) { 109 | let mut stdout = stdout(); 110 | let _ = execute!(stdout, DisableMouseCapture); 111 | let _ = execute!(stdout, LeaveAlternateScreen); 112 | disable_raw_mode().ok(); 113 | } 114 | } 115 | 116 | fn listen_events( 117 | event_tx: UnboundedSender, 118 | mut abort: oneshot::Receiver<()>, 119 | ) -> JoinHandle<()> { 120 | tokio::spawn(async move { 121 | fn convert_event(ev: TermEvent) -> Event { 122 | match ev { 123 | TermEvent::Key(key) => Event::Input(InputEvent::Key(key)), 124 | TermEvent::Mouse(ev) => Event::Input(InputEvent::Mouse(ev)), 125 | TermEvent::Resize(cols, rows) => Event::Resize { 126 | width: cols as usize, 127 | height: rows as usize, 128 | }, 129 | } 130 | } 131 | 132 | fn is_next_available() -> bool { 133 | crossterm::event::poll(Duration::from_secs(0)).unwrap() 134 | } 135 | 136 | let mut stream = EventStream::new().fuse(); 137 | loop { 138 | tokio::select! { 139 | biased; 140 | Ok(_) = &mut abort => { 141 | break; 142 | } 143 | Some(Ok(ev)) = stream.next() => { 144 | match ev { 145 | TermEvent::Key(KeyEvent { 146 | code: KeyCode::Char(key), 147 | modifiers: KeyModifiers::NONE, 148 | }) if is_next_available() => { 149 | let mut next_event = None; 150 | let mut buf = key.to_string(); 151 | while is_next_available() && next_event.is_none() { 152 | if let Some(Ok(ev)) = stream.next().await { 153 | match ev { 154 | TermEvent::Key(KeyEvent { 155 | code: KeyCode::Char(ch), 156 | modifiers: KeyModifiers::SHIFT, 157 | }) => { 158 | buf.push(ch); 159 | } 160 | TermEvent::Key(KeyEvent { 161 | code, 162 | modifiers: KeyModifiers::NONE, 163 | }) => match code { 164 | KeyCode::Char(ch) => { 165 | buf.push(ch); 166 | } 167 | KeyCode::Enter => { 168 | buf.push('\n'); 169 | } 170 | KeyCode::Tab => { 171 | buf.push('\t'); 172 | } 173 | _ => { 174 | next_event = Some(ev); 175 | } 176 | }, 177 | ev => { 178 | next_event = Some(ev); 179 | } 180 | } 181 | } 182 | } 183 | 184 | let _ = event_tx.send(Event::Input(InputEvent::KeyBatch(buf))); 185 | if let Some(ev) = next_event { 186 | let _ = event_tx.send(convert_event(ev)); 187 | } 188 | } 189 | _ => { 190 | let _ = event_tx.send(convert_event(ev)); 191 | } 192 | } 193 | } 194 | } 195 | } 196 | }) 197 | } 198 | 199 | pub struct Drawer<'a> { 200 | stdout: Stdout, 201 | // Keep the terminal reference so that we don't write into stdout after 202 | // it has been dropped. 203 | _terminal: &'a Terminal, 204 | } 205 | 206 | impl<'a> Drawer<'a> { 207 | pub fn before_drawing(&mut self) { 208 | // Hide the cursor to prevent flickering. 209 | queue!( 210 | self.stdout, 211 | SynchronizedOutput::Begin, 212 | cursor::Hide, 213 | SetAttribute(Attribute::Reset), 214 | MoveTo(0, 0), 215 | ) 216 | .ok(); 217 | } 218 | 219 | pub fn draw(&mut self, op: &DrawOp) { 220 | match op { 221 | DrawOp::MoveTo { y, x } => { 222 | queue!(self.stdout, MoveTo(*x as u16, *y as u16)).ok(); 223 | } 224 | DrawOp::Grapheme(s) => { 225 | queue!(self.stdout, Print(s)).ok(); 226 | } 227 | DrawOp::FgColor(color) => { 228 | queue!(self.stdout, SetForegroundColor(*color)).ok(); 229 | } 230 | DrawOp::BgColor(color) => { 231 | queue!(self.stdout, SetBackgroundColor(*color)).ok(); 232 | } 233 | DrawOp::Bold => { 234 | queue!(self.stdout, SetAttribute(Attribute::Bold)).ok(); 235 | } 236 | DrawOp::NoBold => { 237 | queue!(self.stdout, SetAttribute(Attribute::NormalIntensity)).ok(); 238 | } 239 | DrawOp::Invert => { 240 | queue!(self.stdout, SetAttribute(Attribute::Reverse)).ok(); 241 | } 242 | DrawOp::NoInvert => { 243 | queue!(self.stdout, SetAttribute(Attribute::NoReverse)).ok(); 244 | } 245 | DrawOp::Underline => { 246 | queue!(self.stdout, SetAttribute(Attribute::Underlined)).ok(); 247 | } 248 | DrawOp::NoUnderline => { 249 | queue!(self.stdout, SetAttribute(Attribute::NoUnderline)).ok(); 250 | } 251 | } 252 | } 253 | 254 | pub fn flush(&mut self, cursor_pos: Option<(usize, usize)>) { 255 | if let Some((y, x)) = cursor_pos { 256 | queue!(self.stdout, MoveTo(x as u16, y as u16), cursor::Show).ok(); 257 | } 258 | queue!(self.stdout, SynchronizedOutput::End).ok(); 259 | self.stdout.flush().ok(); 260 | } 261 | } 262 | 263 | impl<'a> Drop for Drawer<'a> { 264 | fn drop(&mut self) { 265 | self.stdout.flush().ok(); 266 | } 267 | } 268 | 269 | /// An terminal extension which allows applying multiple changes in the terminal 270 | /// at once not to show intermediate results. 271 | /// 272 | /// There're two specifications for this purpose and we support both of them: 273 | /// 274 | /// - iTerm2 / Alacritty: 275 | /// - Contour: 276 | pub enum SynchronizedOutput { 277 | Begin, 278 | End, 279 | } 280 | 281 | impl crossterm::Command for SynchronizedOutput { 282 | fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { 283 | // FIXME: 284 | write!(f, "") 285 | 286 | // let (param_2026, iterm2_op) = match self { 287 | // SynchronizedOutput::Begin => ('h', '1'), 288 | // SynchronizedOutput::End => ('l', '2'), 289 | // }; 290 | 291 | // write!( 292 | // f, 293 | // concat!( 294 | // "\x1b[?2026{}", // CSI ? 2026 param 295 | // "\x1bP={}s\x1b\\" // ESC P = OP s ESC \ 296 | // ), 297 | // param_2026, iterm2_op 298 | // ) 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/editorconfig/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noa_editorconfig" 3 | version = "0.0.0" 4 | authors = ["Seiya Nuta "] 5 | edition = "2021" 6 | 7 | [lib] 8 | path = "lib.rs" 9 | 10 | [dependencies] 11 | log = "0" 12 | 13 | [dev-dependencies] 14 | pretty_assertions = "1" 15 | -------------------------------------------------------------------------------- /src/editorconfig/detect_indent.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::IndentStyle; 4 | 5 | pub fn detect_indent_style(text: &str) -> Option<(IndentStyle, usize)> { 6 | // This map holds the occurrences of differences in the indentation from the previous line. 7 | let mut occurences = HashMap::new(); 8 | let mut prev = None; 9 | for line in text.split('\n') { 10 | let indent_char = match line.chars().next() { 11 | Some('\t') => '\t', 12 | Some(' ') => ' ', 13 | _ => continue, 14 | }; 15 | let indent_count = line.chars().take_while(|&ch| ch == indent_char).count(); 16 | 17 | let current = (indent_char, indent_count); 18 | if prev.is_none() || Some(current) != prev { 19 | let count_diff = match prev { 20 | Some(prev) if current.0 == prev.0 => current.1.abs_diff(prev.1), 21 | _ => current.1, 22 | }; 23 | 24 | if count_diff > 0 { 25 | occurences 26 | .entry((current.0, count_diff)) 27 | .and_modify(|count| *count += 1) 28 | .or_insert(1usize); 29 | } 30 | } 31 | 32 | prev = Some(current); 33 | } 34 | 35 | occurences 36 | .into_iter() 37 | .max_by(|(_, a), (_, b)| a.cmp(b)) 38 | .map(|((indent_char, indent_size), _)| { 39 | let style = match indent_char { 40 | '\t' => IndentStyle::Tab, 41 | _ => IndentStyle::Space, 42 | }; 43 | (style, indent_size) 44 | }) 45 | } 46 | 47 | #[cfg(test)] 48 | mod tests { 49 | use super::*; 50 | 51 | #[test] 52 | fn corner_cases() { 53 | assert_eq!(detect_indent_style(""), None); 54 | assert_eq!(detect_indent_style("int a;"), None); 55 | } 56 | 57 | #[test] 58 | fn guess_tab_indent() { 59 | assert_eq!( 60 | detect_indent_style( 61 | r#" 62 | int main() { 63 | if () { 64 | printf(); 65 | hello(); 66 | world(); 67 | } 68 | } 69 | "#, 70 | ), 71 | Some((IndentStyle::Tab, 1)) 72 | ); 73 | 74 | assert_eq!( 75 | detect_indent_style( 76 | r#" 77 | int main() { 78 | printf("hi"); 79 | } 80 | "#, 81 | ), 82 | Some((IndentStyle::Tab, 1)) 83 | ); 84 | } 85 | 86 | #[test] 87 | fn guess_2_spaces_indent() { 88 | assert_eq!( 89 | detect_indent_style( 90 | r#" 91 | int main() { 92 | if () { 93 | printf(); 94 | hello(); 95 | world(); 96 | } 97 | } 98 | "#, 99 | ), 100 | Some((IndentStyle::Space, 2)) 101 | ); 102 | 103 | assert_eq!( 104 | detect_indent_style( 105 | r#" 106 | int main() { 107 | printf("hi"); 108 | } 109 | "#, 110 | ), 111 | Some((IndentStyle::Space, 2)) 112 | ); 113 | } 114 | 115 | #[test] 116 | fn guess_4_spaces_indent() { 117 | assert_eq!( 118 | detect_indent_style( 119 | r#" 120 | int main() { 121 | if () { 122 | printf(); 123 | hello(); 124 | world(); 125 | } 126 | } 127 | "#, 128 | ), 129 | Some((IndentStyle::Space, 4)) 130 | ); 131 | 132 | assert_eq!( 133 | detect_indent_style( 134 | r#" 135 | int main() { 136 | printf("hi"); 137 | } 138 | "#, 139 | ), 140 | Some((IndentStyle::Space, 4)) 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/languages/.gitignore: -------------------------------------------------------------------------------- 1 | tree_sitter 2 | -------------------------------------------------------------------------------- /src/languages/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noa_languages" 3 | version = "0.0.0" 4 | authors = ["Seiya Nuta "] 5 | edition = "2021" 6 | 7 | [lib] 8 | path = "lib.rs" 9 | 10 | [dependencies] 11 | log = "0" 12 | once_cell = "1" 13 | tree-sitter = "0" 14 | 15 | [build-dependencies] 16 | cc = { version = "*", features = ["parallel"] } 17 | -------------------------------------------------------------------------------- /src/languages/build.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::format_push_string)] 2 | use std::{path::Path, process::Command}; 3 | 4 | #[path = "languages.rs"] 5 | mod languages; 6 | use languages::{TreeSitter, LANGUAGES}; 7 | 8 | const NVIM_TREESITTER_REPO: &str = "https://github.com/nvim-treesitter/nvim-treesitter"; 9 | 10 | fn git_clone_and_pull(repo_url: &str, repo_dir: &Path) { 11 | if !repo_dir.exists() { 12 | println!("Cloning {}", repo_url); 13 | let ok = Command::new("git") 14 | .arg("clone") 15 | .args(&["--depth", "1"]) 16 | .arg(repo_url) 17 | .arg(&repo_dir) 18 | .spawn() 19 | .expect("failed to clone a tree-sitter grammar repo") 20 | .wait() 21 | .expect("failed to wait git-clone(1)") 22 | .success(); 23 | 24 | if !ok { 25 | panic!("failed to clone {}", repo_url); 26 | } 27 | } 28 | } 29 | 30 | fn extract_inherits_in_scm(scm_path: &Path) -> Vec { 31 | let mut inherits = Vec::new(); 32 | for line in std::fs::read_to_string(scm_path).unwrap().lines() { 33 | if line.starts_with("; inherits:") { 34 | let mut parts = line.split("inherits:"); 35 | parts.next(); 36 | for lang in parts.next().unwrap().split(',') { 37 | inherits.push(lang.trim().to_string()); 38 | } 39 | } 40 | } 41 | 42 | inherits 43 | } 44 | 45 | fn get_query_path(lang_name: &str, scm_name: &str) -> String { 46 | format!( 47 | "tree_sitter/nvim_treesitter/queries/{}/{}.scm", 48 | lang_name, scm_name 49 | ) 50 | } 51 | 52 | fn main() { 53 | println!("cargo:rerun-if-changed=build.rs"); 54 | println!("cargo:rerun-if-changed=languages.rs"); 55 | 56 | let nvim_treesitter_dir = Path::new("tree_sitter/nvim_treesitter"); 57 | git_clone_and_pull(NVIM_TREESITTER_REPO, nvim_treesitter_dir); 58 | 59 | let grammars_dir = Path::new("tree_sitter/grammars"); 60 | for lang in LANGUAGES { 61 | println!("Downloading {}", lang.name); 62 | let repo_dir = grammars_dir.join(&lang.name); 63 | 64 | if let Some(TreeSitter { dir, url, sources }) = &lang.tree_sitter { 65 | git_clone_and_pull(url, &repo_dir); 66 | 67 | let mut src_files = Vec::new(); 68 | for file in *sources { 69 | let path = repo_dir.join(dir.unwrap_or(".")).join(file); 70 | src_files.push(path); 71 | } 72 | 73 | println!("Compiling {}", lang.name); 74 | let mut c_files = Vec::new(); 75 | let mut cpp_files = Vec::new(); 76 | 77 | for file in src_files { 78 | match file.extension().unwrap().to_str().unwrap() { 79 | "c" => c_files.push(file), 80 | "cpp" | "cxx" | "cc" => cpp_files.push(file), 81 | _ => panic!("unsupported source file: {}", file.display()), 82 | } 83 | } 84 | 85 | let include_dir = repo_dir.join(dir.unwrap_or(".")).join("src"); 86 | if !c_files.is_empty() { 87 | cc::Build::new() 88 | .include(&include_dir) 89 | .opt_level(3) 90 | .cargo_metadata(true) 91 | .warnings(false) 92 | .files(c_files) 93 | .compile(&format!("tree-sitter-{}-c", lang.name)); 94 | } 95 | 96 | if !cpp_files.is_empty() { 97 | cc::Build::new() 98 | .include(&include_dir) 99 | .opt_level(3) 100 | .cpp(true) 101 | .cargo_metadata(true) 102 | .warnings(false) 103 | .flag("-Wno-switch") // markdown/src/scanner.c 104 | .files(cpp_files) 105 | .compile(&format!("tree-sitter-{}-cpp", lang.name)); 106 | } 107 | } 108 | } 109 | 110 | println!("Generating tree_sitter/mod.rs"); 111 | let mut mod_rs = String::new(); 112 | mod_rs.push_str("#![allow(clippy::all)]\n"); 113 | mod_rs.push_str("pub use tree_sitter::*;\n"); 114 | mod_rs.push_str("extern \"C\" {\n"); 115 | for lang in LANGUAGES { 116 | if lang.tree_sitter.is_some() { 117 | mod_rs.push_str(&format!( 118 | " fn tree_sitter_{}() -> Language;\n", 119 | lang.name 120 | )); 121 | } 122 | } 123 | mod_rs.push_str("}\n\n"); 124 | mod_rs.push_str("pub fn get_tree_sitter_parser(name: &str) -> Option {\n"); 125 | mod_rs.push_str(" match name {\n"); 126 | for lang in LANGUAGES { 127 | if lang.tree_sitter.is_some() { 128 | mod_rs.push_str(&format!( 129 | " \"{}\" => Some(unsafe {{ tree_sitter_{}() }}),\n", 130 | lang.name, lang.name 131 | )); 132 | } 133 | } 134 | mod_rs.push_str(" _ => None\n"); 135 | mod_rs.push_str(" }\n"); 136 | mod_rs.push_str("}\n\n"); 137 | 138 | for scm_name in &["highlights", "indents"] { 139 | mod_rs.push_str(&format!( 140 | "pub fn get_{}_query(name: &str) -> Option<&str> {{\n", 141 | scm_name 142 | )); 143 | mod_rs.push_str(" match name {\n"); 144 | for lang in LANGUAGES { 145 | let scm = get_query_path(lang.name, scm_name); 146 | let scm_path = Path::new(&scm); 147 | if scm_path.exists() { 148 | mod_rs.push_str(&format!(" \"{}\" => Some(concat!(\n", lang.name)); 149 | for inherit in extract_inherits_in_scm(scm_path) { 150 | let scm = get_query_path(&inherit, scm_name); 151 | if !Path::new(&scm).exists() { 152 | panic!( 153 | "{} is referenced from {}, but does not exist", 154 | scm, lang.name 155 | ); 156 | } 157 | mod_rs.push_str(&format!(" include_str!(\"../{}\"),\n", scm)); 158 | } 159 | 160 | mod_rs.push_str(&format!(" include_str!(\"../{}\"),\n", scm)); 161 | mod_rs.push_str(" )),\n"); 162 | } 163 | } 164 | mod_rs.push_str(" _ => None\n"); 165 | mod_rs.push_str(" }\n"); 166 | mod_rs.push_str("}\n"); 167 | } 168 | 169 | std::fs::write("tree_sitter/mod.rs", mod_rs).unwrap(); 170 | } 171 | -------------------------------------------------------------------------------- /src/languages/languages.rs: -------------------------------------------------------------------------------- 1 | //! Language definitions. 2 | //! 3 | //! This file must be independent: it must not depend on any other crate except 4 | //! `std` because it's also used from `build.rs`. 5 | use std::hash::{Hash, Hasher}; 6 | 7 | pub struct TreeSitter { 8 | pub dir: Option<&'static str>, 9 | pub url: &'static str, 10 | pub sources: &'static [&'static str], 11 | } 12 | 13 | pub struct Language { 14 | pub name: &'static str, 15 | pub filenames: &'static [&'static str], 16 | pub extensions: &'static [&'static str], 17 | pub line_comment: Option<&'static str>, 18 | /// `\1` is replaced with the finder query. 19 | pub heutristic_search_regex: Option<&'static str>, 20 | pub tree_sitter: Option, 21 | } 22 | 23 | impl Hash for Language { 24 | fn hash(&self, state: &mut H) { 25 | self.name.hash(state); 26 | } 27 | } 28 | 29 | impl PartialEq for Language { 30 | fn eq(&self, other: &Language) -> bool { 31 | self.name == other.name 32 | } 33 | } 34 | 35 | impl Eq for Language {} 36 | 37 | pub static LANGUAGES: &[Language] = &[ 38 | Language { 39 | name: "plain", 40 | filenames: &[], 41 | extensions: &[], 42 | line_comment: None, 43 | heutristic_search_regex: None, 44 | tree_sitter: None, 45 | }, 46 | Language { 47 | name: "rust", 48 | filenames: &[], 49 | extensions: &["rs"], 50 | line_comment: Some("//"), 51 | heutristic_search_regex: Some(r"(type|struct|enum|trait|static|const|fn)\s\1"), 52 | tree_sitter: Some(TreeSitter { 53 | url: "https://github.com/tree-sitter/tree-sitter-rust", 54 | sources: &["src/parser.c", "src/scanner.c"], 55 | dir: None, 56 | }), 57 | }, 58 | Language { 59 | name: "c", 60 | filenames: &[], 61 | extensions: &["c", "h"], 62 | line_comment: Some("//"), 63 | heutristic_search_regex: None, 64 | tree_sitter: Some(TreeSitter { 65 | url: "https://github.com/tree-sitter/tree-sitter-c", 66 | sources: &["src/parser.c"], 67 | dir: None, 68 | }), 69 | }, 70 | Language { 71 | name: "cpp", 72 | filenames: &[], 73 | extensions: &["cpp", "cxx", "hpp", "hxx"], 74 | line_comment: Some("//"), 75 | heutristic_search_regex: None, 76 | tree_sitter: Some(TreeSitter { 77 | url: "https://github.com/tree-sitter/tree-sitter-cpp", 78 | sources: &["src/parser.c", "src/scanner.cc"], 79 | dir: None, 80 | }), 81 | }, 82 | Language { 83 | name: "javascript", 84 | filenames: &[], 85 | extensions: &["js"], 86 | line_comment: Some("//"), 87 | heutristic_search_regex: None, 88 | tree_sitter: Some(TreeSitter { 89 | url: "https://github.com/tree-sitter/tree-sitter-javascript", 90 | sources: &["src/parser.c", "src/scanner.c"], 91 | dir: None, 92 | }), 93 | }, 94 | Language { 95 | name: "python", 96 | filenames: &[], 97 | extensions: &["py"], 98 | line_comment: Some("#"), 99 | heutristic_search_regex: None, 100 | tree_sitter: Some(TreeSitter { 101 | url: "https://github.com/tree-sitter/tree-sitter-python", 102 | sources: &["src/parser.c", "src/scanner.cc"], 103 | dir: None, 104 | }), 105 | }, 106 | Language { 107 | name: "go", 108 | filenames: &[], 109 | extensions: &["go"], 110 | line_comment: Some("//"), 111 | heutristic_search_regex: None, 112 | tree_sitter: Some(TreeSitter { 113 | url: "https://github.com/tree-sitter/tree-sitter-go", 114 | sources: &["src/parser.c"], 115 | dir: None, 116 | }), 117 | }, 118 | Language { 119 | name: "bash", 120 | filenames: &[], 121 | extensions: &["sh", "bash"], 122 | line_comment: Some("#"), 123 | heutristic_search_regex: None, 124 | tree_sitter: Some(TreeSitter { 125 | url: "https://github.com/tree-sitter/tree-sitter-bash", 126 | sources: &["src/parser.c", "src/scanner.cc"], 127 | dir: None, 128 | }), 129 | }, 130 | Language { 131 | name: "html", 132 | filenames: &[], 133 | extensions: &["html"], 134 | line_comment: None, 135 | heutristic_search_regex: None, 136 | tree_sitter: Some(TreeSitter { 137 | url: "https://github.com/tree-sitter/tree-sitter-html", 138 | sources: &["src/parser.c", "src/scanner.cc"], 139 | dir: None, 140 | }), 141 | }, 142 | Language { 143 | name: "css", 144 | filenames: &[], 145 | extensions: &["css"], 146 | line_comment: None, 147 | heutristic_search_regex: None, 148 | tree_sitter: Some(TreeSitter { 149 | url: "https://github.com/tree-sitter/tree-sitter-css", 150 | sources: &["src/parser.c", "src/scanner.c"], 151 | dir: None, 152 | }), 153 | }, 154 | Language { 155 | name: "scss", 156 | filenames: &[], 157 | extensions: &["scss"], 158 | line_comment: Some("//"), 159 | heutristic_search_regex: None, 160 | tree_sitter: Some(TreeSitter { 161 | url: "https://github.com/serenadeai/tree-sitter-scss", 162 | sources: &["src/parser.c", "src/scanner.c"], 163 | dir: None, 164 | }), 165 | }, 166 | Language { 167 | name: "typescript", 168 | filenames: &[], 169 | extensions: &["ts"], 170 | line_comment: Some("//"), 171 | heutristic_search_regex: None, 172 | tree_sitter: Some(TreeSitter { 173 | url: "https://github.com/tree-sitter/tree-sitter-typescript", 174 | sources: &["src/parser.c", "src/scanner.c"], 175 | dir: Some("typescript"), 176 | }), 177 | }, 178 | Language { 179 | name: "tsx", 180 | filenames: &[], 181 | extensions: &["tsx"], 182 | line_comment: Some("//"), 183 | heutristic_search_regex: None, 184 | tree_sitter: Some(TreeSitter { 185 | url: "https://github.com/tree-sitter/tree-sitter-typescript", 186 | sources: &["src/parser.c", "src/scanner.c"], 187 | dir: Some("tsx"), 188 | }), 189 | }, 190 | Language { 191 | name: "markdown", 192 | filenames: &[], 193 | extensions: &["md"], 194 | line_comment: None, 195 | heutristic_search_regex: None, 196 | tree_sitter: Some(TreeSitter { 197 | url: "https://github.com/MDeiml/tree-sitter-markdown", 198 | sources: &["src/parser.c", "src/scanner.cc"], 199 | dir: None, 200 | }), 201 | }, 202 | Language { 203 | name: "toml", 204 | filenames: &[], 205 | extensions: &["toml"], 206 | line_comment: Some("#"), 207 | heutristic_search_regex: None, 208 | tree_sitter: Some(TreeSitter { 209 | url: "https://github.com/ikatyang/tree-sitter-toml", 210 | sources: &["src/parser.c", "src/scanner.c"], 211 | dir: None, 212 | }), 213 | }, 214 | Language { 215 | name: "json", 216 | filenames: &[], 217 | extensions: &["json"], 218 | line_comment: None, 219 | heutristic_search_regex: None, 220 | tree_sitter: Some(TreeSitter { 221 | url: "https://github.com/tree-sitter/tree-sitter-json", 222 | sources: &["src/parser.c"], 223 | dir: None, 224 | }), 225 | }, 226 | Language { 227 | name: "yaml", 228 | filenames: &[], 229 | extensions: &["yml", "yaml"], 230 | line_comment: Some("#"), 231 | heutristic_search_regex: None, 232 | tree_sitter: Some(TreeSitter { 233 | url: "https://github.com/ikatyang/tree-sitter-yaml", 234 | sources: &["src/parser.c", "src/scanner.cc"], 235 | dir: None, 236 | }), 237 | }, 238 | Language { 239 | name: "make", 240 | filenames: &["Makefile"], 241 | extensions: &["mk", "makefile"], 242 | line_comment: Some("#"), 243 | heutristic_search_regex: None, 244 | tree_sitter: Some(TreeSitter { 245 | url: "https://github.com/alemuller/tree-sitter-make", 246 | sources: &["src/parser.c"], 247 | dir: None, 248 | }), 249 | }, 250 | Language { 251 | name: "dockerfile", 252 | filenames: &["Dockerfile"], 253 | extensions: &["dockerfile"], 254 | line_comment: Some("#"), 255 | heutristic_search_regex: None, 256 | tree_sitter: Some(TreeSitter { 257 | url: "https://github.com/camdencheek/tree-sitter-dockerfile", 258 | sources: &["src/parser.c"], 259 | dir: None, 260 | }), 261 | }, 262 | Language { 263 | name: "regex", 264 | filenames: &[], 265 | extensions: &[], 266 | line_comment: None, 267 | heutristic_search_regex: None, 268 | tree_sitter: Some(TreeSitter { 269 | url: "https://github.com/tree-sitter/tree-sitter-regex", 270 | sources: &["src/parser.c"], 271 | dir: None, 272 | }), 273 | }, 274 | Language { 275 | name: "comment", 276 | filenames: &[], 277 | extensions: &[], 278 | line_comment: None, 279 | heutristic_search_regex: None, 280 | tree_sitter: Some(TreeSitter { 281 | url: "https://github.com/stsewd/tree-sitter-comment", 282 | sources: &["src/parser.c", "src/scanner.c"], 283 | dir: None, 284 | }), 285 | }, 286 | ]; 287 | -------------------------------------------------------------------------------- /src/languages/lib.rs: -------------------------------------------------------------------------------- 1 | #[allow(unused_imports)] 2 | #[macro_use] 3 | extern crate log; 4 | 5 | use once_cell::sync::Lazy; 6 | 7 | pub use crate::languages::*; 8 | use std::{collections::HashMap, ffi::OsString, path::Path}; 9 | 10 | pub mod languages; 11 | pub mod tree_sitter; 12 | 13 | pub fn guess_language(path: &Path) -> Option<&'static Language> { 14 | static FILE_NAMES: Lazy> = Lazy::new(|| { 15 | let mut file_names = HashMap::new(); 16 | for language in LANGUAGES.iter() { 17 | for file_name in language.filenames.iter() { 18 | file_names.insert(file_name.into(), language); 19 | } 20 | } 21 | file_names 22 | }); 23 | 24 | static EXTENSIONS: Lazy> = Lazy::new(|| { 25 | let mut extensions = HashMap::new(); 26 | for language in LANGUAGES.iter() { 27 | for extension in language.extensions.iter() { 28 | extensions.insert(extension.into(), language); 29 | } 30 | } 31 | extensions 32 | }); 33 | 34 | if let Some(file_name) = path.file_name() { 35 | if let Some(language) = FILE_NAMES.get(file_name) { 36 | return Some(language); 37 | } 38 | } 39 | 40 | if let Some(extension) = path.extension() { 41 | if let Some(language) = EXTENSIONS.get(extension) { 42 | return Some(language); 43 | } 44 | } 45 | 46 | None 47 | } 48 | 49 | pub fn get_language_by_name(name: &str) -> Option<&'static Language> { 50 | LANGUAGES.iter().find(|&lang| lang.name == name) 51 | } 52 | -------------------------------------------------------------------------------- /src/noa/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noa" 3 | version = "0.0.0" 4 | authors = ["Seiya Nuta "] 5 | edition = "2021" 6 | 7 | [[bin]] 8 | name = "noa" 9 | path = "main.rs" 10 | 11 | [dependencies] 12 | log = "0" 13 | anyhow = "1" 14 | clap = { version = "3", features = ["derive"] } 15 | tokio = { version = "1", features = ["full", "tracing"] } 16 | futures = "0" 17 | regex = "1" 18 | toml = "0" 19 | serde = { version = "1.0", features = ["derive"] } 20 | once_cell = "1" 21 | arc-swap = "1" 22 | parking_lot = "0" 23 | which = "4" 24 | dirs = "3" 25 | base64 = "0" 26 | 27 | noa_common = { path = "../common" } 28 | noa_buffer = { path = "../buffer" } 29 | noa_languages = { path = "../languages" } 30 | noa_editorconfig = { path = "../editorconfig" } 31 | noa_compositor = { path = "../compositor" } 32 | 33 | [dev-dependencies] 34 | pretty_assertions = "0" 35 | insta = "1" 36 | -------------------------------------------------------------------------------- /src/noa/actions/change_case.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use noa_compositor::compositor::Compositor; 3 | 4 | use crate::editor::Editor; 5 | 6 | use super::Action; 7 | 8 | pub struct ToUpperCase; 9 | 10 | impl Action for ToUpperCase { 11 | fn name(&self) -> &'static str { 12 | "to_upper_case" 13 | } 14 | 15 | fn run(&self, editor: &mut Editor, _compositor: &mut Compositor) -> Result<()> { 16 | editor 17 | .current_document_mut() 18 | .edit_selection_current_word(|text| text.to_ascii_uppercase()); 19 | 20 | Ok(()) 21 | } 22 | } 23 | 24 | pub struct ToLowerCase; 25 | 26 | impl Action for ToLowerCase { 27 | fn name(&self) -> &'static str { 28 | "to_lower_case" 29 | } 30 | 31 | fn run(&self, editor: &mut Editor, _compositor: &mut Compositor) -> Result<()> { 32 | editor 33 | .current_document_mut() 34 | .edit_selection_current_word(|text| text.to_ascii_lowercase()); 35 | 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/noa/actions/goto.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use noa_compositor::compositor::Compositor; 4 | 5 | use crate::editor::Editor; 6 | 7 | use super::Action; 8 | 9 | pub struct GoToLine; 10 | 11 | impl Action for GoToLine { 12 | fn name(&self) -> &'static str { 13 | "goto_line" 14 | } 15 | 16 | fn run(&self, _editor: &mut Editor, _compositor: &mut Compositor) -> Result<()> { 17 | // TODO: 18 | Ok(()) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/noa/actions/linemap.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use noa_compositor::compositor::Compositor; 3 | 4 | use crate::editor::Editor; 5 | 6 | use super::Action; 7 | 8 | pub struct MoveToNextDiff; 9 | 10 | impl Action for MoveToNextDiff { 11 | fn name(&self) -> &'static str { 12 | "move_to_next_diff" 13 | } 14 | 15 | fn run(&self, _editor: &mut Editor, _compositor: &mut Compositor) -> Result<()> { 16 | // TODO: 17 | // let doc = editor.documents.current_mut(); 18 | // let linemap = doc.linemap().load(); 19 | // match linemap.next_diff_line(doc.buffer().main_cursor().moving_position().y) { 20 | // Some(pos) => { 21 | // doc.buffer_mut().move_main_cursor_to_pos(pos); 22 | // } 23 | // None => { 24 | // notify_warn!("no next diff line"); 25 | // } 26 | // } 27 | Ok(()) 28 | } 29 | } 30 | 31 | pub struct MoveToPrevDiff; 32 | 33 | impl Action for MoveToPrevDiff { 34 | fn name(&self) -> &'static str { 35 | "move_to_prev_diff" 36 | } 37 | 38 | fn run(&self, _editor: &mut Editor, _compositor: &mut Compositor) -> Result<()> { 39 | // TODO: 40 | // let doc = editor.documents.current_mut(); 41 | // let linemap = doc.linemap().load(); 42 | // match linemap.prev_diff_line(doc.buffer().main_cursor().moving_position().y) { 43 | // Some(pos) => { 44 | // doc.buffer_mut().move_main_cursor_to_pos(pos); 45 | // } 46 | // None => { 47 | // notify_warn!("no previous diff line"); 48 | // } 49 | // } 50 | Ok(()) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/noa/actions/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{any::Any, collections::HashMap}; 2 | 3 | use anyhow::{anyhow, Result}; 4 | 5 | use noa_compositor::compositor::Compositor; 6 | use once_cell::sync::Lazy; 7 | 8 | use crate::{editor::Editor, notify_error}; 9 | 10 | mod basic_editing; 11 | mod change_case; 12 | mod goto; 13 | mod linemap; 14 | mod scrolling; 15 | 16 | pub const ACTIONS: &[&dyn Action] = &[ 17 | &basic_editing::Save, 18 | &basic_editing::SaveAll, 19 | &basic_editing::OpenFilder, 20 | &basic_editing::BackspaceWord, 21 | &basic_editing::Truncate, 22 | &basic_editing::Delete, 23 | &basic_editing::MoveToTop, 24 | &basic_editing::MoveToBeginningOfLine, 25 | &basic_editing::MoveToEndOfLine, 26 | &basic_editing::MoveToNextWord, 27 | &basic_editing::MoveToPrevWord, 28 | &basic_editing::FindCurrentWord, 29 | &basic_editing::FindCurrentWordGlobally, 30 | &basic_editing::SelectAllCurrentWord, 31 | &basic_editing::SelectPrevWord, 32 | &basic_editing::SelectNextWord, 33 | &basic_editing::MoveLineUp, 34 | &basic_editing::MoveLinesDown, 35 | &basic_editing::AddCursorsUp, 36 | &basic_editing::AddCursorsDown, 37 | &basic_editing::DuplicateLinesUp, 38 | &basic_editing::DuplicateLinesDown, 39 | &basic_editing::SelectUntilBeginningOfLine, 40 | &basic_editing::SelectUntilEndOfLine, 41 | &basic_editing::Cut, 42 | &basic_editing::Copy, 43 | &basic_editing::Paste, 44 | &basic_editing::Undo, 45 | &basic_editing::UndoCursors, 46 | &basic_editing::Redo, 47 | &basic_editing::SoftWrap, 48 | &basic_editing::CommentOut, 49 | &basic_editing::ExpandSelection, 50 | &change_case::ToUpperCase, 51 | &change_case::ToLowerCase, 52 | &linemap::MoveToNextDiff, 53 | &linemap::MoveToPrevDiff, 54 | &scrolling::PageUp, 55 | &scrolling::PageDown, 56 | &goto::GoToLine, 57 | ]; 58 | 59 | pub trait Action: Any + Send + Sync { 60 | fn name(&self) -> &'static str; 61 | fn run(&self, editor: &mut Editor, compositor: &mut Compositor) -> Result<()>; 62 | } 63 | 64 | static ACTION_MAP: Lazy> = Lazy::new(|| { 65 | let mut map = HashMap::new(); 66 | for action in ACTIONS { 67 | map.insert(action.name(), *action); 68 | } 69 | map 70 | }); 71 | 72 | pub fn execute_action( 73 | editor: &mut Editor, 74 | compositor: &mut Compositor, 75 | action: &str, 76 | ) -> Result<()> { 77 | match ACTION_MAP.get(action) { 78 | Some(action) => action.run(editor, compositor), 79 | None => Err(anyhow!("unknown action \"{}\"", action)), 80 | } 81 | } 82 | 83 | pub fn execute_action_or_notify( 84 | editor: &mut Editor, 85 | compositor: &mut Compositor, 86 | action: &str, 87 | ) { 88 | if let Err(err) = execute_action(editor, compositor, action) { 89 | notify_error!("action: {}", err); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/noa/actions/scrolling.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use noa_compositor::compositor::Compositor; 4 | 5 | use crate::editor::Editor; 6 | 7 | use super::Action; 8 | 9 | pub struct PageUp; 10 | 11 | impl Action for PageUp { 12 | fn name(&self) -> &'static str { 13 | "page_up" 14 | } 15 | 16 | fn run(&self, _editor: &mut Editor, _compositor: &mut Compositor) -> Result<()> { 17 | // TODO: 18 | // editor.current_document_mut().scroll_up(); 19 | Ok(()) 20 | } 21 | } 22 | 23 | pub struct PageDown; 24 | 25 | impl Action for PageDown { 26 | fn name(&self) -> &'static str { 27 | "page_down" 28 | } 29 | 30 | fn run(&self, _editor: &mut Editor, _compositor: &mut Compositor) -> Result<()> { 31 | // TODO: 32 | // editor.current_document_mut().scroll_down(); 33 | Ok(()) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/noa/clipboard.rs: -------------------------------------------------------------------------------- 1 | use std::io::prelude::*; 2 | use std::process::{Command, Stdio}; 3 | 4 | use anyhow::Result; 5 | use noa_buffer::buffer::Buffer; 6 | use once_cell::sync::Lazy; 7 | use parking_lot::Mutex; 8 | 9 | use which::which; 10 | 11 | /// Represents data in the clipboard with more detailed contexts. 12 | #[derive(Clone, Debug)] 13 | pub struct ClipboardData { 14 | /// The texts copied into the clipboard. It's more than one if multiple 15 | /// cursors were selected. 16 | pub texts: Vec, 17 | } 18 | 19 | impl ClipboardData { 20 | pub fn from_buffer(buffer: &Buffer) -> ClipboardData { 21 | let mut texts = Vec::new(); 22 | for c in buffer.cursors() { 23 | texts.push(buffer.substr(c.selection())); 24 | } 25 | 26 | ClipboardData { texts } 27 | } 28 | 29 | pub fn equals_to_str(&self, text: &str) -> bool { 30 | self.texts.join("\n") == text 31 | } 32 | 33 | fn write_all(&self, writer: &mut W) -> Result<()> 34 | where 35 | W: Write + Unpin, 36 | { 37 | writer.write_all(self.to_string().as_bytes())?; 38 | Ok(()) 39 | } 40 | } 41 | 42 | impl ToString for ClipboardData { 43 | fn to_string(&self) -> String { 44 | self.texts.join("\n") 45 | } 46 | } 47 | 48 | impl Default for ClipboardData { 49 | fn default() -> Self { 50 | ClipboardData { 51 | texts: vec!["".to_string()], 52 | } 53 | } 54 | } 55 | 56 | #[derive(Clone, Debug)] 57 | pub enum SystemClipboardData { 58 | Ours(ClipboardData), 59 | Others(String), 60 | } 61 | 62 | pub trait ClipboardProvider: Send { 63 | fn copy_from_clipboard(&self) -> Result; 64 | fn copy_into_clipboard(&self, data: ClipboardData) -> Result<()>; 65 | } 66 | 67 | static LAST_OUR_DATA: Lazy> = 68 | Lazy::new(|| Mutex::new(ClipboardData::default())); 69 | 70 | /// Uses Cmd + V (or Ctrl + Shift + V in some Linux's terminal) to paste 71 | /// contents and uses the OSC52 escape sequence to copy contents into the 72 | /// system's clipboard. 73 | /// 74 | /// This is quite useful when you're using noa in remote. 75 | struct Osc52Provider; 76 | 77 | impl Osc52Provider { 78 | fn probe() -> Option { 79 | Some(Osc52Provider) 80 | } 81 | } 82 | 83 | impl ClipboardProvider for Osc52Provider { 84 | // User should not use this: they should use Cmd + V to paste from the 85 | // clipboard instead. 86 | fn copy_from_clipboard(&self) -> Result { 87 | // Use LAST_OUR_DATA as clipboard. 88 | Ok(SystemClipboardData::Ours(LAST_OUR_DATA.lock().clone())) 89 | } 90 | 91 | fn copy_into_clipboard(&self, data: ClipboardData) -> Result<()> { 92 | let mut stdout = std::io::stdout(); 93 | 94 | // OSC52 95 | write!( 96 | stdout, 97 | "\x1b]52;c;{}\x07", 98 | base64::encode(&data.to_string()) 99 | ) 100 | .ok(); 101 | stdout.flush().ok(); 102 | 103 | Ok(()) 104 | } 105 | } 106 | 107 | struct MacOsProvider; 108 | 109 | impl MacOsProvider { 110 | fn probe() -> Option { 111 | if which("pbcopy").is_err() || which("pbcopy").is_err() { 112 | return None; 113 | } 114 | 115 | Some(MacOsProvider) 116 | } 117 | } 118 | 119 | impl ClipboardProvider for MacOsProvider { 120 | fn copy_from_clipboard(&self) -> Result { 121 | let mut child = Command::new("pbpaste").stdout(Stdio::piped()).spawn()?; 122 | 123 | let mut stdout = child.stdout.take().unwrap(); 124 | let mut buf = String::new(); 125 | stdout.read_to_string(&mut buf)?; 126 | 127 | Ok(get_last_clipboard_data(&buf) 128 | .map(SystemClipboardData::Ours) 129 | .unwrap_or_else(|| SystemClipboardData::Others(buf))) 130 | } 131 | 132 | fn copy_into_clipboard(&self, data: ClipboardData) -> Result<()> { 133 | let mut child = Command::new("pbcopy").stdin(Stdio::piped()).spawn()?; 134 | 135 | let mut stdin = child.stdin.take().unwrap(); 136 | data.write_all(&mut stdin)?; 137 | 138 | save_last_clipboard_data(data); 139 | Ok(()) 140 | } 141 | } 142 | 143 | struct FallbackProvider; 144 | 145 | impl ClipboardProvider for FallbackProvider { 146 | fn copy_from_clipboard(&self) -> Result { 147 | // Use LAST_OUR_DATA as clipboard. 148 | Ok(SystemClipboardData::Ours(LAST_OUR_DATA.lock().clone())) 149 | } 150 | 151 | fn copy_into_clipboard(&self, data: ClipboardData) -> Result<()> { 152 | // Use LAST_OUR_DATA as clipboard. 153 | *LAST_OUR_DATA.lock() = data; 154 | Ok(()) 155 | } 156 | } 157 | 158 | pub fn build_provider() -> Box { 159 | if std::env::var("SSH_CONNECTION").is_ok() { 160 | if let Some(provider) = Osc52Provider::probe() { 161 | return Box::new(provider); 162 | } 163 | } 164 | 165 | if cfg!(target_os = "macos") { 166 | if let Some(provider) = MacOsProvider::probe() { 167 | return Box::new(provider); 168 | } 169 | } 170 | 171 | Box::new(FallbackProvider) 172 | } 173 | 174 | /// Returns `ClipboardData` if `text` matches to the lastly pasted data. 175 | pub fn get_last_clipboard_data(text: &str) -> Option { 176 | let last_data = LAST_OUR_DATA.lock(); 177 | if last_data.equals_to_str(text) { 178 | Some(last_data.clone()) 179 | } else { 180 | None 181 | } 182 | } 183 | 184 | fn save_last_clipboard_data(data: ClipboardData) { 185 | *LAST_OUR_DATA.lock() = data; 186 | } 187 | -------------------------------------------------------------------------------- /src/noa/config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use anyhow::{Context, Result}; 4 | use noa_common::warn_once; 5 | use noa_compositor::{ 6 | canvas::{Color, Style}, 7 | terminal::{KeyCode, KeyModifiers}, 8 | }; 9 | 10 | use once_cell::sync::Lazy; 11 | 12 | use serde::Deserialize; 13 | 14 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize)] 15 | #[serde(rename_all = "snake_case")] 16 | pub enum Modifier { 17 | Shift, 18 | Ctrl, 19 | Alt, 20 | } 21 | 22 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize)] 23 | #[serde(rename_all = "snake_case")] 24 | pub enum KeyBindingScope { 25 | Buffer, 26 | } 27 | 28 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 29 | pub struct KeyBinding { 30 | pub scope: KeyBindingScope, 31 | pub modifiers: Vec, 32 | // "enter", "tab", "F1", "up", "down", "right", "left", or "a".."z". 33 | pub key: String, 34 | pub action: String, 35 | } 36 | 37 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 38 | #[serde(rename_all = "snake_case")] 39 | enum ThemeDecoration { 40 | Underline, 41 | Bold, 42 | Inverted, 43 | } 44 | 45 | #[derive(Clone, Debug, PartialEq, Eq, Deserialize)] 46 | struct ThemeItem { 47 | #[serde(default)] 48 | pub fg: String, 49 | #[serde(default)] 50 | pub bg: String, 51 | #[serde(default)] 52 | pub bold: bool, 53 | #[serde(default)] 54 | pub underline: bool, 55 | #[serde(default)] 56 | pub inverted: bool, 57 | } 58 | 59 | #[derive(Clone, Debug, Default, Deserialize)] 60 | struct ConfigFile { 61 | key_bindings: Vec, 62 | theme: HashMap, 63 | colors: HashMap, 64 | } 65 | 66 | fn parse_keybindings( 67 | map: &mut HashMap<(KeyBindingScope, KeyCode, KeyModifiers), KeyBinding>, 68 | bindings: &[KeyBinding], 69 | ) { 70 | for binding in bindings { 71 | let keycode = match binding.key.as_str() { 72 | "enter" => KeyCode::Enter, 73 | "tab" => KeyCode::Tab, 74 | "backtab" => KeyCode::BackTab, 75 | "backspace" => KeyCode::Backspace, 76 | "delete" => KeyCode::Delete, 77 | "up" => KeyCode::Up, 78 | "down" => KeyCode::Down, 79 | "left" => KeyCode::Left, 80 | "right" => KeyCode::Right, 81 | "esc" => KeyCode::Esc, 82 | "home" => KeyCode::Home, 83 | "end" => KeyCode::End, 84 | "pageup" => KeyCode::PageUp, 85 | "pagedown" => KeyCode::PageDown, 86 | "F1" => KeyCode::F(1), 87 | "F2" => KeyCode::F(2), 88 | "F3" => KeyCode::F(3), 89 | "F4" => KeyCode::F(4), 90 | "F5" => KeyCode::F(5), 91 | "F6" => KeyCode::F(6), 92 | "F7" => KeyCode::F(7), 93 | "F8" => KeyCode::F(8), 94 | "F9" => KeyCode::F(9), 95 | "F10" => KeyCode::F(10), 96 | "F11" => KeyCode::F(11), 97 | "F12" => KeyCode::F(12), 98 | s if s.len() == 1 => KeyCode::Char(s.chars().next().unwrap()), 99 | s => { 100 | panic!("invalid key binding: key='{}'", s); 101 | } 102 | }; 103 | 104 | let mut modifiers = KeyModifiers::empty(); 105 | for modifier in &binding.modifiers { 106 | match modifier { 107 | Modifier::Shift => modifiers |= KeyModifiers::SHIFT, 108 | Modifier::Ctrl => modifiers |= KeyModifiers::CONTROL, 109 | Modifier::Alt => modifiers |= KeyModifiers::ALT, 110 | } 111 | } 112 | 113 | map.insert((binding.scope, keycode, modifiers), binding.clone()); 114 | } 115 | } 116 | 117 | static DEFAULT_CONFIG_FILE: Lazy = Lazy::new(|| { 118 | toml::from_str(include_str!("defaults.toml")) 119 | .context("failed to parse defaults.toml") 120 | .unwrap() 121 | }); 122 | 123 | static USER_CONFIG_FILE: Lazy = Lazy::new(|| { 124 | let paths = &[ 125 | dirs::home_dir().unwrap().join(".noa.toml"), 126 | dirs::home_dir().unwrap().join(".config/noa/config.toml"), 127 | ]; 128 | 129 | for path in paths { 130 | if path.exists() { 131 | return toml::from_str(&std::fs::read_to_string(path).unwrap()) 132 | .with_context(|| format!("failed to parse {}", path.display())) 133 | .unwrap(); 134 | } 135 | } 136 | 137 | Default::default() 138 | }); 139 | 140 | static KEY_BINDINGS: Lazy> = 141 | Lazy::new(|| { 142 | let mut map = HashMap::new(); 143 | parse_keybindings(&mut map, &DEFAULT_CONFIG_FILE.key_bindings); 144 | parse_keybindings(&mut map, &USER_CONFIG_FILE.key_bindings); 145 | map 146 | }); 147 | 148 | static THEME: Lazy> = Lazy::new(|| { 149 | let mut styles = HashMap::new(); 150 | let mut color_mappings = HashMap::new(); 151 | 152 | color_mappings.insert("".to_owned(), Color::Reset); 153 | color_mappings.insert("default".to_owned(), Color::Reset); 154 | color_mappings.insert("black".to_owned(), Color::Black); 155 | color_mappings.insert("darkgrey".to_owned(), Color::DarkGrey); 156 | color_mappings.insert("red".to_owned(), Color::Red); 157 | color_mappings.insert("darkred".to_owned(), Color::DarkRed); 158 | color_mappings.insert("green".to_owned(), Color::Green); 159 | color_mappings.insert("darkgreen".to_owned(), Color::DarkGreen); 160 | color_mappings.insert("yellow".to_owned(), Color::Yellow); 161 | color_mappings.insert("darkyellow".to_owned(), Color::DarkYellow); 162 | color_mappings.insert("blue".to_owned(), Color::Blue); 163 | color_mappings.insert("darkblue".to_owned(), Color::DarkBlue); 164 | color_mappings.insert("magenta".to_owned(), Color::Magenta); 165 | color_mappings.insert("darkmagenta".to_owned(), Color::DarkMagenta); 166 | color_mappings.insert("cyan".to_owned(), Color::Cyan); 167 | color_mappings.insert("darkcyan".to_owned(), Color::DarkCyan); 168 | color_mappings.insert("white".to_owned(), Color::White); 169 | color_mappings.insert("grey".to_owned(), Color::Grey); 170 | 171 | parse_theme( 172 | &mut styles, 173 | &mut color_mappings, 174 | &DEFAULT_CONFIG_FILE.theme, 175 | &DEFAULT_CONFIG_FILE.colors, 176 | ) 177 | .expect("failed to parse default theme"); 178 | parse_theme( 179 | &mut styles, 180 | &mut color_mappings, 181 | &USER_CONFIG_FILE.theme, 182 | &USER_CONFIG_FILE.colors, 183 | ) 184 | .expect("failed to parse user theme"); 185 | styles 186 | }); 187 | 188 | pub fn get_keybinding_for( 189 | scope: KeyBindingScope, 190 | keycode: KeyCode, 191 | modifiers: KeyModifiers, 192 | ) -> Option { 193 | KEY_BINDINGS.get(&(scope, keycode, modifiers)).cloned() 194 | } 195 | 196 | fn parse_color(color: &str) -> Result { 197 | let color = match color { 198 | "default" => Color::Reset, 199 | "black" => Color::Black, 200 | "red" => Color::Red, 201 | "green" => Color::Green, 202 | "blue" => Color::Blue, 203 | "yellow" => Color::Yellow, 204 | "cyan" => Color::Cyan, 205 | "white" => Color::White, 206 | "grey" => Color::Grey, 207 | "magenta" => Color::Magenta, 208 | "darkgrey" => Color::DarkGrey, 209 | "darkred" => Color::DarkRed, 210 | "darkgreen" => Color::DarkGreen, 211 | "darkyellow" => Color::DarkYellow, 212 | "darkblue" => Color::DarkBlue, 213 | "darkmagenta" => Color::DarkMagenta, 214 | rgb if rgb.starts_with('#') && rgb.len() == 7 => { 215 | let r = u8::from_str_radix(&rgb[1..3], 16) 216 | .with_context(|| format!("failed to parse rgb: {}", rgb))?; 217 | let g = u8::from_str_radix(&rgb[3..5], 16) 218 | .with_context(|| format!("failed to parse rgb: {}", rgb))?; 219 | let b = u8::from_str_radix(&rgb[5..7], 16) 220 | .with_context(|| format!("failed to parse rgb: {}", rgb))?; 221 | Color::Rgb { r, g, b } 222 | } 223 | _ => return Err(anyhow::anyhow!("invalid color: {}", color)), 224 | }; 225 | 226 | Ok(color) 227 | } 228 | 229 | fn parse_theme( 230 | styles: &mut HashMap, 231 | color_mappings: &mut HashMap, 232 | theme: &HashMap, 233 | colors: &HashMap, 234 | ) -> Result<()> { 235 | for (name, color) in colors { 236 | color_mappings.insert(name.to_string(), parse_color(color)?); 237 | } 238 | 239 | for (key, value) in theme { 240 | let fg = color_mappings 241 | .get(&value.fg) 242 | .copied() 243 | .with_context(|| format!("failed to find color \"{}\"", value.fg))?; 244 | let bg = color_mappings 245 | .get(&value.bg) 246 | .copied() 247 | .with_context(|| format!("failed to find color \"{}\"", value.bg))?; 248 | 249 | styles.insert( 250 | key.to_owned(), 251 | Style { 252 | fg, 253 | bg, 254 | bold: value.bold, 255 | underline: value.underline, 256 | inverted: value.inverted, 257 | }, 258 | ); 259 | } 260 | 261 | Ok(()) 262 | } 263 | 264 | pub fn theme_for(key: &str) -> Style { 265 | match THEME.get(key) { 266 | Some(style) => *style, 267 | None => { 268 | warn_once!("not defined theme: \"{}\"", key); 269 | Default::default() 270 | } 271 | } 272 | } 273 | 274 | pub fn parse_config_files() { 275 | Lazy::force(&KEY_BINDINGS); 276 | Lazy::force(&THEME); 277 | } 278 | -------------------------------------------------------------------------------- /src/noa/defaults.toml: -------------------------------------------------------------------------------- 1 | # Default values. These can be overridden by the config file ~/.config/noa/noa.toml 2 | # or ~/.noa.toml. 3 | key_bindings = [ 4 | { scope = "buffer", key = "s", modifiers = ["ctrl"], action = "save" }, 5 | { scope = "buffer", key = "f", modifiers = ["ctrl"], action = "open_finder" }, 6 | { scope = "buffer", key = "r", modifiers = ["ctrl"], action = "open_buffer_switcher" }, 7 | { scope = "buffer", key = "w", modifiers = ["ctrl"], action = "backspace_word" }, 8 | { scope = "buffer", key = "k", modifiers = ["ctrl"], action = "truncate" }, 9 | { scope = "buffer", key = "d", modifiers = ["ctrl"], action = "delete" }, 10 | { scope = "buffer", key = "a", modifiers = ["ctrl"], action = "move_to_beginning_of_line" }, 11 | { scope = "buffer", key = "e", modifiers = ["ctrl"], action = "move_to_end_of_line" }, 12 | { scope = "buffer", key = "f", modifiers = ["alt"], action = "move_to_next_word" }, 13 | { scope = "buffer", key = "b", modifiers = ["alt"], action = "move_to_prev_word" }, 14 | { scope = "buffer", key = "h", modifiers = ["ctrl"], action = "find_current_word" }, 15 | { scope = "buffer", key = "h", modifiers = ["alt"], action = "select_all_current_word" }, 16 | { scope = "buffer", key = "g", modifiers = ["ctrl"], action = "find_current_word_globally" }, 17 | { scope = "buffer", key = "up", modifiers = ["ctrl"], action = "move_to_prev_diff" }, 18 | { scope = "buffer", key = "down", modifiers = ["ctrl"], action = "move_to_next_diff" }, 19 | { scope = "buffer", key = "left", modifiers = ["alt", "shift"], action = "select_prev_word" }, 20 | { scope = "buffer", key = "right", modifiers = ["alt", "shift"], action = "select_next_word" }, 21 | { scope = "buffer", key = "up", modifiers = ["ctrl", "alt"], action = "add_cursors_up" }, 22 | { scope = "buffer", key = "down", modifiers = ["ctrl", "alt"], action = "add_cursors_down" }, 23 | { scope = "buffer", key = "up", modifiers = ["alt", "shift"], action = "duplicate_lines_up" }, 24 | { scope = "buffer", key = "down", modifiers = ["alt", "shift"], action = "duplicate_lines_down" }, 25 | { scope = "buffer", key = "up", modifiers = ["alt"], action = "move_lines_up" }, 26 | { scope = "buffer", key = "down", modifiers = ["alt"], action = "move_lines_down" }, 27 | { scope = "buffer", key = "left", modifiers = ["ctrl", "shift"], action = "select_until_beginning_of_line" }, 28 | { scope = "buffer", key = "right", modifiers = ["ctrl", "shift"], action = "select_until_end_of_line" }, 29 | { scope = "buffer", key = "b", modifiers = ["ctrl"], action = "expand_selection" }, 30 | { scope = "buffer", key = "x", modifiers = ["ctrl"], action = "cut" }, 31 | { scope = "buffer", key = "c", modifiers = ["ctrl"], action = "copy" }, 32 | { scope = "buffer", key = "v", modifiers = ["ctrl"], action = "paste" }, 33 | { scope = "buffer", key = "u", modifiers = ["ctrl"], action = "undo" }, 34 | { scope = "buffer", key = "u", modifiers = ["alt"], action = "redo" }, 35 | { scope = "buffer", key = "y", modifiers = ["ctrl"], action = "undo_cursors" }, 36 | { scope = "buffer", key = "n", modifiers = ["ctrl"], action = "comment_out" }, 37 | { scope = "buffer", key = "home", modifiers = [], action = "move_to_top" }, 38 | ] 39 | 40 | [colors] 41 | 42 | [theme] 43 | "buffer.find_match" = { bg = "grey" } 44 | "buffer.current_line" = { bg = "" } 45 | "buffer.line_status" = { fg = "grey" } 46 | "buffer.flash" = { bg = "yellow" } 47 | "buffer.matching_bracket" = { bg = "grey", bold = true } 48 | 49 | "line_status.modified" = { bg = "grey" } 50 | "line_status.added" = { bg = "grey" } 51 | "line_status.deleted" = { bg = "grey" } 52 | 53 | "label" = { bold = true, inverted = true } 54 | "prompt.name" = { bg = "grey", bold = true } 55 | "meta_line.background" = { inverted = true } 56 | 57 | "selector.input" = { fg = "grey" } 58 | "selector.selected" = { fg = "grey" } 59 | 60 | "notification.warn" = { fg = "grey" } 61 | "notification.info" = { fg = "grey" } 62 | "notification.error" = { fg = "grey" } 63 | 64 | "completion.item" = { bg = "grey" } 65 | "completion.selected" = { bg = "grey" } 66 | 67 | "syntax.comment" = { fg = "yellow" } 68 | "syntax.boolean" = { fg = "grey" } 69 | "syntax.conditional" = { fg = "red" } 70 | "syntax.constant" = { fg = "grey" } 71 | "syntax.constant.builtin" = { fg = "grey" } 72 | "syntax.field" = { fg = "grey" } 73 | "syntax.float" = { fg = "grey" } 74 | "syntax.function" = { fg = "green" } 75 | "syntax.function.macro" = { fg = "green" } 76 | "syntax.include" = { fg = "red" } 77 | "syntax.keyword" = { fg = "red" } 78 | "syntax.keyword.function" = { fg = "magenta" } 79 | "syntax.keyword.operator" = { fg = "magenta" } 80 | "syntax.keyword.return" = { fg = "magenta" } 81 | "syntax.label" = { fg = "grey" } 82 | "syntax._name" = { fg = "grey" } 83 | "syntax.namespace" = { fg = "grey" } 84 | "syntax.none" = { fg = "grey" } 85 | "syntax.number" = { fg = "grey" } 86 | "syntax.operator" = { fg = "grey" } 87 | "syntax.parameter" = { fg = "grey" } 88 | "syntax.property" = { fg = "grey" } 89 | "syntax.punctuation.bracket" = { fg = "grey" } 90 | "syntax.punctuation.delimiter" = { fg = "grey" } 91 | "syntax.punctuation.special" = { fg = "grey" } 92 | "syntax.repeat" = { fg = "grey" } 93 | "syntax.string" = { fg = "green" } 94 | "syntax.string.escape" = { fg = "blue" } 95 | "syntax.string.special" = { fg = "blue" } 96 | "syntax.text.emphasis" = { fg = "grey" } 97 | "syntax.text.literal" = { fg = "grey" } 98 | "syntax.text.strong" = { fg = "grey" } 99 | "syntax.text.title" = { fg = "grey" } 100 | "syntax.type" = { fg = "green" } 101 | "syntax.type.builtin" = { fg = "green" } 102 | "syntax.value" = { fg = "grey" } 103 | "syntax.variable" = { fg = "grey" } 104 | "syntax.variable.builtin" = { fg = "grey" } 105 | "syntax._parent" = {} 106 | -------------------------------------------------------------------------------- /src/noa/document.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::ErrorKind, 4 | ops::{Deref, DerefMut}, 5 | path::{Path, PathBuf}, 6 | sync::atomic::{AtomicUsize, Ordering}, 7 | time::SystemTime, 8 | }; 9 | 10 | use anyhow::Result; 11 | use noa_buffer::{buffer::Buffer, cursor::Position, raw_buffer::RawBuffer, scroll::Scroll}; 12 | 13 | use crate::{notify_info, notify_warn}; 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 16 | pub struct DocumentId(usize); 17 | 18 | impl DocumentId { 19 | pub fn alloc() -> Self { 20 | static NEXT_ID: AtomicUsize = AtomicUsize::new(1); 21 | DocumentId(NEXT_ID.fetch_add(1, Ordering::SeqCst)) 22 | } 23 | } 24 | 25 | #[derive(Debug)] 26 | pub enum DocumentKind { 27 | Scratch, 28 | File { path: PathBuf }, 29 | } 30 | 31 | pub struct Document { 32 | pub id: DocumentId, 33 | pub kind: DocumentKind, 34 | pub name: String, 35 | pub buffer: Buffer, 36 | pub saved_buffer: RawBuffer, 37 | last_saved_at: Option, 38 | pub path: Option, 39 | pub backup_path: Option, 40 | pub scroll: Scroll, 41 | } 42 | 43 | impl Document { 44 | pub fn virtual_file(name: &str, initial_content: &str) -> Document { 45 | let mut buffer = Buffer::from_text(initial_content); 46 | buffer.save_undo(); 47 | let saved_buffer = buffer.raw_buffer().clone(); 48 | Document { 49 | name: name.to_string(), 50 | id: DocumentId::alloc(), 51 | kind: DocumentKind::Scratch, 52 | buffer, 53 | saved_buffer, 54 | last_saved_at: None, 55 | path: None, 56 | backup_path: None, 57 | scroll: Scroll::zeroed(), 58 | } 59 | } 60 | 61 | pub async fn open(path: &Path) -> Result { 62 | let file = File::open(path)?; 63 | let buffer = Buffer::from_reader(file)?; 64 | let saved_buffer = buffer.raw_buffer().clone(); 65 | let name = path.file_name().unwrap().to_string_lossy().to_string(); 66 | Ok(Document { 67 | id: DocumentId::alloc(), 68 | name, 69 | kind: DocumentKind::File { 70 | path: path.to_owned(), 71 | }, 72 | buffer, 73 | saved_buffer, 74 | last_saved_at: None, 75 | path: Some(path.to_owned()), 76 | backup_path: None, // TODO: 77 | scroll: Scroll::zeroed(), 78 | }) 79 | } 80 | 81 | pub fn save(&mut self) { 82 | let path = match &self.path { 83 | Some(path) => path, 84 | None => return, 85 | }; 86 | 87 | trace!("saving into a file: {}", path.display()); 88 | let with_sudo = match self.buffer.save_to_file(path) { 89 | Ok(()) => { 90 | if let Some(backup_path) = &self.backup_path { 91 | let _ = std::fs::remove_file(backup_path); 92 | } 93 | 94 | false 95 | } 96 | Err(err) if err.kind() == ErrorKind::PermissionDenied => { 97 | match self.buffer.save_to_file_with_sudo(path) { 98 | Ok(()) => { 99 | if let Some(backup_path) = &self.backup_path { 100 | let _ = std::fs::remove_file(backup_path); 101 | } 102 | 103 | true 104 | } 105 | Err(err) => { 106 | notify_warn!("failed to save: {}", err); 107 | return; 108 | } 109 | } 110 | } 111 | Err(err) => { 112 | notify_warn!("failed to save: {}", err); 113 | return; 114 | } 115 | }; 116 | 117 | self.saved_buffer = self.buffer.raw_buffer().clone(); 118 | 119 | // FIXME: By any chance, the file was modified by another process 120 | // between saving the file and updating the last saved time here. 121 | // 122 | // For now, we just ignore that case. 123 | match std::fs::metadata(path).and_then(|meta| meta.modified()) { 124 | Ok(modified) => { 125 | self.last_saved_at = Some(modified); 126 | } 127 | Err(err) => { 128 | notify_warn!("failed to get last saved time: {}", err); 129 | } 130 | } 131 | 132 | notify_info!( 133 | "written {} lines{}", 134 | self.buffer.num_lines(), 135 | if with_sudo { " w/ sudo" } else { "" } 136 | ); 137 | } 138 | 139 | pub fn is_dirty(&self) -> bool { 140 | let a = self.buffer.raw_buffer(); 141 | let b = &self.saved_buffer; 142 | 143 | a.len_chars() != b.len_chars() && a != b 144 | } 145 | 146 | pub fn scroll_down(&mut self, n: usize, screen_width: usize) { 147 | self.scroll.scroll_down( 148 | &self.buffer, 149 | screen_width, 150 | self.buffer.editorconfig().tab_width, 151 | n, 152 | ); 153 | } 154 | 155 | pub fn scroll_up(&mut self, n: usize, screen_width: usize) { 156 | self.scroll.scroll_up( 157 | &self.buffer, 158 | screen_width, 159 | self.buffer.editorconfig().tab_width, 160 | n, 161 | ); 162 | } 163 | 164 | pub fn adjust_scroll( 165 | &mut self, 166 | virtual_screen_width: usize, 167 | screen_width: usize, 168 | screen_height: usize, 169 | first_visible_pos: Position, 170 | last_visible_pos: Position, 171 | ) { 172 | self.scroll.adjust_scroll( 173 | &self.buffer, 174 | virtual_screen_width, 175 | screen_width, 176 | screen_height, 177 | self.buffer.editorconfig().tab_width, 178 | first_visible_pos, 179 | last_visible_pos, 180 | self.buffer.main_cursor().moving_position(), 181 | ); 182 | } 183 | } 184 | 185 | impl Deref for Document { 186 | type Target = Buffer; 187 | 188 | fn deref(&self) -> &Self::Target { 189 | &self.buffer 190 | } 191 | } 192 | 193 | impl DerefMut for Document { 194 | fn deref_mut(&mut self) -> &mut Self::Target { 195 | &mut self.buffer 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/noa/editor.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | clipboard::{self, ClipboardProvider}, 5 | document::{Document, DocumentId}, 6 | }; 7 | 8 | pub struct Editor { 9 | current_doc: DocumentId, 10 | documents: HashMap, 11 | pub clipboard: Box, 12 | } 13 | 14 | impl Editor { 15 | pub fn new() -> Self { 16 | let mut documents = HashMap::new(); 17 | let scratch_doc = Document::virtual_file("[scratch]", ""); 18 | let scratch_id = scratch_doc.id; 19 | documents.insert(scratch_id, scratch_doc); 20 | 21 | Editor { 22 | documents, 23 | current_doc: scratch_id, 24 | clipboard: clipboard::build_provider(), 25 | } 26 | } 27 | 28 | pub fn add_document(&mut self, mut doc: Document) { 29 | doc.save_undo(); 30 | self.documents.insert(doc.id, doc); 31 | } 32 | 33 | pub fn current_document(&self) -> &Document { 34 | self.documents.get(&self.current_doc).unwrap() 35 | } 36 | 37 | pub fn current_document_mut(&mut self) -> &mut Document { 38 | self.documents.get_mut(&self.current_doc).unwrap() 39 | } 40 | 41 | pub fn switch_document(&mut self, doc_id: DocumentId) { 42 | self.current_document_mut().save_undo(); 43 | self.current_doc = doc_id; 44 | } 45 | 46 | pub fn add_and_switch_document(&mut self, doc: Document) { 47 | let doc_id = doc.id; 48 | self.add_document(doc); 49 | self.switch_document(doc_id); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/noa/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(unused)] // FIXME: 2 | 3 | #[macro_use] 4 | extern crate log; 5 | 6 | #[macro_use] 7 | extern crate noa_common; 8 | 9 | use std::{path::PathBuf, process::Stdio, time::Duration}; 10 | 11 | use clap::Parser; 12 | use editor::Editor; 13 | use noa_common::logger::install_logger; 14 | use noa_compositor::{ 15 | compositor::Compositor, 16 | terminal::{Event, InputEvent, KeyCode, KeyModifiers}, 17 | }; 18 | use tokio::{ 19 | sync::mpsc, 20 | time::{self, Instant}, 21 | }; 22 | use views::{buffer_view::BufferView, metaline_view::MetaLine}; 23 | 24 | mod actions; 25 | mod clipboard; 26 | mod config; 27 | mod document; 28 | mod editor; 29 | mod notification; 30 | mod views; 31 | 32 | pub enum MainloopCommand { 33 | Quit, 34 | ExternalCommand(Box), 35 | } 36 | 37 | const FOREVER: Duration = Duration::from_secs(30 * 24 * 60 * 60 /* (almost) forever */); 38 | const UNDO_TIMEOUT: Duration = Duration::from_millis(500); 39 | 40 | async fn mainloop(mut editor: Editor) { 41 | let mut compositor = Compositor::new(); 42 | let (mainloop_tx, mut mainloop_rx) = mpsc::unbounded_channel(); 43 | compositor.add_frontmost_layer(Box::new(BufferView::new(mainloop_tx.clone()))); 44 | compositor.add_frontmost_layer(Box::new(MetaLine::new())); 45 | 46 | let undo_timeout = time::sleep(FOREVER); 47 | tokio::pin!(undo_timeout); 48 | 'outer: loop { 49 | trace_timing!("render", 5 /* ms */, { 50 | compositor.render(&mut editor); 51 | }); 52 | 53 | let timeout = time::sleep(Duration::from_millis(5)); 54 | tokio::pin!(timeout); 55 | 56 | // Handle all pending events until the timeout is reached. 57 | 'inner: for i in 0.. { 58 | tokio::select! { 59 | biased; 60 | 61 | Some(command) = mainloop_rx.recv() => { 62 | match command { 63 | MainloopCommand::Quit => break 'outer, 64 | MainloopCommand::ExternalCommand(mut cmd) => { 65 | cmd.stdin(Stdio::inherit()) 66 | .stdout(Stdio::piped()) 67 | .stderr(Stdio::inherit()); 68 | 69 | let result = compositor.run_in_cooked_mode(&mut editor, || { 70 | cmd.spawn().and_then(|child| child.wait_with_output()) 71 | }).await; 72 | 73 | match result { 74 | Ok(output) => { 75 | info!("output: {:?}", output); 76 | } 77 | Err(err) => notify_error!("failed to spawn: {}", err), 78 | } 79 | } 80 | } 81 | } 82 | 83 | Some(ev) = compositor.receive_event() => { 84 | trace_timing!("handle_event", 5 /* ms */, { 85 | let prev_buffer = editor.current_document().raw_buffer().clone(); 86 | 87 | compositor.handle_event(&mut editor, ev); 88 | 89 | let doc = editor.current_document(); 90 | if *doc.raw_buffer() != prev_buffer { 91 | undo_timeout.as_mut().reset(Instant::now() + UNDO_TIMEOUT); 92 | } 93 | }); 94 | } 95 | 96 | _ = &mut undo_timeout => { 97 | editor.current_document_mut().save_undo(); 98 | undo_timeout.as_mut().reset(Instant::now() + FOREVER); 99 | } 100 | 101 | // No pending events. 102 | _ = futures::future::ready(()), if i > 0 => { 103 | // Since we've already handled at least one event, if there're no 104 | // pending events, we should break the loop to update the 105 | // terminal contents. 106 | break 'inner; 107 | } 108 | 109 | _ = &mut timeout, if i > 0 => { 110 | // Taking too long to handle events. Break the loop to update the 111 | // terminal contents. 112 | break 'inner; 113 | } 114 | } 115 | } 116 | } 117 | } 118 | 119 | #[derive(Parser, Debug)] 120 | struct Args { 121 | #[clap(name = "FILE", parse(from_os_str))] 122 | files: Vec, 123 | } 124 | 125 | #[tokio::main] 126 | async fn main() { 127 | let args = Args::parse(); 128 | 129 | // TODO: 130 | // warm_up_search_cache(); 131 | 132 | let mut editor = editor::Editor::new(); 133 | 134 | for file in args.files { 135 | let doc = document::Document::open(&file) 136 | .await 137 | .expect("failed to open file"); 138 | editor.add_and_switch_document(doc); 139 | } 140 | 141 | install_logger("main"); 142 | 143 | tokio::spawn(mainloop(editor)).await; 144 | } 145 | -------------------------------------------------------------------------------- /src/noa/notification.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{ 2 | atomic::{AtomicBool, Ordering}, 3 | Arc, 4 | }; 5 | 6 | use arc_swap::ArcSwap; 7 | use once_cell::sync::Lazy; 8 | 9 | #[derive(Debug)] 10 | pub enum Notification { 11 | Info(String), 12 | Warn(String), 13 | Error(String), 14 | } 15 | 16 | impl From for Notification { 17 | fn from(err: anyhow::Error) -> Notification { 18 | Notification::Error(format!("{}", err)) 19 | } 20 | } 21 | 22 | pub struct NotificationManager { 23 | notification: ArcSwap>, 24 | } 25 | 26 | impl NotificationManager { 27 | fn new() -> NotificationManager { 28 | NotificationManager { 29 | notification: ArcSwap::from_pointee(None), 30 | } 31 | } 32 | 33 | pub fn is_empty(&self) -> bool { 34 | self.notification.load().is_none() 35 | } 36 | 37 | pub fn last_notification(&self) -> arc_swap::Guard>> { 38 | self.notification.load() 39 | } 40 | 41 | pub fn clear(&self) { 42 | self.notification.store(Arc::new(None)); 43 | } 44 | 45 | pub fn notify(&self, noti: Notification) { 46 | info!("notification: {:?}", noti); 47 | 48 | if PRINT_TO_STDOUT.load(Ordering::SeqCst) { 49 | match ¬i { 50 | Notification::Error(msg) => eprintln!("{}", msg), 51 | Notification::Warn(msg) => eprintln!("{}", msg), 52 | Notification::Info(msg) => eprintln!("{}", msg), 53 | } 54 | } 55 | 56 | self.notification.store(Arc::new(Some(noti))); 57 | } 58 | } 59 | 60 | #[macro_export] 61 | macro_rules! notify_info { 62 | ($($arg:tt)+) => {{ 63 | use $crate::notification::{Notification, notification_manager}; 64 | let noti = Notification::Info(format!($($arg)+)); 65 | notification_manager().notify(noti); 66 | }} 67 | } 68 | 69 | #[macro_export] 70 | macro_rules! notify_warn { 71 | ($($arg:tt)+) => {{ 72 | use $crate::notification::{Notification, notification_manager}; 73 | let noti = Notification::Warn(format!($($arg)+)); 74 | notification_manager().notify(noti); 75 | }} 76 | } 77 | 78 | #[macro_export] 79 | macro_rules! notify_error { 80 | ($($arg:tt)+) => {{ 81 | use $crate::notification::{Notification, notification_manager}; 82 | let noti = Notification::Error(format!($($arg)+)); 83 | notification_manager().notify(noti); 84 | }} 85 | } 86 | 87 | #[macro_export] 88 | macro_rules! notify_anyhow_error { 89 | ($err:expr) => {{ 90 | use $crate::notification::{notification_manager, Notification}; 91 | let noti = Notification::from($err); 92 | notification_manager().notify(noti); 93 | }}; 94 | } 95 | 96 | static NOTIFICATIONS: Lazy = Lazy::new(NotificationManager::new); 97 | static PRINT_TO_STDOUT: AtomicBool = AtomicBool::new(false); 98 | 99 | pub fn notification_manager() -> &'static Lazy { 100 | &NOTIFICATIONS 101 | } 102 | 103 | pub fn set_stdout_mode(enable: bool) { 104 | PRINT_TO_STDOUT.store(enable, Ordering::SeqCst); 105 | } 106 | -------------------------------------------------------------------------------- /src/noa/views/metaline_view.rs: -------------------------------------------------------------------------------- 1 | use noa_buffer::display_width::DisplayWidth; 2 | use noa_compositor::{ 3 | canvas::CanvasViewMut, 4 | surface::{Layout, RectSize, Surface}, 5 | }; 6 | 7 | use crate::{ 8 | config::theme_for, 9 | editor::Editor, 10 | notification::{notification_manager, Notification}, 11 | }; 12 | 13 | use super::truncate_to_width_suffix; 14 | 15 | pub const META_LINE_HEIGHT: usize = 2; 16 | 17 | pub enum MetaLineMode { 18 | Normal, 19 | Search, 20 | } 21 | 22 | pub struct MetaLine { 23 | mode: MetaLineMode, 24 | clear_notification_after: usize, 25 | } 26 | impl MetaLine { 27 | pub fn new() -> Self { 28 | MetaLine { 29 | mode: MetaLineMode::Normal, 30 | clear_notification_after: 0, 31 | } 32 | } 33 | } 34 | 35 | impl Surface for MetaLine { 36 | type Context = Editor; 37 | 38 | fn name(&self) -> &str { 39 | "meta_line" 40 | } 41 | 42 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { 43 | self 44 | } 45 | 46 | fn is_active(&self, _editor: &mut Editor) -> bool { 47 | true 48 | } 49 | 50 | fn layout(&mut self, _editor: &mut Editor, screen_size: RectSize) -> (Layout, RectSize) { 51 | ( 52 | Layout::Fixed { 53 | y: screen_size.height.saturating_sub(META_LINE_HEIGHT), 54 | x: 0, 55 | }, 56 | RectSize { 57 | height: META_LINE_HEIGHT, 58 | width: screen_size.width, 59 | }, 60 | ) 61 | } 62 | 63 | fn cursor_position(&self, _editor: &mut Editor) -> Option<(usize, usize)> { 64 | None 65 | } 66 | 67 | fn render(&mut self, editor: &mut Editor, canvas: &mut CanvasViewMut<'_>) { 68 | canvas.clear(); 69 | 70 | let doc = editor.current_document(); 71 | // Apply the style. 72 | canvas.apply_style(0, 0, canvas.width(), theme_for("meta_line.background")); 73 | 74 | match self.mode { 75 | MetaLineMode::Search => { 76 | // TODO: 77 | } 78 | MetaLineMode::Normal => { 79 | // Cursor position. 80 | let cursor_pos = doc.main_cursor().moving_position(); 81 | let cursor_col = cursor_pos.x + 1; 82 | let cursor_text = if doc.cursors().len() > 1 { 83 | let num_invisible_cursors = doc 84 | .cursors() 85 | .iter() 86 | .filter(|c| { 87 | let _pos = c.moving_position(); 88 | 89 | // TODO: 90 | // pos < view.first_visible_position() 91 | // || pos > view.last_visible_position() 92 | false 93 | }) 94 | .count(); 95 | if num_invisible_cursors > 0 { 96 | format!( 97 | "{} ({}+{})", 98 | cursor_col, 99 | doc.cursors().len(), 100 | num_invisible_cursors 101 | ) 102 | } else { 103 | format!("{} ({})", cursor_col, doc.cursors().len()) 104 | } 105 | } else { 106 | format!("{}", cursor_col) 107 | }; 108 | 109 | // Is the buffer dirty? 110 | let is_dirty = if doc.is_dirty() { "[+]" } else { "" }; 111 | 112 | let left_text = [is_dirty].join(" "); 113 | let right_text = [cursor_text.as_str()].join(" "); 114 | 115 | // File name. 116 | let filename = truncate_to_width_suffix( 117 | &doc.name, 118 | canvas 119 | .width() 120 | .saturating_sub(left_text.display_width() + right_text.display_width() + 3), 121 | ); 122 | let filename_width = filename.display_width(); 123 | 124 | canvas.write_str( 125 | 0, 126 | canvas 127 | .width() 128 | .saturating_sub(1 + right_text.display_width()), 129 | &right_text, 130 | ); 131 | canvas.write_str(0, 1, filename); 132 | canvas.write_str(0, 1 + filename_width + 1, &left_text); 133 | } 134 | }; 135 | 136 | // Notification. 137 | if let Some(noti) = notification_manager().last_notification().as_ref() { 138 | let (theme_key, text) = match noti { 139 | Notification::Info(message) => ("notification.info", message.as_str()), 140 | Notification::Warn(message) => ("notification.warn", message.as_str()), 141 | Notification::Error(err) => ("notification.error", err.as_str()), 142 | }; 143 | 144 | let message = text.lines().next().unwrap_or(""); 145 | canvas.write_str(1, 1, message); 146 | canvas.apply_style(1, 1, canvas.width(), theme_for(theme_key)); 147 | }; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/noa/views/mod.rs: -------------------------------------------------------------------------------- 1 | use noa_buffer::display_width::DisplayWidth; 2 | 3 | pub mod buffer_view; 4 | pub mod metaline_view; 5 | 6 | pub(super) fn truncate_to_width_suffix(s: &str, width: usize) -> &str { 7 | if s.display_width() <= width { 8 | return s; 9 | } 10 | 11 | let mut prev_substr = None; 12 | for (offset, _) in s.char_indices() { 13 | let substr = &s[s.len() - offset..]; 14 | if substr.display_width() > width { 15 | return prev_substr.unwrap_or(""); 16 | } 17 | prev_substr = Some(substr); 18 | } 19 | 20 | prev_substr.unwrap_or(s) 21 | } 22 | -------------------------------------------------------------------------------- /test.c: -------------------------------------------------------------------------------- 1 | asdasdasd 2 | 12 3 | 4 | 12345 5 | 6 | 7 | // You can check for the existence of subcommands, and if found use their matches just as you would the top level cmd. You can check the value provided by positional arguments, or option arguments ;) 8 | asdasd 9 | 10 | 11 | Hello from noa! 12 | 13 | crossterm::execute!(std::io::stdout(), crossterm::terminal::LeaveAlternateScreen).unwrap(); 14 | 15 | // Print captured stderr. 16 | drop(stderr_hold); 17 | 18 | // Resume panic unwind. 19 | std::panic::resume_unwind(e); 20 | --------------------------------------------------------------------------------