use super::*;
const RECENT_POPUP_CAP: usize = 16;
pub(crate) fn cmdline_popup_list(app: &App, line: &str) -> Option<CmdlineCompleteState> {
if line.trim().is_empty() {
if app.recent_commands.is_empty() {
return None;
}
return Some(CmdlineCompleteState {
head: String::new(),
matches: app
.recent_commands
.iter()
.take(RECENT_POPUP_CAP)
.cloned()
.collect(),
idx: 0,
last_shown: String::new(),
});
}
compute_cmdline_completions_for_app(app, line).filter(|s| !s.matches.is_empty())
}
pub(crate) fn compute_cmdline_completions_for_app(
app: &App,
line: &str,
) -> Option<CmdlineCompleteState> {
use crate::input::vim::EX_COMPLETION_NAMES;
let split_at = line.rfind(char::is_whitespace).map(|i| i + 1).unwrap_or(0);
let head = &line[..split_at];
let token = &line[split_at..];
let first_word = head.split_whitespace().next().unwrap_or("");
if !head.is_empty() && matches!(first_word, "b" | "buffer") {
let token_lc = token.to_lowercase();
let mut matches: Vec<String> = app
.panes
.iter()
.filter_map(|p| match p {
Pane::Editor(b) => Some(b.display_name().to_string()),
_ => None,
})
.filter(|n| n.to_lowercase().contains(&token_lc) || token.is_empty())
.collect();
matches.sort();
matches.dedup();
return Some(CmdlineCompleteState {
head: head.to_string(),
matches,
idx: 0,
last_shown: String::new(),
});
}
if !head.is_empty() && matches!(first_word, "colorscheme" | "colo") {
let mut matches: Vec<String> = crate::ui::theme::names()
.into_iter()
.filter(|n| n.starts_with(token))
.map(String::from)
.collect();
matches.sort();
matches.dedup();
return Some(CmdlineCompleteState {
head: head.to_string(),
matches,
idx: 0,
last_shown: String::new(),
});
}
if head.is_empty() {
let token_lc = token.to_lowercase();
let mut scored: Vec<(i32, String)> = Vec::new();
for cmd in crate::command::registry().all() {
let mut score = 0i32;
let id_lc = cmd.id.to_lowercase();
if id_lc.starts_with(&token_lc) {
score = score.max(300);
} else if !token.is_empty() && id_lc.contains(&token_lc) {
score = score.max(200);
}
if score == 0
&& token.chars().count() >= 2
&& cmd.title.to_lowercase().contains(&token_lc)
{
score = 100;
}
if score > 0 {
if let Some(pos) = app.recent_commands.iter().position(|r| r == cmd.id) {
score += (50 - pos as i32).max(0);
}
scored.push((score, cmd.id.to_string()));
}
}
for name in EX_COMPLETION_NAMES {
if name.starts_with(token) {
scored.push((150, name.to_string()));
}
}
scored.sort_by(|a, b| b.0.cmp(&a.0).then_with(|| a.1.cmp(&b.1)));
scored.dedup_by(|a, b| a.1 == b.1);
let matches: Vec<String> = scored.into_iter().map(|(_, s)| s).collect();
return Some(CmdlineCompleteState {
head: String::new(),
matches,
idx: 0,
last_shown: String::new(),
});
}
let path_takers = [
"e", "edit", "sp", "split", "vs", "vsp", "vsplit", "tabnew", "tabe", "tabedit", "badd",
"ba", "saveas", "w", "write", "source", "so", "r", "read", "Files",
];
if !path_takers.contains(&first_word) {
return Some(CmdlineCompleteState {
head: head.to_string(),
matches: Vec::new(),
idx: 0,
last_shown: String::new(),
});
}
let (dir_part, stem) = match token.rfind('/') {
Some(i) => (&token[..=i], &token[i + 1..]),
None => ("", token),
};
let base = if dir_part.is_empty() {
app.workspace.to_path_buf()
} else if dir_part.starts_with('/') {
Path::new(dir_part).to_path_buf()
} else {
app.workspace.join(dir_part)
};
let mut matches: Vec<String> = Vec::new();
if let Ok(entries) = std::fs::read_dir(&base) {
for ent in entries.flatten() {
let Some(name) = ent.file_name().to_str().map(|s| s.to_string()) else {
continue;
};
if !name.starts_with(stem) {
continue;
}
if name.starts_with('.') && !stem.starts_with('.') {
continue;
}
let is_dir = ent.file_type().map(|t| t.is_dir()).unwrap_or(false);
let display = if is_dir {
format!("{dir_part}{name}/")
} else {
format!("{dir_part}{name}")
};
matches.push(display);
}
}
matches.sort();
matches.dedup();
Some(CmdlineCompleteState {
head: head.to_string(),
matches,
idx: 0,
last_shown: String::new(),
})
}
pub(crate) fn parse_undo_age_spec(arg: &str) -> Option<u64> {
let arg = arg.trim();
if arg.is_empty() {
return None;
}
let unit = arg.chars().last()?;
let mult = match unit {
's' => 1u64,
'm' => 60,
'h' => 3600,
'd' => 86_400,
_ => return None,
};
let num_part = &arg[..arg.len() - unit.len_utf8()];
num_part.parse::<u64>().ok().map(|n| n * mult)
}
pub(crate) fn parse_line_range(
line: &str,
current_line: usize,
line_count: usize,
) -> Option<(usize, usize, &str)> {
let first = line.chars().next()?;
if !(first.is_ascii_digit() || first == '.' || first == '$') {
return None;
}
let bytes = line.as_bytes();
let mut split = 0usize;
while split < bytes.len() {
let b = bytes[split];
if b.is_ascii_alphabetic() || b == b'>' || b == b'<' {
break;
}
split += 1;
}
if split == 0 || split == bytes.len() {
return None;
}
let spec = &line[..split];
let remainder = &line[split..];
let (from_str, to_str) = match spec.find(',') {
Some(comma) => (&spec[..comma], &spec[comma + 1..]),
None => (spec, spec),
};
let resolve = |part: &str| -> Option<usize> {
let part = part.trim();
if part == "$" {
return Some(line_count.saturating_sub(1));
}
if part == "." || part.is_empty() {
return Some(current_line);
}
if let Some(rest) = part.strip_prefix(".+") {
let n: usize = rest.parse().ok()?;
return Some(
current_line
.saturating_add(n)
.min(line_count.saturating_sub(1)),
);
}
if let Some(rest) = part.strip_prefix(".-") {
let n: usize = rest.parse().ok()?;
return Some(current_line.saturating_sub(n));
}
if let Some(rest) = part.strip_prefix('+') {
let n: usize = rest.parse().ok()?;
return Some(
current_line
.saturating_add(n)
.min(line_count.saturating_sub(1)),
);
}
if let Some(rest) = part.strip_prefix('-') {
let n: usize = rest.parse().ok()?;
return Some(current_line.saturating_sub(n));
}
let n: usize = part.parse().ok()?;
Some(n.saturating_sub(1).min(line_count.saturating_sub(1)))
};
let from = resolve(from_str)?;
let to = resolve(to_str)?;
let (lo, hi) = if from <= to { (from, to) } else { (to, from) };
Some((lo, hi, remainder))
}
pub(crate) fn parse_substitute(line: &str) -> Option<Substitute> {
let (rest, whole_buffer) = if let Some(r) = line.strip_prefix("%s/") {
(r, true)
} else {
let r = line.strip_prefix("s/")?;
(r, false)
};
let mut parts: Vec<String> = Vec::with_capacity(3);
let mut cur = String::new();
let mut chars = rest.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('/') => cur.push('/'),
Some('\\') => cur.push('\\'),
Some('n') => cur.push('\n'),
Some('t') => cur.push('\t'),
Some(other) => {
cur.push('\\');
cur.push(other);
}
None => cur.push('\\'),
}
} else if c == '/' {
parts.push(std::mem::take(&mut cur));
if parts.len() == 2 {
let flags: String = chars.collect();
parts.push(flags);
break;
}
} else {
cur.push(c);
}
}
if parts.len() < 2 {
parts.push(String::new());
}
let find = parts.remove(0);
let replace = parts.remove(0);
let flags = parts.first().cloned().unwrap_or_default();
let case_insensitive = flags.contains('i');
let confirm = flags.contains('c');
let count_only = flags.contains('n');
let global = flags.contains('g');
Some(Substitute {
find,
replace,
case_insensitive,
whole_buffer,
confirm,
count_only,
global,
line_range: None,
})
}
impl App {
pub fn open_ex_command_prompt(&mut self) {
self.no_pane_cmdline = Some(String::new());
}
pub fn no_pane_cmdline_push_char(&mut self, ch: char) {
if let Some(buf) = self.no_pane_cmdline.as_mut() {
buf.push(ch);
}
}
pub fn no_pane_cmdline_commit(&mut self) {
let Some(line) = self.no_pane_cmdline.take() else {
return;
};
let line = line.trim().to_string();
if !line.is_empty() {
self.run_ex_command(&line);
}
}
pub fn no_pane_cmdline_cancel(&mut self) {
self.no_pane_cmdline = None;
}
}