mirror of
https://github.com/openai/codex.git
synced 2026-11-06 08:31:30 +00:00
Fix skill completion around bound mentions with suffixes (#33237)
## What changed - Use `$`-mention boundary rules when deciding whether a skill mention followed by a suffix is already bound. - Fall back to the editable skill token on the left when the token on the right is a bound mention such as `$bound/path` or `$bound.config`. ## Testing - Add completion-target coverage for path and configuration suffixes. - Add a composer snapshot covering the skill popup fallback for a bound path suffix. GitOrigin-RevId: bf72237dfd56cfe6f590a3fab10c2bfe03b088a8
This commit is contained in:
1 parent
b6f1cfa3f3
commit
b556ce3fec
4 files changed
+65
-5
No files matched your search
@@ -4064,6 +4064,12 @@ fn ends_plaintext_at_mention(bytes: &[u8], index: usize) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ends_plaintext_at_dollar_mention(bytes: &[u8], index: usize) -> bool {
|
||||||
|
bytes
|
||||||
|
.get(index)
|
||||||
|
.is_none_or(|byte| !is_mention_name_char(*byte))
|
||||||
|
}
|
||||||
|
|
||||||
fn starts_plaintext_at_mention(text: &str, index: usize) -> bool {
|
fn starts_plaintext_at_mention(text: &str, index: usize) -> bool {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
return true;
|
return true;
|
||||||
@@ -4114,9 +4120,7 @@ fn find_next_mention_token_range(text: &str, token: &str, from: usize) -> Option
|
|||||||
let ends_plaintext_mention = if sigil == b'@' {
|
let ends_plaintext_mention = if sigil == b'@' {
|
||||||
ends_plaintext_at_mention(bytes, end)
|
ends_plaintext_at_mention(bytes, end)
|
||||||
} else {
|
} else {
|
||||||
bytes
|
ends_plaintext_at_dollar_mention(bytes, end)
|
||||||
.get(end)
|
|
||||||
.is_none_or(|byte| !is_mention_name_char(*byte))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if starts_plaintext_mention && ends_plaintext_mention {
|
if starts_plaintext_mention && ends_plaintext_mention {
|
||||||
@@ -6504,6 +6508,25 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn skill_popup_falls_back_from_bound_skill_with_path_suffix_snapshot() {
|
||||||
|
snapshot_composer_state(
|
||||||
|
"skill_popup_falls_back_from_bound_skill_with_path_suffix",
|
||||||
|
/*enhanced_keys_supported*/ false,
|
||||||
|
|composer| {
|
||||||
|
composer.set_skill_mentions(Some(vec![test_skill_metadata("left")]));
|
||||||
|
composer.set_text_content_with_mention_bindings(
|
||||||
|
"$left $bound/path".to_string(),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
vec![test_skill_binding("bound")],
|
||||||
|
);
|
||||||
|
composer.draft.textarea.set_cursor("$left ".len());
|
||||||
|
composer.sync_popups();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn skill_popup_closes_after_trailing_space_snapshot() {
|
fn skill_popup_closes_after_trailing_space_snapshot() {
|
||||||
snapshot_composer_state(
|
snapshot_composer_state(
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Cursor-neighborhood resolution for sigil-prefixed composer completions.
|
//! Cursor-neighborhood resolution for sigil-prefixed composer completions.
|
||||||
|
|
||||||
|
use super::ends_plaintext_at_dollar_mention;
|
||||||
use super::ends_plaintext_at_mention;
|
use super::ends_plaintext_at_mention;
|
||||||
use super::is_mention_name_char;
|
use super::is_mention_name_char;
|
||||||
use crate::bottom_pane::textarea::TextArea;
|
use crate::bottom_pane::textarea::TextArea;
|
||||||
@@ -268,7 +269,7 @@ pub(super) fn current_prefixed_token_range_with_dollar_predicate(
|
|||||||
/// Returns whether a token candidate's sigil and mention name are editable plaintext.
|
/// Returns whether a token candidate's sigil and mention name are editable plaintext.
|
||||||
///
|
///
|
||||||
/// A candidate is bound when either its entire range is atomic or its mention-name prefix is
|
/// A candidate is bound when either its entire range is atomic or its mention-name prefix is
|
||||||
/// atomic and the remaining text begins with punctuation that terminates the mention.
|
/// atomic and the remaining text begins with a terminator for that sigil.
|
||||||
pub(super) fn prefixed_token_range_is_editable(
|
pub(super) fn prefixed_token_range_is_editable(
|
||||||
textarea: &TextArea,
|
textarea: &TextArea,
|
||||||
prefix: char,
|
prefix: char,
|
||||||
@@ -285,9 +286,14 @@ pub(super) fn prefixed_token_range_is_editable(
|
|||||||
.take_while(|byte| is_mention_name_char(**byte))
|
.take_while(|byte| is_mention_name_char(**byte))
|
||||||
.count();
|
.count();
|
||||||
let mention_end = range.start + prefix.len_utf8() + name_len;
|
let mention_end = range.start + prefix.len_utf8() + name_len;
|
||||||
|
let ends_bound_mention = if prefix == '@' {
|
||||||
|
ends_plaintext_at_mention(textarea.text().as_bytes(), mention_end)
|
||||||
|
} else {
|
||||||
|
ends_plaintext_at_dollar_mention(textarea.text().as_bytes(), mention_end)
|
||||||
|
};
|
||||||
!(name_len > 0
|
!(name_len > 0
|
||||||
&& mention_end < range.end
|
&& mention_end < range.end
|
||||||
&& ends_plaintext_at_mention(textarea.text().as_bytes(), mention_end)
|
&& ends_bound_mention
|
||||||
&& textarea
|
&& textarea
|
||||||
.element_id_for_exact_range(range.start..mention_end)
|
.element_id_for_exact_range(range.start..mention_end)
|
||||||
.is_some())
|
.is_some())
|
||||||
|
|||||||
@@ -81,6 +81,24 @@ fn current_prefixed_token_falls_back_from_bound_at_token_on_right() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_prefixed_token_falls_back_from_bound_dollar_suffix_on_right() {
|
||||||
|
for suffix in ["/path", ".config"] {
|
||||||
|
let text = format!("$left $bound{suffix}");
|
||||||
|
let bound_start = text.find("$bound").expect("bound skill mention");
|
||||||
|
let bound_end = bound_start + "$bound".len();
|
||||||
|
let mut textarea = TextArea::new();
|
||||||
|
textarea.insert_str(&text);
|
||||||
|
textarea.add_element_range(bound_start..bound_end);
|
||||||
|
textarea.set_cursor("$left ".len());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
current_prefixed_token_range(&textarea, '$', /*allow_empty*/ true),
|
||||||
|
Some((0.."$left".len(), "left".to_string()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn current_prefixed_token_targets_later_segment_between_bound_elements() {
|
fn current_prefixed_token_targets_later_segment_between_bound_elements() {
|
||||||
let text = "$bound1$one$bound2$two$bound3";
|
let text = "$bound1$one$bound2$two$bound3";
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/bottom_pane/chat_composer.rs
|
||||||
|
expression: terminal.backend()
|
||||||
|
---
|
||||||
|
" "
|
||||||
|
"› $left $bound/path "
|
||||||
|
" "
|
||||||
|
" "
|
||||||
|
" "
|
||||||
|
" "
|
||||||
|
" left [Skill] Example skill used in tests. "
|
||||||
|
" "
|
||||||
|
" Press enter to insert or esc to close "
|
||||||
Reference in new issue
Block a user