@@ -35,6 +35,7 @@ use fig_util::terminal::{
3535 current_terminal_version,
3636} ;
3737use flume:: Sender ;
38+ use regex:: Regex ;
3839use tokio:: sync:: Mutex ;
3940use tracing:: {
4041 error,
@@ -50,6 +51,9 @@ use crate::history::{
5051 HistorySender ,
5152} ;
5253
54+ const HISTORY_COUNT_DEFAULT : usize = 49 ;
55+ const DEBOUNCE_DURATION_DEFAULT : Duration = Duration :: from_millis ( 300 ) ;
56+
5357static INLINE_ENABLED : Mutex < bool > = Mutex :: const_new ( true ) ;
5458
5559static LAST_RECEIVED : Mutex < Option < SystemTime > > = Mutex :: const_new ( None ) ;
@@ -60,6 +64,19 @@ static COMPLETION_CACHE: LazyLock<Mutex<CompletionCache>> = LazyLock::new(|| Mut
6064
6165static TELEMETRY_QUEUE : Mutex < TelemetryQueue > = Mutex :: const_new ( TelemetryQueue :: new ( ) ) ;
6266
67+ static HISTORY_COUNT : LazyLock < usize > = LazyLock :: new ( || {
68+ std:: env:: var ( "Q_INLINE_SHELL_COMPLETION_HISTORY_COUNT" )
69+ . ok ( )
70+ . and_then ( |s| s. parse ( ) . ok ( ) )
71+ . unwrap_or ( HISTORY_COUNT_DEFAULT )
72+ } ) ;
73+ static DEBOUNCE_DURATION : LazyLock < Duration > = LazyLock :: new ( || {
74+ std:: env:: var ( "Q_INLINE_SHELL_COMPLETION_DEBOUNCE_MS" )
75+ . ok ( )
76+ . and_then ( |s| s. parse ( ) . ok ( ) )
77+ . map_or ( DEBOUNCE_DURATION_DEFAULT , Duration :: from_millis)
78+ } ) ;
79+
6380pub async fn on_prompt ( ) {
6481 COMPLETION_CACHE . lock ( ) . await . clear ( ) ;
6582 TELEMETRY_QUEUE . lock ( ) . await . send_all_items ( ) . await ;
@@ -166,13 +183,6 @@ pub async fn handle_request(
166183 }
167184
168185 // debounce requests
169- let debounce_duration = Duration :: from_millis (
170- std:: env:: var ( "Q_INLINE_SHELL_COMPLETION_DEBOUNCE_MS" )
171- . ok ( )
172- . and_then ( |s| s. parse ( ) . ok ( ) )
173- . unwrap_or ( 300 ) ,
174- ) ;
175-
176186 let now = SystemTime :: now ( ) ;
177187 LAST_RECEIVED . lock ( ) . await . replace ( now) ;
178188
@@ -181,7 +191,7 @@ pub async fn handle_request(
181191 } ;
182192
183193 for _ in 0 ..3 {
184- tokio:: time:: sleep ( debounce_duration ) . await ;
194+ tokio:: time:: sleep ( * DEBOUNCE_DURATION ) . await ;
185195 if * LAST_RECEIVED . lock ( ) . await == Some ( now) {
186196 // TODO: determine behavior here, None or Some(unix timestamp)
187197 * LAST_RECEIVED . lock ( ) . await = Some ( SystemTime :: now ( ) ) ;
@@ -206,12 +216,7 @@ pub async fn handle_request(
206216 let ( history_query_tx, history_query_rx) = flume:: bounded ( 1 ) ;
207217 if let Err ( err) = history_sender
208218 . send_async ( history:: HistoryCommand :: Query (
209- HistoryQueryParams {
210- limit : std:: env:: var ( "Q_INLINE_SHELL_COMPLETION_HISTORY_COUNT" )
211- . ok ( )
212- . and_then ( |s| s. parse ( ) . ok ( ) )
213- . unwrap_or ( 50 ) ,
214- } ,
219+ HistoryQueryParams { limit : * HISTORY_COUNT } ,
215220 history_query_tx,
216221 ) )
217222 . await
@@ -247,7 +252,7 @@ pub async fn handle_request(
247252 let response = match client. generate_recommendations ( input) . await {
248253 Err ( err) if err. is_throttling_error ( ) => {
249254 warn ! ( %err, "Too many requests, trying again in 1 second" ) ;
250- tokio:: time:: sleep ( Duration :: from_secs ( 1 ) . saturating_sub ( debounce_duration ) ) . await ;
255+ tokio:: time:: sleep ( Duration :: from_secs ( 1 ) . saturating_sub ( * DEBOUNCE_DURATION ) ) . await ;
251256 continue ;
252257 } ,
253258 other => other,
@@ -257,13 +262,13 @@ pub async fn handle_request(
257262 Ok ( output) => {
258263 let request_id = output. request_id . unwrap_or_default ( ) ;
259264 let session_id = output. session_id . unwrap_or_default ( ) ;
260- let completions = output. recommendations ;
261- let number_of_recommendations = completions . len ( ) ;
265+ let recommendations = output. recommendations ;
266+ let number_of_recommendations = recommendations . len ( ) as i32 ;
262267 let mut completion_cache = COMPLETION_CACHE . lock ( ) . await ;
263268
264- let mut completions = completions
269+ let mut completions = recommendations
265270 . into_iter ( )
266- . map ( |choice| clean_completion ( & choice. content ) . to_owned ( ) )
271+ . map ( |choice| clean_completion ( & choice. content ) . clone ( ) )
267272 . collect :: < Vec < _ > > ( ) ;
268273
269274 // skips the first one which we will recommend, we only cache the rest
@@ -296,7 +301,7 @@ pub async fn handle_request(
296301 async move {
297302 TELEMETRY_QUEUE . lock ( ) . await . items . push ( TelemetryQueueItem {
298303 suggested_chars_len : completion. chars ( ) . count ( ) as i32 ,
299- number_of_recommendations : number_of_recommendations as i32 ,
304+ number_of_recommendations,
300305 suggestion : completion,
301306 timestamp : SystemTime :: now ( ) ,
302307 session_id,
@@ -373,16 +378,33 @@ fn prompt(history: &[CommandInfo], buffer: &str) -> String {
373378 } )
374379}
375380
376- fn clean_completion ( response : & str ) -> & str {
381+ static RE_1 : LazyLock < Regex > = LazyLock :: new ( || Regex :: new ( & format ! ( "{}\\ s+.*" , * HISTORY_COUNT + 1 ) ) . unwrap ( ) ) ;
382+ static RE_2 : LazyLock < Regex > = LazyLock :: new ( || Regex :: new ( & format ! ( "{}\\ s+.*" , * HISTORY_COUNT + 2 ) ) . unwrap ( ) ) ;
383+
384+ fn clean_completion ( response : & str ) -> String {
385+ // only return the first line of the response
377386 let first_line = match response. split_once ( '\n' ) {
378387 Some ( ( left, _) ) => left,
379388 None => response,
380389 } ;
381- first_line. trim_end ( )
390+
391+ // replace parts of the prompt that potentially are the next lines without a newline
392+ let res = RE_1 . replace ( first_line, "" ) ;
393+ let res = RE_2 . replace ( & res, "" ) ;
394+
395+ // trim any remaining whitespace
396+ res. trim_end ( ) . to_owned ( )
382397}
383398
384399#[ cfg( test) ]
385400mod tests {
401+ use fig_settings:: history:: {
402+ HistoryColumn ,
403+ Order ,
404+ OrderBy ,
405+ WhereExpression ,
406+ } ;
407+
386408 use super :: * ;
387409
388410 #[ test]
@@ -410,5 +432,51 @@ mod tests {
410432 assert_eq ! ( clean_completion( "echo hello\n echo world" ) , "echo hello" ) ;
411433 assert_eq ! ( clean_completion( "echo hello \n echo world\n " ) , "echo hello" ) ;
412434 assert_eq ! ( clean_completion( "echo hello " ) , "echo hello" ) ;
435+
436+ // Trim potential excess lines from the model
437+ assert_eq ! ( clean_completion( "cd 50 ls" ) , "cd" ) ;
438+ assert_eq ! (
439+ clean_completion( "git add 50 git commit -m \" initial commit\" " ) ,
440+ "git add"
441+ ) ;
442+ assert_eq ! ( clean_completion( "cd 51 ls" ) , "cd" ) ;
443+ assert_eq ! (
444+ clean_completion( "git add 51 git commit -m \" initial commit\" " ) ,
445+ "git add"
446+ ) ;
447+ }
448+
449+ #[ ignore = "not in CI" ]
450+ #[ tokio:: test]
451+ async fn test_inline_suggestion_prompt ( ) {
452+ let history = fig_settings:: history:: History :: new ( ) ;
453+ let commands = history
454+ . rows (
455+ Some ( WhereExpression :: NotNull ( HistoryColumn :: ExitCode ) ) ,
456+ vec ! [ OrderBy :: new( HistoryColumn :: Id , Order :: Desc ) ] ,
457+ * HISTORY_COUNT ,
458+ 0 ,
459+ )
460+ . unwrap ( ) ;
461+ let prompt = prompt ( & commands, "cd " ) ;
462+
463+ let client = fig_api_client:: Client :: new ( ) . await . unwrap ( ) ;
464+ let out = client
465+ . generate_recommendations ( RecommendationsInput {
466+ file_context : FileContext {
467+ left_file_content : prompt,
468+ right_file_content : "" . into ( ) ,
469+ filename : "history.sh" . into ( ) ,
470+ programming_language : ProgrammingLanguage {
471+ language_name : LanguageName :: Shell ,
472+ } ,
473+ } ,
474+ max_results : 1 ,
475+ next_token : None ,
476+ } )
477+ . await
478+ . unwrap ( ) ;
479+
480+ println ! ( "out: {out:?}" ) ;
413481 }
414482}
0 commit comments