mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
comments
This commit is contained in:
@@ -14,13 +14,9 @@ pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec<usize>, i32)> {
|
||||
return Some((Vec::new(), i32::MAX));
|
||||
}
|
||||
|
||||
// Build a lowercased view of the haystack alongside a mapping from the
|
||||
// lowered-character indices back to the ORIGINAL character indices.
|
||||
let mut lowered_chars: Vec<char> = Vec::new();
|
||||
let mut lowered_to_orig_char_idx: Vec<usize> = Vec::new();
|
||||
for (orig_idx, ch) in haystack.chars().enumerate() {
|
||||
// Lowercasing may yield multiple chars; map each back to the same
|
||||
// original char index so we can highlight correctly.
|
||||
for lc in ch.to_lowercase() {
|
||||
lowered_chars.push(lc);
|
||||
lowered_to_orig_char_idx.push(orig_idx);
|
||||
@@ -29,28 +25,24 @@ pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec<usize>, i32)> {
|
||||
|
||||
let lowered_needle: Vec<char> = needle.to_lowercase().chars().collect();
|
||||
|
||||
// Greedy subsequence match over the lowered chars.
|
||||
let mut result_orig_indices: Vec<usize> = Vec::with_capacity(lowered_needle.len());
|
||||
let mut last_lower_pos: Option<usize> = None;
|
||||
let mut cur = 0usize; // current search start in lowered_chars
|
||||
let mut cur = 0usize;
|
||||
for &nc in lowered_needle.iter() {
|
||||
let mut found_at: Option<usize> = None;
|
||||
while cur < lowered_chars.len() {
|
||||
if lowered_chars[cur] == nc {
|
||||
found_at = Some(cur);
|
||||
cur += 1; // next search starts after this position
|
||||
cur += 1;
|
||||
break;
|
||||
}
|
||||
cur += 1;
|
||||
}
|
||||
let pos = found_at?;
|
||||
// Map the lowered pos back to the original character index.
|
||||
result_orig_indices.push(lowered_to_orig_char_idx[pos]);
|
||||
last_lower_pos = Some(pos);
|
||||
}
|
||||
|
||||
// Score: window length (in lowered char positions) minus lowered needle
|
||||
// length (tighter is better), with a strong bonus for prefix match.
|
||||
let first_lower_pos = if result_orig_indices.is_empty() {
|
||||
0usize
|
||||
} else {
|
||||
@@ -65,10 +57,9 @@ pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec<usize>, i32)> {
|
||||
(last_lower_pos as i32 - first_lower_pos as i32 + 1) - (lowered_needle.len() as i32);
|
||||
let mut score = window.max(0);
|
||||
if first_lower_pos == 0 {
|
||||
score -= 100; // strong bonus for prefix match
|
||||
score -= 100;
|
||||
}
|
||||
|
||||
// Ensure indices are unique and sorted for robust highlighting consumers.
|
||||
result_orig_indices.sort_unstable();
|
||||
result_orig_indices.dedup();
|
||||
Some((result_orig_indices, score))
|
||||
@@ -77,7 +68,6 @@ pub fn fuzzy_match(haystack: &str, needle: &str) -> Option<(Vec<usize>, i32)> {
|
||||
/// Convenience wrapper to get only the indices for a fuzzy match.
|
||||
pub fn fuzzy_indices(haystack: &str, needle: &str) -> Option<Vec<usize>> {
|
||||
fuzzy_match(haystack, needle).map(|(mut idx, _)| {
|
||||
// Already sorted/deduped, but keep invariant local to this helper too.
|
||||
idx.sort_unstable();
|
||||
idx.dedup();
|
||||
idx
|
||||
@@ -99,7 +89,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unicode_dotted_i_istanbul_highlighting() {
|
||||
// 'İ' lowercases to two chars (i + combining dot). Ensure index 0 is highlighted once.
|
||||
let (idx, _score) = match fuzzy_match("İstanbul", "is") {
|
||||
Some(v) => v,
|
||||
None => panic!("expected a match"),
|
||||
@@ -109,8 +98,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unicode_german_sharp_s_casefold() {
|
||||
// Ensure we at least don't panic with non-ASCII inputs; lowercase-based
|
||||
// matching does not equate ß with "ss", so this should be None.
|
||||
assert!(fuzzy_match("straße", "strasse").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,14 +187,10 @@ impl App<'_> {
|
||||
let pasted = pasted.replace("\r", "\n");
|
||||
app_event_tx.send(AppEvent::Paste(pasted));
|
||||
}
|
||||
_ => {
|
||||
// Ignore any other events.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Timeout expired, no `Event` is available
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -344,24 +340,19 @@ impl App<'_> {
|
||||
modifiers: crossterm::event::KeyModifiers::CONTROL,
|
||||
kind: KeyEventKind::Press,
|
||||
..
|
||||
} => {
|
||||
match &mut self.app_state {
|
||||
AppState::Chat { widget } => {
|
||||
if widget.composer_is_empty() {
|
||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||
} else {
|
||||
// Treat Ctrl+D as a normal key event when the composer
|
||||
// is not empty so that it doesn't quit the application
|
||||
// prematurely.
|
||||
self.dispatch_key_event(key_event);
|
||||
}
|
||||
}
|
||||
AppState::GitWarning { .. } => {
|
||||
} => match &mut self.app_state {
|
||||
AppState::Chat { widget } => {
|
||||
if widget.composer_is_empty() {
|
||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||
} else {
|
||||
self.dispatch_key_event(key_event);
|
||||
}
|
||||
AppState::DangerWarning { .. } => {}
|
||||
}
|
||||
}
|
||||
AppState::GitWarning { .. } => {
|
||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||
}
|
||||
AppState::DangerWarning { .. } => {}
|
||||
},
|
||||
KeyEvent {
|
||||
kind: KeyEventKind::Press | KeyEventKind::Repeat,
|
||||
..
|
||||
@@ -654,9 +645,7 @@ impl App<'_> {
|
||||
GitWarningOutcome::Quit => {
|
||||
self.app_event_tx.send(AppEvent::ExitRequest);
|
||||
}
|
||||
GitWarningOutcome::None => {
|
||||
// do nothing
|
||||
}
|
||||
GitWarningOutcome::None => {}
|
||||
},
|
||||
AppState::DangerWarning { screen, .. } => match screen.handle_key_event(key_event) {
|
||||
DangerWarningOutcome::Continue => {
|
||||
@@ -667,8 +656,6 @@ impl App<'_> {
|
||||
},
|
||||
);
|
||||
let _ = ct_execute!(std::io::stdout(), LeaveAlternateScreen);
|
||||
// After leaving the alternate screen, resync our viewport/cursor
|
||||
// so the chat composer stays anchored at the bottom.
|
||||
self.fixup_viewport_after_danger = true;
|
||||
if let AppState::DangerWarning {
|
||||
mut widget,
|
||||
@@ -692,8 +679,6 @@ impl App<'_> {
|
||||
},
|
||||
);
|
||||
let _ = ct_execute!(std::io::stdout(), LeaveAlternateScreen);
|
||||
// After leaving the alternate screen, resync our viewport/cursor
|
||||
// so the chat composer stays anchored at the bottom.
|
||||
self.fixup_viewport_after_danger = true;
|
||||
if let AppState::DangerWarning { widget, .. } = taken {
|
||||
self.app_state = AppState::Chat { widget };
|
||||
@@ -748,7 +733,6 @@ mod tests {
|
||||
("/model another_model", "another_model", "another_model"),
|
||||
];
|
||||
for (line, raw_expected, norm_expected) in cases {
|
||||
// Extract raw args as in chat_composer
|
||||
let raw = if let Some(stripped) = line.strip_prefix('/') {
|
||||
let token = stripped.trim_start();
|
||||
let cmd_token = token.split_whitespace().next().unwrap_or("");
|
||||
@@ -758,7 +742,6 @@ mod tests {
|
||||
String::new()
|
||||
};
|
||||
assert_eq!(raw, raw_expected, "raw args for '{line}'");
|
||||
// Normalize as in app dispatch logic
|
||||
let normalized = strip_surrounding_quotes(&raw).trim().to_string();
|
||||
assert_eq!(normalized, norm_expected, "normalized args for '{line}'");
|
||||
}
|
||||
|
||||
@@ -238,9 +238,6 @@ impl ChatComposer {
|
||||
ActivePopup::Selection(SelectionPopup::new_model(current_model, options));
|
||||
}
|
||||
}
|
||||
// If the composer currently contains a `/model` command, initialize the
|
||||
// popup's query from its arguments. Otherwise, leave the popup visible
|
||||
// with an empty query.
|
||||
let first_line_owned = self.first_line().to_string();
|
||||
if let ParsedSlash::Command { cmd, args } = parse_slash_line(&first_line_owned) {
|
||||
if cmd == SlashCommand::Model {
|
||||
@@ -268,7 +265,6 @@ impl ChatComposer {
|
||||
));
|
||||
}
|
||||
}
|
||||
// Initialize the popup's query from the arguments to `/approvals`, if present.
|
||||
let first_line_owned = self.first_line().to_string();
|
||||
if let ParsedSlash::Command { cmd, args } = parse_slash_line(&first_line_owned) {
|
||||
if cmd == SlashCommand::Approvals {
|
||||
@@ -366,7 +362,6 @@ impl ChatComposer {
|
||||
..
|
||||
} => {
|
||||
if let Some(cmd) = popup.selected_command() {
|
||||
// Extract arguments after the command from the first line using the shared parser.
|
||||
let args_opt = match parse_slash_line(&first_line_owned) {
|
||||
ParsedSlash::Command {
|
||||
cmd: parsed_cmd,
|
||||
@@ -378,28 +373,21 @@ impl ChatComposer {
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Send command + args (if any) to the app layer.
|
||||
self.app_event_tx.send(AppEvent::DispatchCommand {
|
||||
cmd: *cmd,
|
||||
args: args_opt,
|
||||
});
|
||||
// Clear textarea so no residual text remains.
|
||||
self.textarea.set_text("");
|
||||
// Hide popup since the command has been dispatched.
|
||||
self.active_popup = ActivePopup::None;
|
||||
return (InputResult::None, true);
|
||||
}
|
||||
// No valid selection – treat as invalid command: dismiss popup and surface error.
|
||||
let invalid_token = match parse_slash_line(&first_line_owned) {
|
||||
ParsedSlash::Command { cmd, .. } => cmd.command().to_string(),
|
||||
ParsedSlash::Incomplete { token } => token.to_string(),
|
||||
ParsedSlash::None => String::new(),
|
||||
};
|
||||
// Prevent immediate reopen for the same token.
|
||||
self.dismissed.slash = Some(invalid_token.clone());
|
||||
self.active_popup = ActivePopup::None;
|
||||
|
||||
// Emit an error entry into history so the user understands what happened.
|
||||
{
|
||||
use crate::history_cell::HistoryCell;
|
||||
let message = if invalid_token.is_empty() {
|
||||
@@ -441,7 +429,6 @@ impl ChatComposer {
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
} => {
|
||||
// Hide popup without modifying text, remember token to avoid immediate reopen.
|
||||
if let Some(tok) = Self::current_at_token(&self.textarea) {
|
||||
self.dismissed.file = Some(tok.to_string());
|
||||
}
|
||||
@@ -458,7 +445,6 @@ impl ChatComposer {
|
||||
} => {
|
||||
if let Some(sel) = popup.selected_match() {
|
||||
let sel_path = sel.to_string();
|
||||
// Drop popup borrow before using self mutably again.
|
||||
self.insert_selected_path(&sel_path);
|
||||
self.active_popup = ActivePopup::None;
|
||||
return (InputResult::None, true);
|
||||
@@ -496,7 +482,6 @@ impl ChatComposer {
|
||||
KeyEvent {
|
||||
code: KeyCode::Esc, ..
|
||||
} => {
|
||||
// Hide selection popup; keep composer content unchanged.
|
||||
self.active_popup = ActivePopup::None;
|
||||
(InputResult::None, true)
|
||||
}
|
||||
@@ -682,7 +667,6 @@ impl ChatComposer {
|
||||
.unwrap_or(after_cursor.len());
|
||||
let end_idx = cursor_offset + end_rel_idx;
|
||||
|
||||
// Replace the slice `[start_idx, end_idx)` with the chosen path and a trailing space.
|
||||
let mut new_text =
|
||||
String::with_capacity(text.len() - (end_idx - start_idx) + path.len() + 1);
|
||||
new_text.push_str(&text[..start_idx]);
|
||||
@@ -696,11 +680,6 @@ impl ChatComposer {
|
||||
/// Handle key event when no popup is visible.
|
||||
fn handle_key_event_without_popup(&mut self, key_event: KeyEvent) -> (InputResult, bool) {
|
||||
match key_event {
|
||||
// -------------------------------------------------------------
|
||||
// History navigation (Up / Down) – only when the composer is not
|
||||
// empty or when the cursor is at the correct position, to avoid
|
||||
// interfering with normal cursor movement.
|
||||
// -------------------------------------------------------------
|
||||
KeyEvent {
|
||||
code: KeyCode::Up | KeyCode::Down,
|
||||
..
|
||||
@@ -730,7 +709,6 @@ impl ChatComposer {
|
||||
let mut text = self.textarea.text().to_string();
|
||||
self.textarea.set_text("");
|
||||
|
||||
// Replace all pending pastes in the text
|
||||
for (placeholder, actual) in &self.pending_pastes {
|
||||
if text.contains(placeholder) {
|
||||
text = text.replace(placeholder, actual);
|
||||
@@ -751,7 +729,6 @@ impl ChatComposer {
|
||||
|
||||
/// Handle generic Input events that modify the textarea content.
|
||||
fn handle_input_basic(&mut self, input: KeyEvent) -> (InputResult, bool) {
|
||||
// Special handling for backspace on placeholders
|
||||
if let KeyEvent {
|
||||
code: KeyCode::Backspace,
|
||||
..
|
||||
@@ -761,12 +738,8 @@ impl ChatComposer {
|
||||
return (InputResult::None, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Normal input handling
|
||||
self.textarea.input(input);
|
||||
let text_after = self.textarea.text();
|
||||
|
||||
// Check if any placeholders were removed and remove their corresponding pending pastes
|
||||
self.pending_pastes
|
||||
.retain(|(placeholder, _)| text_after.contains(placeholder));
|
||||
|
||||
@@ -801,9 +774,6 @@ impl ChatComposer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronize `self.command_popup` with the current text in the
|
||||
/// textarea. This must be called after every modification that can change
|
||||
/// the text so the popup is shown/updated/hidden as appropriate.
|
||||
fn sync_command_popup(&mut self) {
|
||||
let first_line = self.textarea.text().lines().next().unwrap_or("");
|
||||
let input_starts_with_slash = first_line.starts_with('/');
|
||||
@@ -827,8 +797,6 @@ impl ChatComposer {
|
||||
}
|
||||
_ => {
|
||||
if input_starts_with_slash {
|
||||
// Avoid immediate reopen of the slash popup if it was just dismissed for
|
||||
// this exact command token.
|
||||
if self
|
||||
.dismissed
|
||||
.slash
|
||||
@@ -845,10 +813,7 @@ impl ChatComposer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Synchronize `self.file_search_popup` with the current text in the textarea.
|
||||
/// Note this is only called when self.active_popup is NOT Command.
|
||||
fn sync_file_search_popup(&mut self) {
|
||||
// Determine if there is an @token underneath the cursor.
|
||||
let query = match Self::current_at_token(&self.textarea) {
|
||||
Some(token) => token,
|
||||
None => {
|
||||
@@ -857,8 +822,6 @@ impl ChatComposer {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// If user dismissed popup for this exact query, don't reopen until text changes.
|
||||
if self.dismissed.file.as_ref() == Some(&query) {
|
||||
return;
|
||||
}
|
||||
@@ -881,13 +844,6 @@ impl ChatComposer {
|
||||
self.dismissed.file = None;
|
||||
}
|
||||
|
||||
/// Synchronize the selection popup filter with the current composer text.
|
||||
///
|
||||
/// When a selection popup is open, we want typing to filter the visible
|
||||
/// options. If the user is typing a slash command (e.g. `/model o3` or
|
||||
/// `/approvals auto`), we use only the arguments after the command token
|
||||
/// as the filter. Otherwise, we treat the entire first line as the filter
|
||||
/// so that typing free‑form text narrows the list as well.
|
||||
fn sync_selection_popup(&mut self) {
|
||||
let first_line_owned = self.first_line().to_string();
|
||||
match (&mut self.active_popup, parse_slash_line(&first_line_owned)) {
|
||||
@@ -897,13 +853,10 @@ impl ChatComposer {
|
||||
SelectionKind::Model if cmd == SlashCommand::Model => popup.set_query(args),
|
||||
SelectionKind::Execution if cmd == SlashCommand::Approvals => popup.set_query(args),
|
||||
_ => {
|
||||
// Command present but not relevant to the open selector –
|
||||
// fall back to using the free‑form text as the query.
|
||||
popup.set_query(first_line_owned.trim());
|
||||
}
|
||||
},
|
||||
(ActivePopup::Selection(popup), _no_slash_cmd) => {
|
||||
// No slash command present – use whatever is typed as the query.
|
||||
popup.set_query(first_line_owned.trim());
|
||||
}
|
||||
_ => {}
|
||||
|
||||
@@ -179,9 +179,7 @@ impl BottomPane<'_> {
|
||||
ConditionalUpdate::NeedsRedraw => {
|
||||
self.request_redraw();
|
||||
}
|
||||
ConditionalUpdate::NoRedraw => {
|
||||
// No redraw needed.
|
||||
}
|
||||
ConditionalUpdate::NoRedraw => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +209,6 @@ impl BottomPane<'_> {
|
||||
|
||||
match (running, self.active_view.is_some()) {
|
||||
(true, false) => {
|
||||
// Show status indicator overlay.
|
||||
self.active_view = Some(Box::new(StatusIndicatorView::new(
|
||||
self.app_event_tx.clone(),
|
||||
)));
|
||||
@@ -220,17 +217,13 @@ impl BottomPane<'_> {
|
||||
(false, true) => {
|
||||
if let Some(mut view) = self.active_view.take() {
|
||||
if view.should_hide_when_task_is_done() {
|
||||
// Leave self.active_view as None.
|
||||
self.request_redraw();
|
||||
} else {
|
||||
// Preserve the view.
|
||||
self.active_view = Some(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// No change.
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +301,6 @@ impl BottomPane<'_> {
|
||||
|
||||
impl WidgetRef for &BottomPane<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
// Show BottomPaneView if present.
|
||||
if let Some(ov) = &self.active_view {
|
||||
ov.render(area, buf);
|
||||
} else {
|
||||
|
||||
@@ -99,7 +99,6 @@ mod tests {
|
||||
s.ensure_visible(len, vis);
|
||||
assert_eq!(s.scroll_top, 0);
|
||||
|
||||
// Move up wraps to end
|
||||
s.move_up_wrap(len);
|
||||
s.ensure_visible(len, vis);
|
||||
assert_eq!(s.selected_idx, Some(len - 1));
|
||||
@@ -108,7 +107,6 @@ mod tests {
|
||||
None => panic!("expected Some(selected_idx) after wrap"),
|
||||
}
|
||||
|
||||
// Move down wraps to start
|
||||
s.move_down_wrap(len);
|
||||
s.ensure_visible(len, vis);
|
||||
assert_eq!(s.selected_idx, Some(0));
|
||||
|
||||
@@ -112,7 +112,6 @@ impl<T: Clone> SelectionList<T> {
|
||||
pub fn visible_rows(&self) -> Vec<(GenericDisplayRow, Option<&T>)> {
|
||||
let query = self.query.trim();
|
||||
|
||||
// Helper to convert an item to a GenericDisplayRow with optional match indices.
|
||||
let to_row = |it: &SelectionItem<T>, match_indices: Option<Vec<usize>>| GenericDisplayRow {
|
||||
name: it.name.clone(),
|
||||
match_indices,
|
||||
@@ -128,9 +127,6 @@ impl<T: Clone> SelectionList<T> {
|
||||
.collect();
|
||||
}
|
||||
|
||||
// Fuzzy search across names and aliases; sort by score (smaller is better),
|
||||
// then by name for a stable ordering. If the current item matches, include
|
||||
// it with indices so the UI can highlight matches.
|
||||
let mut out: Vec<(GenericDisplayRow, Option<&T>, i32, usize)> = Vec::new();
|
||||
|
||||
for it in self.items.iter() {
|
||||
@@ -143,7 +139,6 @@ impl<T: Clone> SelectionList<T> {
|
||||
));
|
||||
continue;
|
||||
}
|
||||
// Try aliases; keep the best score but do not show indices on the primary name.
|
||||
let mut best_alias_score: Option<i32> = None;
|
||||
for alias in it.aliases.iter() {
|
||||
if let Some((_idx, score)) = fuzzy_match(alias, query) {
|
||||
@@ -155,8 +150,6 @@ impl<T: Clone> SelectionList<T> {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort: lower score first (fuzzy_match returns smaller score = better),
|
||||
// then by name asc, then by name length as a final tiebreaker.
|
||||
out.sort_by(|a, b| {
|
||||
a.2.cmp(&b.2)
|
||||
.then_with(|| a.0.name.cmp(&b.0.name))
|
||||
@@ -176,7 +169,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn selection_list_query_and_navigation() {
|
||||
// Build a small list with aliases similar to execution-mode popup.
|
||||
let items = vec![
|
||||
SelectionItem::new("a", "Auto".to_string()).with_aliases(vec!["auto".into()]),
|
||||
SelectionItem::new("u", "Untrusted".to_string()).with_aliases(vec!["untrusted".into()]),
|
||||
@@ -185,31 +177,25 @@ mod tests {
|
||||
|
||||
let mut list = SelectionList::new(items);
|
||||
|
||||
// No query shows all, selection clamped to first row.
|
||||
let rows = list.visible_rows();
|
||||
assert_eq!(rows.len(), 3);
|
||||
assert_eq!(list.selected_value(), Some("a"));
|
||||
|
||||
// Up wraps to the end.
|
||||
list.move_up();
|
||||
assert_eq!(list.selected_value(), Some("r"));
|
||||
// Down wraps back to the start.
|
||||
list.move_down();
|
||||
assert_eq!(list.selected_value(), Some("a"));
|
||||
|
||||
// Query by name prefix prefers the tighter match.
|
||||
list.set_query("auto");
|
||||
let rows = list.visible_rows();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(list.selected_value(), Some("a"));
|
||||
|
||||
// Query by alias works too.
|
||||
list.set_query("read-only");
|
||||
let rows = list.visible_rows();
|
||||
assert_eq!(rows.len(), 1);
|
||||
assert_eq!(list.selected_value(), Some("r"));
|
||||
|
||||
// No matches clears selection.
|
||||
list.set_query("not-a-match");
|
||||
let rows = list.visible_rows();
|
||||
assert_eq!(rows.len(), 0);
|
||||
|
||||
@@ -94,7 +94,6 @@ impl ExecutionPreset {
|
||||
/// Strip a single pair of surrounding quotes from the provided string if present.
|
||||
/// Supports straight and common curly quotes: '…', "…", ‘…’, “…”.
|
||||
pub fn strip_surrounding_quotes(s: &str) -> &str {
|
||||
// Opening/closing pairs (note curly quotes differ on each side)
|
||||
const QUOTE_PAIRS: &[(char, char)] = &[('\"', '\"'), ('\'', '\''), ('“', '”'), ('‘', '’')];
|
||||
|
||||
let t = s.trim();
|
||||
@@ -125,8 +124,6 @@ pub fn execution_mode_label(approval: AskForApproval, sandbox: &SandboxPolicy) -
|
||||
.unwrap_or("Custom")
|
||||
}
|
||||
|
||||
// removed unused execution_mode_description and cli_flags helpers
|
||||
|
||||
/// Parse a free-form token to an execution preset (approval+sandbox).
|
||||
pub fn parse_execution_mode_token(s: &str) -> Option<(AskForApproval, SandboxPolicy)> {
|
||||
ExecutionPreset::parse_token(s).map(|p| p.to_policies())
|
||||
@@ -180,7 +177,6 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn execution_preset_round_trip() {
|
||||
// For each preset, ensure to_policies -> from_policies -> label/description are consistent
|
||||
let presets = [
|
||||
ExecutionPreset::ReadOnly,
|
||||
ExecutionPreset::Untrusted,
|
||||
@@ -190,13 +186,9 @@ mod tests {
|
||||
|
||||
for p in presets {
|
||||
let (a, s) = p.to_policies();
|
||||
// Back to preset
|
||||
assert_eq!(ExecutionPreset::from_policies(a, &s), Some(p));
|
||||
// Labels and descriptions are non-empty and stable
|
||||
assert!(!p.label().is_empty());
|
||||
assert!(!p.description().is_empty());
|
||||
|
||||
// Parsing the canonical token yields the same preset
|
||||
let token = match p {
|
||||
ExecutionPreset::ReadOnly => "read-only",
|
||||
ExecutionPreset::Untrusted => "untrusted",
|
||||
|
||||
Reference in New Issue
Block a user