1- use crossterm:: event:: { KeyCode , KeyEvent } ;
1+ use crossterm:: event:: { KeyCode , KeyEvent , KeyModifiers } ;
22use ratatui:: {
33 Frame ,
44 layout:: { Constraint , Layout , Rect } ,
55 style:: Style ,
6- text:: Line ,
6+ text:: { Line , Span } ,
77 widgets:: Paragraph ,
88} ;
99
@@ -19,6 +19,10 @@ pub struct EditMode {
1919 buffer : String ,
2020}
2121
22+ const MIN_EDIT_AREA_HEIGHT : u16 = 1 ;
23+ const EDIT_LABEL : & str = "编辑: " ;
24+ const EDIT_CONTINUATION_PREFIX : & str = " " ;
25+
2226impl EditMode {
2327 pub fn new ( initial : String , initial_char : Option < char > ) -> Self {
2428 let mut buffer = initial;
@@ -36,6 +40,10 @@ impl Mode for EditMode {
3640
3741 fn handle_key ( & mut self , _view : EditorView < ' _ > , key : KeyEvent ) -> ModeResult {
3842 match key. code {
43+ KeyCode :: Enter if key. modifiers . contains ( KeyModifiers :: SHIFT ) => {
44+ self . buffer . push ( '\n' ) ;
45+ EventResult :: Handled
46+ }
3947 KeyCode :: Enter => EventResult :: Command ( EditorIntent :: CommitEdit ( self . buffer . clone ( ) ) ) ,
4048 KeyCode :: Esc => {
4149 EventResult :: Command ( EditorIntent :: SwitchMode ( Box :: new ( NavigationMode ) ) )
@@ -53,33 +61,12 @@ impl Mode for EditMode {
5361 }
5462
5563 fn render ( & self , frame : & mut Frame , area : Rect , read : EditorReadModel < ' _ > ) -> Rect {
56- use ratatui:: text:: Span ;
57-
64+ let edit_area_height = self . edit_area_height ( area. height ) ;
5865 let [ table_area, edit_area] =
59- Layout :: vertical ( [ Constraint :: Fill ( 1 ) , Constraint :: Length ( 1 ) ] ) . areas ( area) ;
66+ Layout :: vertical ( [ Constraint :: Fill ( 1 ) , Constraint :: Length ( edit_area_height) ] )
67+ . areas ( area) ;
6068
61- let mut spans = vec ! [ Span :: styled(
62- "编辑: " ,
63- Style :: default ( ) . fg( read. theme. accent) ,
64- ) ] ;
65- if self . buffer . is_empty ( ) {
66- spans. push ( Span :: styled (
67- "(空)" ,
68- Style :: default ( ) . fg ( read. theme . text_dim ) ,
69- ) ) ;
70- } else {
71- spans. push ( Span :: styled (
72- self . buffer . as_str ( ) ,
73- Style :: default ( ) . fg ( read. theme . text ) ,
74- ) ) ;
75- let cursor_char = if read. blink_visible { "█" } else { " " } ;
76- spans. push ( Span :: styled (
77- cursor_char,
78- Style :: default ( ) . fg ( read. theme . text ) ,
79- ) ) ;
80- }
81-
82- frame. render_widget ( Paragraph :: new ( Line :: from ( spans) ) , edit_area) ;
69+ frame. render_widget ( Paragraph :: new ( self . render_lines ( read) ) , edit_area) ;
8370 table_area
8471 }
8572
@@ -94,6 +81,9 @@ impl Mode for EditMode {
9481 Span :: styled( "Enter" , Style :: default ( ) . fg( read. theme. accent) ) ,
9582 Span :: styled( " 确认" , Style :: default ( ) . fg( read. theme. text_dim) ) ,
9683 Span :: styled( " " , Style :: default ( ) . fg( read. theme. text_dim) ) ,
84+ Span :: styled( "Shift+Enter" , Style :: default ( ) . fg( read. theme. accent) ) ,
85+ Span :: styled( " 换行" , Style :: default ( ) . fg( read. theme. text_dim) ) ,
86+ Span :: styled( " " , Style :: default ( ) . fg( read. theme. text_dim) ) ,
9787 Span :: styled( "Esc" , Style :: default ( ) . fg( read. theme. accent) ) ,
9888 Span :: styled( " 取消" , Style :: default ( ) . fg( read. theme. text_dim) ) ,
9989 ] ) ) ,
@@ -109,3 +99,157 @@ impl Mode for EditMode {
10999 }
110100 }
111101}
102+
103+ impl EditMode {
104+ fn edit_area_height ( & self , available_height : u16 ) -> u16 {
105+ let needed_height = self
106+ . buffer
107+ . split ( '\n' )
108+ . count ( )
109+ . max ( MIN_EDIT_AREA_HEIGHT as usize ) as u16 ;
110+ needed_height. min ( available_height)
111+ }
112+
113+ fn render_lines ( & self , read : EditorReadModel < ' _ > ) -> Vec < Line < ' static > > {
114+ if self . buffer . is_empty ( ) {
115+ return vec ! [ Line :: from( vec![
116+ Span :: styled( EDIT_LABEL , Style :: default ( ) . fg( read. theme. accent) ) ,
117+ Span :: styled( "(空)" , Style :: default ( ) . fg( read. theme. text_dim) ) ,
118+ ] ) ] ;
119+ }
120+
121+ let cursor_char = if read. blink_visible { "█" } else { " " } ;
122+ let mut lines = Vec :: new ( ) ;
123+ let parts: Vec < & str > = self . buffer . split ( '\n' ) . collect ( ) ;
124+ for ( index, part) in parts. iter ( ) . enumerate ( ) {
125+ let is_first = index == 0 ;
126+ let is_last = index + 1 == parts. len ( ) ;
127+ let mut spans = Vec :: new ( ) ;
128+ if is_first {
129+ spans. push ( Span :: styled (
130+ EDIT_LABEL ,
131+ Style :: default ( ) . fg ( read. theme . accent ) ,
132+ ) ) ;
133+ } else {
134+ spans. push ( Span :: raw ( EDIT_CONTINUATION_PREFIX ) ) ;
135+ }
136+
137+ spans. push ( Span :: styled (
138+ ( * part) . to_string ( ) ,
139+ Style :: default ( ) . fg ( read. theme . text ) ,
140+ ) ) ;
141+ if is_last {
142+ spans. push ( Span :: styled (
143+ cursor_char,
144+ Style :: default ( ) . fg ( read. theme . text ) ,
145+ ) ) ;
146+ }
147+
148+ lines. push ( Line :: from ( spans) ) ;
149+ }
150+
151+ lines
152+ }
153+ }
154+
155+ #[ cfg( test) ]
156+ mod tests {
157+ use crossterm:: event:: { KeyCode , KeyEvent , KeyModifiers } ;
158+ use ratatui:: { Terminal , backend:: TestBackend , layout:: Rect } ;
159+
160+ use super :: { EditMode , MIN_EDIT_AREA_HEIGHT } ;
161+ use crate :: {
162+ screen:: EventResult ,
163+ screen:: editor:: mode:: { EditorIntent , EditorView } ,
164+ screen:: editor:: mode:: { EditorReadModel , Mode } ,
165+ theme:: Theme ,
166+ } ;
167+
168+ #[ test]
169+ fn edit_mode_reserves_a_single_line_edit_area_for_single_line_input ( ) {
170+ let backend = TestBackend :: new ( 40 , 10 ) ;
171+ let mut terminal = Terminal :: new ( backend) . unwrap ( ) ;
172+ let mode = EditMode :: new ( String :: new ( ) , None ) ;
173+ let theme = Theme :: dark ( ) ;
174+ let viewport = super :: super :: viewport:: Viewport :: new ( ) ;
175+
176+ terminal
177+ . draw ( |frame| {
178+ let table_area = mode. render (
179+ frame,
180+ frame. area ( ) ,
181+ EditorReadModel {
182+ theme,
183+ viewport : & viewport,
184+ blink_visible : true ,
185+ selection_stats : None ,
186+ } ,
187+ ) ;
188+
189+ assert_eq ! ( table_area, Rect :: new( 0 , 0 , 40 , 10 - MIN_EDIT_AREA_HEIGHT ) ) ;
190+ } )
191+ . unwrap ( ) ;
192+ }
193+
194+ #[ test]
195+ fn edit_mode_grows_for_multiline_input_and_renders_real_lines ( ) {
196+ let backend = TestBackend :: new ( 40 , 10 ) ;
197+ let mut terminal = Terminal :: new ( backend) . unwrap ( ) ;
198+ let mode = EditMode :: new ( "a\n b" . to_string ( ) , None ) ;
199+ let theme = Theme :: dark ( ) ;
200+ let viewport = super :: super :: viewport:: Viewport :: new ( ) ;
201+
202+ let frame = terminal
203+ . draw ( |frame| {
204+ let table_area = mode. render (
205+ frame,
206+ frame. area ( ) ,
207+ EditorReadModel {
208+ theme,
209+ viewport : & viewport,
210+ blink_visible : true ,
211+ selection_stats : None ,
212+ } ,
213+ ) ;
214+
215+ assert_eq ! ( table_area, Rect :: new( 0 , 0 , 40 , 8 ) ) ;
216+ } )
217+ . unwrap ( ) ;
218+
219+ assert ! (
220+ frame
221+ . buffer
222+ . content( )
223+ . iter( )
224+ . all( |cell| cell. symbol( ) != "↵" )
225+ ) ;
226+ assert_eq ! ( frame. buffer[ ( 6 , 9 ) ] . symbol( ) , "b" ) ;
227+ assert_eq ! ( frame. buffer[ ( 7 , 9 ) ] . symbol( ) , "█" ) ;
228+ }
229+
230+ #[ test]
231+ fn shift_enter_inserts_newline_without_committing ( ) {
232+ let mut mode = EditMode :: new ( "a" . to_string ( ) , None ) ;
233+ let result = mode. handle_key (
234+ EditorView :: new ( None ) ,
235+ KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: SHIFT ) ,
236+ ) ;
237+
238+ assert ! ( matches!( result, EventResult :: Handled ) ) ;
239+ assert_eq ! ( mode. edit_buffer( ) , Some ( "a\n " ) ) ;
240+ }
241+
242+ #[ test]
243+ fn enter_commits_multiline_buffer ( ) {
244+ let mut mode = EditMode :: new ( "a\n b" . to_string ( ) , None ) ;
245+ let result = mode. handle_key (
246+ EditorView :: new ( None ) ,
247+ KeyEvent :: new ( KeyCode :: Enter , KeyModifiers :: empty ( ) ) ,
248+ ) ;
249+
250+ assert ! ( matches!(
251+ result,
252+ EventResult :: Command ( EditorIntent :: CommitEdit ( raw) ) if raw == "a\n b"
253+ ) ) ;
254+ }
255+ }
0 commit comments