| 1 | //! Engine tests. Buffers are written with `|` marking the cursor. |
| 2 | |
| 3 | use super::keys::parse_keys; |
| 4 | use super::{Cx, Effect, History, Mode, View, Vim}; |
| 5 | |
| 6 | struct T { |
| 7 | vim: Vim, |
| 8 | text: String, |
| 9 | cursor: usize, |
| 10 | history: History, |
| 11 | readonly: bool, |
| 12 | dirty: bool, |
| 13 | fx: Vec<Effect>, |
| 14 | } |
| 15 | |
| 16 | impl T { |
| 17 | fn new(marked: &str) -> Self { |
| 18 | let cursor = marked.chars().position(|c| c == '|').unwrap_or(0); |
| 19 | Self { |
| 20 | vim: Vim::default(), |
| 21 | text: marked.replace('|', ""), |
| 22 | cursor, |
| 23 | history: History::default(), |
| 24 | readonly: false, |
| 25 | dirty: false, |
| 26 | fx: Vec::new(), |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | fn keys(&mut self, keys: &str) -> &mut Self { |
| 31 | for k in parse_keys(keys) { |
| 32 | let mut cx = Cx { |
| 33 | text: &mut self.text, |
| 34 | cursor: &mut self.cursor, |
| 35 | history: &mut self.history, |
| 36 | readonly: self.readonly, |
| 37 | dirty: self.dirty, |
| 38 | view: View { top: 0, rows: 10 }, |
| 39 | }; |
| 40 | let fx = self.vim.key(k, &mut cx); |
| 41 | self.fx.extend(fx); |
| 42 | } |
| 43 | self |
| 44 | } |
| 45 | |
| 46 | fn show(&self) -> String { |
| 47 | let mut s: Vec<char> = self.text.chars().collect(); |
| 48 | s.insert(self.cursor.min(s.len()), '|'); |
| 49 | s.into_iter().collect() |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | fn after(start: &str, keys: &str) -> String { |
| 54 | T::new(start).keys(keys).show() |
| 55 | } |
| 56 | |
| 57 | #[test] |
| 58 | fn word_and_line_motions() { |
| 59 | assert_eq!(after("|MOV A,#1 ; c", "w"), "MOV |A,#1 ; c"); |
| 60 | assert_eq!(after("|MOV A,#1 ; c", "ww"), "MOV A|,#1 ; c"); |
| 61 | assert_eq!(after("|MOV A,#1 ; c", "W"), "MOV |A,#1 ; c"); |
| 62 | assert_eq!(after("|MOV A,#1 ; c", "2W"), "MOV A,#1 |; c"); |
| 63 | assert_eq!(after("MOV A,#1 ; |c", "b"), "MOV A,#1 |; c"); |
| 64 | assert_eq!(after("|MOV A,#1", "e"), "MO|V A,#1"); |
| 65 | assert_eq!(after(" MOV |A", "0"), "| MOV A"); |
| 66 | assert_eq!(after(" MOV |A", "^"), " |MOV A"); |
| 67 | assert_eq!(after(" |MOV A", "$"), " MOV |A"); |
| 68 | assert_eq!(after("foo\n\n|bar", "b"), "foo\n|\nbar"); |
| 69 | assert_eq!(after("|foo\n\nbar", "w"), "foo\n|\nbar"); |
| 70 | } |
| 71 | |
| 72 | #[test] |
| 73 | fn vertical_motions_keep_the_column() { |
| 74 | assert_eq!(after("abcd|ef\nab\nabcdef", "j"), "abcdef\na|b\nabcdef"); |
| 75 | assert_eq!(after("abcd|ef\nab\nabcdef", "jj"), "abcdef\nab\nabcd|ef"); |
| 76 | assert_eq!(after("a|bc\nabcdef", "$j"), "abc\nabcde|f"); |
| 77 | assert_eq!(after("a\nb\nc\n|d", "gg"), "|a\nb\nc\nd"); |
| 78 | assert_eq!(after("|a\nb\nc\nd", "G"), "a\nb\nc\n|d"); |
| 79 | assert_eq!(after("|a\nb\nc\nd", "3G"), "a\nb\n|c\nd"); |
| 80 | assert_eq!(after("|a\nb\nc\nd", "2j"), "a\nb\n|c\nd"); |
| 81 | assert_eq!(after("|a\n b", "<CR>"), "a\n |b"); |
| 82 | } |
| 83 | |
| 84 | #[test] |
| 85 | fn find_and_match_motions() { |
| 86 | assert_eq!(after("|a,b,c,d", "f,"), "a|,b,c,d"); |
| 87 | assert_eq!(after("|a,b,c,d", "f,;"), "a,b|,c,d"); |
| 88 | assert_eq!(after("|a,b,c,d", "f,;,"), "a|,b,c,d"); |
| 89 | assert_eq!(after("|a,b,c,d", "2f,"), "a,b|,c,d"); |
| 90 | assert_eq!(after("|a,b,c,d", "t,"), "|a,b,c,d"); |
| 91 | assert_eq!(after("|ab,c", "t,"), "a|b,c"); |
| 92 | assert_eq!(after("a,b,|c", "F,"), "a,b|,c"); |
| 93 | assert_eq!(after("MOV A,|#(1+(2))", "%"), "MOV A,#(1+(2)|)"); |
| 94 | assert_eq!(after("MOV A,#(1+(2)|)", "%"), "MOV A,#|(1+(2))"); |
| 95 | assert_eq!(after("|a\nb\n\nc\nd", "}"), "a\nb\n|\nc\nd"); |
| 96 | } |
| 97 | |
| 98 | #[test] |
| 99 | fn delete_yank_put_lines() { |
| 100 | assert_eq!(after("a\n|b\nc", "dd"), "a\n|c"); |
| 101 | assert_eq!(after("a\nb\n|c", "dd"), "a\n|b"); |
| 102 | assert_eq!(after("|a\nb\nc", "2dd"), "|c"); |
| 103 | assert_eq!(after("|a\nb\nc", "dj"), "|c"); |
| 104 | assert_eq!(after("a\n|b\nc", "dk"), "|c"); |
| 105 | assert_eq!(after("|a\nb", "yyp"), "a\n|a\nb"); |
| 106 | assert_eq!(after("a\n|b", "yyp"), "a\nb\n|b"); |
| 107 | assert_eq!(after("a\n|b", "yyP"), "a\n|b\nb"); |
| 108 | assert_eq!(after("| a\nb", "ddp"), "b\n |a"); |
| 109 | assert_eq!(after("|a\nb\nc", "dG"), "|"); |
| 110 | assert_eq!(after("|para\none\n\nnext", "d}"), "|\nnext"); |
| 111 | } |
| 112 | |
| 113 | #[test] |
| 114 | fn charwise_operators() { |
| 115 | assert_eq!(after("foo |bar\nbaz", "dw"), "foo| \nbaz"); |
| 116 | assert_eq!(after("|foo bar", "dw"), "|bar"); |
| 117 | assert_eq!(after("|foo bar", "d2w"), "|"); |
| 118 | assert_eq!(after("|MOV A", "cwMOVX<Esc>"), "MOV|X A"); |
| 119 | assert_eq!(after("MO|V A", "cw<Esc>"), "M|O A"); |
| 120 | assert_eq!(after("MOV A,B|B", "ciwR7<Esc>"), "MOV A,R|7"); |
| 121 | assert_eq!(after("DB \"hel|lo\", 0", "ci\"bye<Esc>"), "DB \"by|e\", 0"); |
| 122 | assert_eq!(after("DB \"hel|lo\", 0", "da\""), "DB |, 0"); |
| 123 | assert_eq!(after("MOV A,#(1+|2)", "di("), "MOV A,#(|)"); |
| 124 | assert_eq!(after("MOV A,#(1+|2)", "da("), "MOV A,|#"); |
| 125 | assert_eq!(after("a|bc", "x"), "a|c"); |
| 126 | assert_eq!(after("ab|c", "x"), "a|b"); |
| 127 | assert_eq!(after("ab|c", "X"), "a|c"); |
| 128 | assert_eq!(after("a|bcd", "D"), "|a"); |
| 129 | assert_eq!(after("a|bcd", "Cx<Esc>"), "a|x"); |
| 130 | assert_eq!(after("a|bcd", "sX<Esc>"), "a|Xcd"); |
| 131 | assert_eq!(after("|MOV A,B", "dt,"), "|,B"); |
| 132 | assert_eq!(after("|MOV A,B", "df,"), "|B"); |
| 133 | assert_eq!(after("|MOV A,B", "y$P"), "MOV A,|BMOV A,B"); |
| 134 | } |
| 135 | |
| 136 | #[test] |
| 137 | fn indent_join_case_replace() { |
| 138 | assert_eq!(after("|MOV A,B", ">>"), " |MOV A,B"); |
| 139 | assert_eq!(after(" MOV |A,B", "<<"), "|MOV A,B"); |
| 140 | assert_eq!(after("|a\nb", "2>>"), " |a\n b"); |
| 141 | assert_eq!(after("|a\n b", "J"), "a| b"); |
| 142 | assert_eq!(after("|a\nb\nc", "3J"), "a b| c"); |
| 143 | assert_eq!(after("|abc", "~~"), "AB|c"); |
| 144 | assert_eq!(after("|abc", "rx"), "|xbc"); |
| 145 | assert_eq!(after("|abc", "2rx"), "x|xc"); |
| 146 | } |
| 147 | |
| 148 | #[test] |
| 149 | fn undo_redo_and_dot() { |
| 150 | assert_eq!(after("|abc", "xxu"), "|bc"); |
| 151 | assert_eq!(after("|abc", "xxuu"), "|abc"); |
| 152 | assert_eq!(after("|abc", "xxuu<C-r>"), "|bc"); |
| 153 | assert_eq!(after("|abc", "ixy<Esc>u"), "|abc"); |
| 154 | assert_eq!(after("|a b c d", "dw."), "|c d"); |
| 155 | assert_eq!(after("|a b c d", "dw2."), "|d"); |
| 156 | assert_eq!(after("|ab\ncd", "Ax<Esc>j."), "abx\ncd|x"); |
| 157 | assert_eq!(after("|a b", "cwX<Esc>w."), "X |X"); |
| 158 | // Undoing a `.` undoes the whole repeated change. |
| 159 | assert_eq!(after("|ab\ncd", "Axy<Esc>j.u"), "abxy\nc|d"); |
| 160 | } |
| 161 | |
| 162 | #[test] |
| 163 | fn insert_mode_and_counts() { |
| 164 | assert_eq!(after("|", "3ix<Esc>"), "xx|x"); |
| 165 | assert_eq!(after("a|b", "ix<Esc>"), "a|xb"); |
| 166 | assert_eq!(after("a|b", "ax<Esc>"), "ab|x"); |
| 167 | assert_eq!(after(" a|b", "Ix<Esc>"), " |xab"); |
| 168 | assert_eq!(after("a|b", "Ax<Esc>"), "ab|x"); |
| 169 | assert_eq!(after("a|b", "ix<BS>y<Esc>"), "a|yb"); |
| 170 | assert_eq!(after("|", "i<Tab>x<Esc>"), " |x"); |
| 171 | assert_eq!(after("ab|", "a<CR>c<Esc>"), "ab\n|c"); |
| 172 | assert_eq!(after("|a", "2ox<Esc>"), "a\nx\n|x"); |
| 173 | } |
| 174 | |
| 175 | #[test] |
| 176 | fn new_lines_line_up_after_labels() { |
| 177 | assert_eq!( |
| 178 | after("|main: MOV A,#1", "oNOP<Esc>"), |
| 179 | "main: MOV A,#1\n NO|P" |
| 180 | ); |
| 181 | assert_eq!(after("|loop:", "oNOP<Esc>"), "loop:\n NO|P"); |
| 182 | assert_eq!( |
| 183 | after("| MOV A,B", "oX<Esc>"), |
| 184 | " MOV A,B\n |X" |
| 185 | ); |
| 186 | assert_eq!(after("|; comment", "oX<Esc>"), "; comment\n|X"); |
| 187 | assert_eq!(after(" |NOP", "OX<Esc>"), " |X\n NOP"); |
| 188 | } |
| 189 | |
| 190 | #[test] |
| 191 | fn visual_mode() { |
| 192 | let mut t = T::new("|abcdef"); |
| 193 | t.keys("lvlly"); |
| 194 | assert_eq!(t.show(), "a|bcdef"); |
| 195 | assert!(t.fx.contains(&Effect::Clipboard("bcd".into()))); |
| 196 | t.keys("$p"); |
| 197 | assert_eq!(t.show(), "abcdefbc|d"); |
| 198 | assert_eq!(after("a\n|b\nc\nd", "Vjd"), "a\n|d"); |
| 199 | assert_eq!(after("|abcdef", "vld"), "|cdef"); |
| 200 | assert_eq!(after("|abcdef", "vlcX<Esc>"), "|Xcdef"); |
| 201 | assert_eq!(after("|a\nb", "Vj>"), " |a\n b"); |
| 202 | assert_eq!(after("|abc", "vl~"), "|ABc"); |
| 203 | assert_eq!(after("|ab cd", "yiwwviwp"), "ab |ab"); |
| 204 | let mut t = T::new("|abc"); |
| 205 | t.keys("v"); |
| 206 | assert_eq!(t.vim.selection(&t.text, t.cursor), Some((0, 1))); |
| 207 | t.keys("l"); |
| 208 | assert_eq!(t.vim.selection(&t.text, t.cursor), Some((0, 2))); |
| 209 | t.keys("<Esc>"); |
| 210 | assert_eq!(t.vim.mode, Mode::Normal); |
| 211 | } |
| 212 | |
| 213 | #[test] |
| 214 | fn registers_and_clipboard() { |
| 215 | let mut t = T::new("|one\ntwo"); |
| 216 | t.keys("\"ayyj\"byy\"ap"); |
| 217 | assert_eq!(t.show(), "one\ntwo\n|one"); |
| 218 | assert!(t.fx.contains(&Effect::Clipboard("one\n".into()))); |
| 219 | // The black hole register doesn't touch the clipboard or `"`. |
| 220 | let mut t = T::new("|ab"); |
| 221 | t.keys("yl\"_xp"); |
| 222 | assert_eq!(t.show(), "b|a"); |
| 223 | assert_eq!(t.fx, vec![Effect::Clipboard("a".into())]); |
| 224 | // Ctrl+V pastes the system clipboard before the cursor. |
| 225 | let mut t = T::new("a|b"); |
| 226 | t.vim.key( |
| 227 | super::VimKey::Paste("XY".into()), |
| 228 | &mut Cx { |
| 229 | text: &mut t.text, |
| 230 | cursor: &mut t.cursor, |
| 231 | history: &mut t.history, |
| 232 | readonly: false, |
| 233 | dirty: false, |
| 234 | view: View::default(), |
| 235 | }, |
| 236 | ); |
| 237 | assert_eq!(t.show(), "aX|Yb"); |
| 238 | } |
| 239 | |
| 240 | #[test] |
| 241 | fn search() { |
| 242 | assert_eq!(after("|foo bar\nbaz bar", "/bar<CR>"), "foo |bar\nbaz bar"); |
| 243 | assert_eq!(after("|foo bar\nbaz bar", "/bar<CR>n"), "foo bar\nbaz |bar"); |
| 244 | assert_eq!( |
| 245 | after("|foo bar\nbaz bar", "/bar<CR>nn"), |
| 246 | "foo |bar\nbaz bar" |
| 247 | ); |
| 248 | assert_eq!(after("|foo bar\nbaz bar", "/bar<CR>N"), "foo bar\nbaz |bar"); |
| 249 | assert_eq!(after("|foo bar\nbaz bar", "?baz<CR>"), "foo bar\n|baz bar"); |
| 250 | assert_eq!(after("|foo Bar bar", "/bar<CR>"), "foo |Bar bar"); |
| 251 | assert_eq!(after("|foo Bar bar", "/Bar<CR>n"), "foo |Bar bar"); |
| 252 | assert_eq!(after("|loop loops loop", "*"), "loop loops |loop"); |
| 253 | assert_eq!(after("|foo bar", "d/bar<CR>"), "|bar"); |
| 254 | assert_eq!(after("|foo bar", "d/zzz<CR>"), "|foo bar"); |
| 255 | assert_eq!(after("|foo bar", "d/<Esc>x"), "|oo bar"); |
| 256 | let mut t = T::new("|foo bar bar"); |
| 257 | t.keys("/bar<CR>"); |
| 258 | assert_eq!(t.vim.search_ranges(&t.text), vec![(4, 7), (8, 11)]); |
| 259 | t.keys(":noh<CR>"); |
| 260 | assert!(t.vim.search_ranges(&t.text).is_empty()); |
| 261 | let mut t = T::new("|foo"); |
| 262 | t.keys("/zzz<CR>"); |
| 263 | assert_eq!(t.show(), "|foo"); |
| 264 | assert!(t.vim.message.as_ref().is_some_and(|m| m.1)); |
| 265 | } |
| 266 | |
| 267 | #[test] |
| 268 | fn macros() { |
| 269 | assert_eq!(after("|1\n2\n3", "qaA;<Esc>jq@a"), "1;\n2;\n|3"); |
| 270 | assert_eq!(after("|1\n2\n3\n4", "qaA;<Esc>jq2@a"), "1;\n2;\n3;\n|4"); |
| 271 | assert_eq!(after("|1\n2\n3", "qaA;<Esc>jq@a@@"), "1;\n2;\n3|;"); |
| 272 | let mut t = T::new("|x"); |
| 273 | t.keys("qa"); |
| 274 | assert_eq!(t.vim.recording(), Some('a')); |
| 275 | t.keys("q"); |
| 276 | assert_eq!(t.vim.recording(), None); |
| 277 | } |
| 278 | |
| 279 | #[test] |
| 280 | fn ex_commands() { |
| 281 | let mut t = T::new("|a\nb\nc"); |
| 282 | t.keys(":w<CR>:wq<CR>:qa!<CR>:make<CR>:bn<CR>"); |
| 283 | assert_eq!( |
| 284 | t.fx, |
| 285 | vec![ |
| 286 | Effect::Save, |
| 287 | Effect::Save, |
| 288 | Effect::Close { force: false }, |
| 289 | Effect::Quit { force: true }, |
| 290 | Effect::Make, |
| 291 | Effect::NextDoc, |
| 292 | ] |
| 293 | ); |
| 294 | assert_eq!(after("|a\nb\n c", ":3<CR>"), "a\nb\n |c"); |
| 295 | assert_eq!(after("|a\nb\n c", ":$<CR>"), "a\nb\n |c"); |
| 296 | let mut t = T::new("|a"); |
| 297 | t.dirty = true; |
| 298 | t.keys(":q<CR>"); |
| 299 | assert!(t.fx.is_empty()); |
| 300 | assert!( |
| 301 | t.vim |
| 302 | .message |
| 303 | .as_ref() |
| 304 | .is_some_and(|m| m.0.starts_with("E37")) |
| 305 | ); |
| 306 | t.keys(":q!<CR>"); |
| 307 | assert_eq!(t.fx, vec![Effect::Close { force: true }]); |
| 308 | let mut t = T::new("|a"); |
| 309 | t.keys(":wat<CR>"); |
| 310 | assert!( |
| 311 | t.vim |
| 312 | .message |
| 313 | .as_ref() |
| 314 | .is_some_and(|m| m.0.starts_with("E492")) |
| 315 | ); |
| 316 | assert_eq!(t.vim.mode, Mode::Normal); |
| 317 | } |
| 318 | |
| 319 | #[test] |
| 320 | fn read_only_allows_moving_and_yanking() { |
| 321 | let mut t = T::new("|abc\ndef"); |
| 322 | t.readonly = true; |
| 323 | t.keys("x"); |
| 324 | assert_eq!(t.show(), "|abc\ndef"); |
| 325 | assert!(t.vim.message.as_ref().is_some_and(|m| m.1)); |
| 326 | t.keys("ix"); |
| 327 | assert_eq!(t.vim.mode, Mode::Normal); |
| 328 | t.keys("jlyl"); |
| 329 | assert_eq!(t.show(), "abc\nd|ef"); |
| 330 | assert!(t.fx.contains(&Effect::Clipboard("e".into()))); |
| 331 | } |
| 332 | |
| 333 | #[test] |
| 334 | fn goto_definition_and_tabs() { |
| 335 | assert_eq!( |
| 336 | after(" SJMP |loop\nloop: NOP", "gd"), |
| 337 | " SJMP loop\n|loop: NOP" |
| 338 | ); |
| 339 | assert_eq!( |
| 340 | after("MOV A,#|RELOAD\nRELOAD EQU 5", "gd"), |
| 341 | "MOV A,#RELOAD\n|RELOAD EQU 5" |
| 342 | ); |
| 343 | let mut t = T::new("|a"); |
| 344 | t.keys("gtgT"); |
| 345 | assert_eq!(t.fx, vec![Effect::NextDoc, Effect::PrevDoc]); |
| 346 | } |
| 347 | |
| 348 | #[test] |
| 349 | fn escape_and_pending() { |
| 350 | let mut t = T::new("|abc"); |
| 351 | t.keys("2d"); |
| 352 | assert_eq!(t.vim.pending_label(), "2d"); |
| 353 | t.keys("<Esc>"); |
| 354 | assert_eq!(t.vim.pending_label(), ""); |
| 355 | assert_eq!(t.show(), "|abc"); |
| 356 | assert_eq!(after("ab|c", "i<Esc>"), "a|bc"); |
| 357 | assert_eq!(after("|abc", "i<Esc>"), "|abc"); |
| 358 | } |