1- use std:: path:: PathBuf ;
1+ use std:: { collections :: HashMap , path:: PathBuf } ;
22
33use itertools:: Itertools ;
44use nu_ansi_term:: { Color , Style } ;
@@ -187,6 +187,8 @@ pub struct Reedline {
187187 // Engine Menus
188188 menus : Vec < ReedlineMenu > ,
189189
190+ abbreviations : HashMap < String , String > ,
191+
190192 // Text editor used to open the line buffer for editing
191193 buffer_editor : Option < BufferEditor > ,
192194
@@ -287,6 +289,7 @@ impl Reedline {
287289 mouse_click_mode : MouseClickMode :: default ( ) ,
288290 cwd : None ,
289291 menus : Vec :: new ( ) ,
292+ abbreviations : HashMap :: new ( ) ,
290293 buffer_editor : None ,
291294 cursor_shapes : None ,
292295 bracketed_paste : BracketedPasteGuard :: default ( ) ,
@@ -619,6 +622,14 @@ impl Reedline {
619622 self
620623 }
621624
625+ /// A builder that adds abbreviations to the Reedline engine
626+ ///
627+ /// Overwrites any existing abbreviations with the same key.
628+ pub fn with_abbreviations ( mut self , abbreviations : HashMap < String , String > ) -> Self {
629+ self . abbreviations . extend ( abbreviations) ;
630+ self
631+ }
632+
622633 /// A builder that adds the history item id
623634 #[ must_use]
624635 pub fn with_history_session_id ( mut self , session : Option < HistorySessionId > ) -> Self {
@@ -1278,6 +1289,9 @@ impl Reedline {
12781289 if let Some ( event) = self . parse_bang_command ( ) {
12791290 return self . handle_editor_event ( prompt, event) ;
12801291 }
1292+ if let Some ( event) = self . try_expand_abbreviation_at_cursor ( true ) {
1293+ return self . handle_editor_event ( prompt, event) ;
1294+ }
12811295
12821296 let buffer = self . editor . get_buffer ( ) . to_string ( ) ;
12831297 match self . validator . as_mut ( ) . map ( |v| v. validate ( & buffer) ) {
@@ -1294,13 +1308,21 @@ impl Reedline {
12941308 if let Some ( event) = self . parse_bang_command ( ) {
12951309 return self . handle_editor_event ( prompt, event) ;
12961310 }
1311+ if let Some ( event) = self . try_expand_abbreviation_at_cursor ( true ) {
1312+ return self . handle_editor_event ( prompt, event) ;
1313+ }
1314+
12971315 Ok ( self . submit_buffer ( prompt) ?)
12981316 }
12991317 ReedlineEvent :: SubmitOrNewline => {
13001318 #[ cfg( feature = "bashisms" ) ]
13011319 if let Some ( event) = self . parse_bang_command ( ) {
13021320 return self . handle_editor_event ( prompt, event) ;
13031321 }
1322+ if let Some ( event) = self . try_expand_abbreviation_at_cursor ( true ) {
1323+ return self . handle_editor_event ( prompt, event) ;
1324+ }
1325+
13041326 let cursor_position_in_buffer = self . editor . insertion_point ( ) ;
13051327 let buffer = self . editor . get_buffer ( ) . to_string ( ) ;
13061328 if cursor_position_in_buffer < buffer. len ( ) {
@@ -1322,6 +1344,12 @@ impl Reedline {
13221344 Ok ( EventStatus :: Exits ( Signal :: HostCommand ( host_command) ) )
13231345 }
13241346 ReedlineEvent :: Edit ( commands) => {
1347+ // Check if a space was just inserted and try to expand abbreviations
1348+ if let Some ( EditCommand :: InsertChar ( ' ' ) ) = commands. first ( ) {
1349+ if let Some ( event) = self . try_expand_abbreviation_at_cursor ( false ) {
1350+ return self . handle_editor_event ( prompt, event) ;
1351+ }
1352+ }
13251353 self . run_edit_commands ( & commands) ;
13261354 if let Some ( menu) = self . menus . iter_mut ( ) . find ( |men| men. is_active ( ) ) {
13271355 if self . quick_completions && menu. can_quick_complete ( ) {
@@ -1729,6 +1757,56 @@ impl Reedline {
17291757 }
17301758 }
17311759
1760+ /// Expands an abbreviation at the word before the cursor, if any exists
1761+ ///
1762+ /// Note, expansion does not occur when inside a string.
1763+ fn try_expand_abbreviation_at_cursor ( & mut self , submitted : bool ) -> Option < ReedlineEvent > {
1764+ let buffer = self . editor . get_buffer ( ) ;
1765+ let cursor_position_in_buffer = self . editor . insertion_point ( ) ;
1766+ if cursor_position_in_buffer == 0 {
1767+ return None ;
1768+ }
1769+
1770+ let ( offset, suffix) = match submitted {
1771+ true => ( 0 , "" ) , // expand on <enter>
1772+ false => ( 1 , " " ) , // expand on <space>
1773+ } ;
1774+
1775+ let word_end = cursor_position_in_buffer - offset;
1776+ let prefix = & buffer[ ..word_end] ;
1777+ let word_start = prefix
1778+ . char_indices ( )
1779+ . rev ( )
1780+ . find ( |( _, ch) | ch. is_whitespace ( ) )
1781+ . map ( |( idx, ch) | idx + ch. len_utf8 ( ) )
1782+ . unwrap_or ( 0 ) ; // byte offset of word start
1783+
1784+ if word_start >= word_end {
1785+ // The first char in the buffer is a space or there are consecutive spaces
1786+ return None ;
1787+ }
1788+ if self . editor . is_inside_string_literal ( word_start) {
1789+ return None ;
1790+ }
1791+
1792+ let word = & buffer[ word_start..word_end] ;
1793+ if let Some ( expansion) = self . abbreviations . get ( word) {
1794+ return Some ( ReedlineEvent :: Edit ( vec ! [
1795+ EditCommand :: MoveToPosition {
1796+ position: word_start,
1797+ select: false ,
1798+ } ,
1799+ EditCommand :: MoveToPosition {
1800+ position: word_end,
1801+ select: true ,
1802+ } ,
1803+ EditCommand :: InsertString ( format!( "{}{}" , expansion, suffix) ) ,
1804+ ] ) ) ;
1805+ }
1806+
1807+ None
1808+ }
1809+
17321810 #[ cfg( feature = "bashisms" ) ]
17331811 /// Parses the ! command to replace entries from the history
17341812 fn parse_bang_command ( & mut self ) -> Option < ReedlineEvent > {
@@ -2358,4 +2436,98 @@ mod tests {
23582436 _ => panic ! ( "Expected Signal::ExternalBreak" ) ,
23592437 }
23602438 }
2439+
2440+ fn reedline_with_abbrevs ( abbrevs : & [ ( & str , & str ) ] ) -> Reedline {
2441+ let map = abbrevs
2442+ . iter ( )
2443+ . map ( |( k, v) | ( k. to_string ( ) , v. to_string ( ) ) )
2444+ . collect ( ) ;
2445+ Reedline :: create ( ) . with_abbreviations ( map)
2446+ }
2447+
2448+ fn set_buffer_at_end ( reedline : & mut Reedline , text : & str ) {
2449+ reedline. run_edit_commands ( & [
2450+ EditCommand :: Clear ,
2451+ EditCommand :: InsertString ( text. to_string ( ) ) ,
2452+ ] ) ;
2453+ }
2454+
2455+ #[ test]
2456+ fn abbreviation_expands_on_submit ( ) {
2457+ let mut reedline = reedline_with_abbrevs ( & [ ( "gc" , "git commit" ) ] ) ;
2458+ set_buffer_at_end ( & mut reedline, "gc" ) ;
2459+ let event = reedline. try_expand_abbreviation_at_cursor ( true ) ;
2460+ assert ! ( event. is_some( ) , "expected expansion on submit" ) ;
2461+ reedline. run_edit_commands ( & match event. unwrap ( ) {
2462+ ReedlineEvent :: Edit ( cmds) => cmds,
2463+ _ => panic ! ( "expected Edit event" ) ,
2464+ } ) ;
2465+ assert_eq ! ( reedline. current_buffer_contents( ) , "git commit" ) ;
2466+ }
2467+
2468+ #[ test]
2469+ fn abbreviation_no_match_returns_none ( ) {
2470+ let mut reedline = reedline_with_abbrevs ( & [ ( "gc" , "git commit" ) ] ) ;
2471+ set_buffer_at_end ( & mut reedline, "gx" ) ;
2472+ assert ! ( reedline. try_expand_abbreviation_at_cursor( true ) . is_none( ) ) ;
2473+ }
2474+
2475+ #[ test]
2476+ fn abbreviation_empty_buffer_returns_none ( ) {
2477+ let mut reedline = reedline_with_abbrevs ( & [ ( "gc" , "git commit" ) ] ) ;
2478+ assert ! ( reedline. try_expand_abbreviation_at_cursor( true ) . is_none( ) ) ;
2479+ }
2480+
2481+ #[ test]
2482+ fn abbreviation_expands_last_word_only ( ) {
2483+ let mut reedline = reedline_with_abbrevs ( & [ ( "gc" , "git commit" ) ] ) ;
2484+ set_buffer_at_end ( & mut reedline, "sudo gc" ) ;
2485+ let event = reedline. try_expand_abbreviation_at_cursor ( true ) ;
2486+ assert ! ( event. is_some( ) ) ;
2487+ reedline. run_edit_commands ( & match event. unwrap ( ) {
2488+ ReedlineEvent :: Edit ( cmds) => cmds,
2489+ _ => panic ! ( "expected Edit event" ) ,
2490+ } ) ;
2491+ assert_eq ! ( reedline. current_buffer_contents( ) , "sudo git commit" ) ;
2492+ }
2493+
2494+ #[ test]
2495+ fn abbreviation_no_expansion_inside_double_quoted_string ( ) {
2496+ let mut reedline = reedline_with_abbrevs ( & [ ( "gc" , "git commit" ) ] ) ;
2497+ set_buffer_at_end ( & mut reedline, "\" gc" ) ;
2498+ assert ! (
2499+ reedline. try_expand_abbreviation_at_cursor( true ) . is_none( ) ,
2500+ "must not expand inside an unclosed double-quoted string"
2501+ ) ;
2502+ }
2503+
2504+ #[ test]
2505+ fn abbreviation_no_expansion_inside_single_quoted_string ( ) {
2506+ let mut reedline = reedline_with_abbrevs ( & [ ( "gc" , "git commit" ) ] ) ;
2507+ set_buffer_at_end ( & mut reedline, "'gc" ) ;
2508+ assert ! (
2509+ reedline. try_expand_abbreviation_at_cursor( true ) . is_none( ) ,
2510+ "must not expand inside an unclosed single-quoted string"
2511+ ) ;
2512+ }
2513+
2514+ #[ test]
2515+ fn abbreviation_non_ascii_key_and_expansion ( ) {
2516+ let mut reedline = reedline_with_abbrevs ( & [ ( "café" , "coffee shop" ) ] ) ;
2517+ set_buffer_at_end ( & mut reedline, "café" ) ;
2518+ let event = reedline. try_expand_abbreviation_at_cursor ( true ) ;
2519+ assert ! ( event. is_some( ) , "expected expansion for non-ASCII key" ) ;
2520+ reedline. run_edit_commands ( & match event. unwrap ( ) {
2521+ ReedlineEvent :: Edit ( cmds) => cmds,
2522+ _ => panic ! ( "expected Edit event" ) ,
2523+ } ) ;
2524+ assert_eq ! ( reedline. current_buffer_contents( ) , "coffee shop" ) ;
2525+ }
2526+
2527+ #[ test]
2528+ fn abbreviation_leading_spaces_returns_none ( ) {
2529+ let mut reedline = reedline_with_abbrevs ( & [ ( "gc" , "git commit" ) ] ) ;
2530+ set_buffer_at_end ( & mut reedline, " " ) ;
2531+ assert ! ( reedline. try_expand_abbreviation_at_cursor( true ) . is_none( ) ) ;
2532+ }
23612533}
0 commit comments