1- use std:: ops:: Deref ;
1+ use std:: { collections :: HashSet , ops:: Deref } ;
22
33use crate :: {
4- history:: SearchQuery , menu_functions:: parse_selection_char, Completer , History , Span ,
5- Suggestion ,
4+ history:: SearchQuery , menu_functions:: parse_selection_char, Completer , History , HistoryItem ,
5+ Result , Span , Suggestion ,
66} ;
77
88const SELECTION_CHAR : char = '!' ;
@@ -15,33 +15,35 @@ pub(crate) struct HistoryCompleter<'menu>(&'menu dyn History);
1515// updating the menu and that must happen in the same thread
1616unsafe impl < ' menu > Send for HistoryCompleter < ' menu > { }
1717
18+ fn search_unique (
19+ completer : & HistoryCompleter ,
20+ line : & str ,
21+ ) -> Result < impl Iterator < Item = HistoryItem > > {
22+ let parsed = parse_selection_char ( line, SELECTION_CHAR ) ;
23+ let values = completer. 0 . search ( SearchQuery :: all_that_contain_rev (
24+ parsed. remainder . to_string ( ) ,
25+ ) ) ?;
26+
27+ let mut seen_matching_command_lines = HashSet :: new ( ) ;
28+ Ok ( values
29+ . into_iter ( )
30+ . filter ( move |value| seen_matching_command_lines. insert ( value. command_line . clone ( ) ) ) )
31+ }
32+
1833impl < ' menu > Completer for HistoryCompleter < ' menu > {
1934 fn complete ( & mut self , line : & str , pos : usize ) -> Vec < Suggestion > {
20- let parsed = parse_selection_char ( line, SELECTION_CHAR ) ;
21- let values = self
22- . 0
23- . search ( SearchQuery :: all_that_contain_rev (
24- parsed. remainder . to_string ( ) ,
25- ) )
26- . expect ( "todo: error handling" ) ;
27-
28- values
29- . into_iter ( )
30- . map ( |value| self . create_suggestion ( line, pos, value. command_line . deref ( ) ) )
31- . collect ( )
35+ match search_unique ( self , line) {
36+ Err ( _) => vec ! [ ] ,
37+ Ok ( search_results) => search_results
38+ . map ( |value| self . create_suggestion ( line, pos, value. command_line . deref ( ) ) )
39+ . collect ( ) ,
40+ }
3241 }
3342
3443 // TODO: Implement `fn partial_complete()`
3544
3645 fn total_completions ( & mut self , line : & str , _pos : usize ) -> usize {
37- let parsed = parse_selection_char ( line, SELECTION_CHAR ) ;
38- let count = self
39- . 0
40- . count ( SearchQuery :: all_that_contain_rev (
41- parsed. remainder . to_string ( ) ,
42- ) )
43- . expect ( "todo: error handling" ) ;
44- count as usize
46+ search_unique ( self , line) . map ( |i| i. count ( ) ) . unwrap_or ( 0 )
4547 }
4648}
4749
@@ -66,3 +68,91 @@ impl<'menu> HistoryCompleter<'menu> {
6668 }
6769 }
6870}
71+
72+ #[ cfg( test) ]
73+ mod tests {
74+ use rstest:: rstest;
75+
76+ use super :: * ;
77+ use crate :: * ;
78+
79+ fn new_history_item ( command_line : & str ) -> HistoryItem {
80+ HistoryItem {
81+ id : None ,
82+ start_timestamp : None ,
83+ command_line : command_line. to_string ( ) ,
84+ session_id : None ,
85+ hostname : None ,
86+ cwd : None ,
87+ duration : None ,
88+ exit_status : None ,
89+ more_info : None ,
90+ }
91+ }
92+
93+ #[ test]
94+ fn duplicates_in_history ( ) -> Result < ( ) > {
95+ let command_line_history = vec ! [
96+ "git stash drop" ,
97+ "git add ." ,
98+ "git status | something | else" ,
99+ "git status" ,
100+ "git commit -m 'something'" ,
101+ "ls" ,
102+ "git push" ,
103+ "git status" ,
104+ ] ;
105+ let expected_history_size = command_line_history. len ( ) ;
106+ let mut history = FileBackedHistory :: new ( command_line_history. len ( ) ) ?;
107+ for command_line in command_line_history {
108+ history. save ( new_history_item ( command_line) ) ?;
109+ }
110+ let input = "git s" ;
111+ let mut sut = HistoryCompleter :: new ( & history) ;
112+
113+ let actual = sut. complete ( input, input. len ( ) ) ;
114+ let num_completions = sut. total_completions ( input, input. len ( ) ) ;
115+
116+ assert_eq ! ( actual[ 0 ] . value, "git status" , "it was the last command" ) ;
117+ assert_eq ! (
118+ actual[ 1 ] . value, "git status | something | else" ,
119+ "next match that is not 'git status' again even though it is next in history"
120+ ) ;
121+ assert_eq ! ( actual[ 2 ] . value, "git stash drop" , "last match" ) ;
122+ assert_eq ! ( actual. get( 3 ) , None ) ;
123+ assert_eq ! (
124+ 3 , num_completions,
125+ "total_completions is consistent with complete"
126+ ) ;
127+
128+ assert_eq ! (
129+ history. count_all( ) . expect( "no error" ) as usize ,
130+ expected_history_size,
131+ "History contains duplicates."
132+ ) ;
133+ Ok ( ( ) )
134+ }
135+
136+ #[ rstest]
137+ #[ case( vec![ ] , "any" , vec![ ] ) ]
138+ #[ case( vec![ "old match" , "recent match" , "between" , "recent match" ] , "match" , vec![ "recent match" , "old match" ] ) ]
139+ #[ case( vec![ "a" , "b" , "c" , "a" , "b" , "c" ] , "" , vec![ "c" , "b" , "a" ] ) ]
140+ fn complete_doesnt_return_duplicates (
141+ #[ case] history_items : Vec < & str > ,
142+ #[ case] line : & str ,
143+ #[ case] expected : Vec < & str > ,
144+ ) -> Result < ( ) > {
145+ let mut history = FileBackedHistory :: new ( history_items. len ( ) ) ?;
146+ for history_item in history_items {
147+ history. save ( new_history_item ( history_item) ) ?;
148+ }
149+ let mut sut = HistoryCompleter :: new ( & history) ;
150+ let actual: Vec < String > = sut
151+ . complete ( line, line. len ( ) )
152+ . into_iter ( )
153+ . map ( |suggestion| suggestion. value )
154+ . collect ( ) ;
155+ assert_eq ! ( actual, expected) ;
156+ Ok ( ( ) )
157+ }
158+ }
0 commit comments