|
| 1 | +// Copyright (c) Microsoft Corporation. |
| 2 | +// Licensed under the MIT License. |
| 3 | + |
| 4 | +use edit::icu; |
| 5 | +use edit::input::Input; |
| 6 | + |
| 7 | +use super::*; |
| 8 | + |
| 9 | +fn outer(layout: &str, classname: &str) -> Rect { |
| 10 | + let node = layout.split_once(&format!("classname: {classname}\r\n")).unwrap().1; |
| 11 | + let values = node.split_once("outer: {").unwrap().1.split_once('}').unwrap().0; |
| 12 | + let values: Vec<_> = values.split(", ").map(|value| value.parse().unwrap()).collect(); |
| 13 | + let [left, top, right, bottom]: [CoordType; 4] = values.try_into().unwrap(); |
| 14 | + Rect { left, top, right, bottom } |
| 15 | +} |
| 16 | + |
| 17 | +fn editor(lines: Option<usize>) -> (sys::Deinit, Tui, State) { |
| 18 | + let sys = sys::init().unwrap(); |
| 19 | + arena::init(32 * MEBI).unwrap(); |
| 20 | + icu::init().unwrap(); |
| 21 | + let mut state = State::new().unwrap(); |
| 22 | + if let Some(lines) = lines { |
| 23 | + let doc = state.documents.add_untitled().unwrap(); |
| 24 | + let mut buffer = doc.buffer.borrow_mut(); |
| 25 | + buffer.set_crlf(false); |
| 26 | + buffer.write_raw(&b"line\n".repeat(lines)); |
| 27 | + } |
| 28 | + (sys, Tui::new().unwrap(), state) |
| 29 | +} |
| 30 | + |
| 31 | +fn frame(tui: &mut Tui, state: &mut State, input: Option<Input<'_>>) { |
| 32 | + crate::draw(tui, input, state); |
| 33 | + for _ in 0..10 { |
| 34 | + if !tui.needs_settling() { |
| 35 | + assert_eq!(state.error_log_count, 0); |
| 36 | + return; |
| 37 | + } |
| 38 | + crate::draw(tui, None, state); |
| 39 | + } |
| 40 | + assert!(!tui.needs_settling(), "Editor layout did not settle"); |
| 41 | + assert_eq!(state.error_log_count, 0); |
| 42 | +} |
| 43 | + |
| 44 | +fn resize(tui: &mut Tui, state: &mut State, width: CoordType, height: CoordType) { |
| 45 | + frame(tui, state, Some(Input::Resize(Size { width, height }))); |
| 46 | +} |
| 47 | + |
| 48 | +#[test] |
| 49 | +fn grid_editor_geometry_tracks_search_documents_and_resize() { |
| 50 | + use StateSearchKind::{Disabled, Hidden, Replace, Search}; |
| 51 | + |
| 52 | + for lines in [None, Some(0), Some(1000)] { |
| 53 | + let (_sys, mut tui, mut state) = editor(lines); |
| 54 | + for width in [1, 2, 80] { |
| 55 | + for search in [Hidden, Disabled, Search, Replace] { |
| 56 | + for height in [24, 8, 7, 6, 5, 4, 3, 2, 1, 80, 32767, 24] { |
| 57 | + state.wants_search.kind = search; |
| 58 | + resize(&mut tui, &mut state, width, height); |
| 59 | + let scratch = arena::scratch_arena(None); |
| 60 | + let layout = tui.debug_layout(&scratch); |
| 61 | + let name = if lines.is_some() { "textarea" } else { "empty" }; |
| 62 | + let editor = outer(&layout, name); |
| 63 | + let search_height = match state.wants_search.kind { |
| 64 | + Search => 2, |
| 65 | + Replace => 3, |
| 66 | + _ => 0, |
| 67 | + }; |
| 68 | + assert_eq!( |
| 69 | + editor.height(), |
| 70 | + (height - 2 - search_height).max(0), |
| 71 | + "width={width} height={height} search_height={search_height}", |
| 72 | + ); |
| 73 | + assert!(editor.width() >= 0); |
| 74 | + assert_eq!(outer(&layout, "statusbar").bottom, height); |
| 75 | + } |
| 76 | + } |
| 77 | + } |
| 78 | + } |
| 79 | +} |
| 80 | + |
| 81 | +#[test] |
| 82 | +fn grid_editor_preserves_focus_through_resize_search_and_menus() { |
| 83 | + for lines in [0, 1000] { |
| 84 | + let (_sys, mut tui, mut state) = editor(Some(lines)); |
| 85 | + for height in [24, 1, 24, 3, 24] { |
| 86 | + resize(&mut tui, &mut state, 80, height); |
| 87 | + } |
| 88 | + if lines > 0 { |
| 89 | + frame(&mut tui, &mut state, Some(Input::Keyboard(kbmod::CTRL | vk::HOME))); |
| 90 | + frame(&mut tui, &mut state, Some(Input::Keyboard(vk::NEXT))); |
| 91 | + let doc = state.documents.active().unwrap(); |
| 92 | + assert_eq!(doc.buffer.borrow().cursor_logical_pos().y, 21); |
| 93 | + frame(&mut tui, &mut state, Some(Input::Keyboard(kbmod::CTRL | vk::END))); |
| 94 | + } |
| 95 | + let mut expected = "line\n".repeat(lines); |
| 96 | + for phase in ["resize", "search", "menu"] { |
| 97 | + if phase == "search" { |
| 98 | + state.wants_search.kind = StateSearchKind::Search; |
| 99 | + state.wants_search.focus = true; |
| 100 | + frame(&mut tui, &mut state, None); |
| 101 | + frame(&mut tui, &mut state, Some(Input::Text("needle"))); |
| 102 | + assert_eq!(state.search_needle, "needle"); |
| 103 | + frame(&mut tui, &mut state, Some(Input::Keyboard(vk::ESCAPE))); |
| 104 | + assert!(state.wants_search.kind == StateSearchKind::Hidden); |
| 105 | + } |
| 106 | + if phase == "menu" { |
| 107 | + frame(&mut tui, &mut state, Some(Input::Keyboard(vk::F10))); |
| 108 | + { |
| 109 | + let scratch = arena::scratch_arena(None); |
| 110 | + let layout = tui.debug_layout(&scratch); |
| 111 | + assert!(layout.contains("classname: flyout\r\n")); |
| 112 | + assert_eq!(outer(&layout, "textarea").height(), 22); |
| 113 | + assert_eq!(outer(&layout, "statusbar").bottom, 24); |
| 114 | + } |
| 115 | + frame(&mut tui, &mut state, Some(Input::Keyboard(vk::ESCAPE))); |
| 116 | + } |
| 117 | + frame(&mut tui, &mut state, Some(Input::Text(phase))); |
| 118 | + expected.push_str(phase); |
| 119 | + let mut text = String::new(); |
| 120 | + state.documents.active().unwrap().buffer.borrow_mut().save_as_string(&mut text); |
| 121 | + let ending = if cfg!(windows) { "" } else { "\n" }; |
| 122 | + assert_eq!(text, format!("{expected}{ending}")); |
| 123 | + let scratch = arena::scratch_arena(None); |
| 124 | + assert!(tui.render(&scratch).contains(phase), "Typed text must be visible"); |
| 125 | + } |
| 126 | + } |
| 127 | +} |
0 commit comments