Skip to content

Commit ab0ef29

Browse files
authored
fix: extra lines in inline recommendation (#446)
* fix: extra lines in inline recommendation * fix clippy
1 parent 410072e commit ab0ef29

File tree

1 file changed

+90
-22
lines changed
  • crates/figterm/src/inline

1 file changed

+90
-22
lines changed

crates/figterm/src/inline/mod.rs

Lines changed: 90 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ use fig_util::terminal::{
3535
current_terminal_version,
3636
};
3737
use flume::Sender;
38+
use regex::Regex;
3839
use tokio::sync::Mutex;
3940
use 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+
5357
static INLINE_ENABLED: Mutex<bool> = Mutex::const_new(true);
5458

5559
static LAST_RECEIVED: Mutex<Option<SystemTime>> = Mutex::const_new(None);
@@ -60,6 +64,19 @@ static COMPLETION_CACHE: LazyLock<Mutex<CompletionCache>> = LazyLock::new(|| Mut
6064

6165
static 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+
6380
pub 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)]
385400
mod 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\necho world"), "echo hello");
411433
assert_eq!(clean_completion("echo hello \necho 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

Comments
 (0)