use std::borrow::Cow;
use std::sync::{Arc, Mutex};
use nu_ansi_term::{Color as NuColor, Style as NuStyle};
use reedline::{
Completer, Highlighter, Prompt, PromptEditMode, PromptHistorySearch, PromptHistorySearchStatus,
Span, StyledText, Suggestion, ValidationResult, Validator,
};
use syntect::easy::HighlightLines;
use syntect::highlighting::{Theme, ThemeSet};
use syntect::parsing::{SyntaxReference, SyntaxSet};
use crate::lex;
use crate::ui::Ui;
const MAGICS: &[&str] = &[
"%help", "%quit", "%exit", "%clear", "%reset", "%src", "%header", "%edit", "%type", "%time",
"%timeit", "%undo", "%cc", "%std",
];
const C_WORDS: &[&str] = &[
"auto",
"bool",
"break",
"case",
"char",
"const",
"continue",
"default",
"do",
"double",
"else",
"enum",
"extern",
"false",
"float",
"for",
"goto",
"if",
"inline",
"int",
"long",
"register",
"restrict",
"return",
"short",
"signed",
"sizeof",
"static",
"struct",
"switch",
"typedef",
"union",
"unsigned",
"void",
"volatile",
"while",
"_Bool",
"_Generic",
"true",
"size_t",
"int8_t",
"int16_t",
"int32_t",
"int64_t",
"uint8_t",
"uint16_t",
"uint32_t",
"uint64_t",
"intptr_t",
"uintptr_t",
"ptrdiff_t",
"NULL",
"printf",
"scanf",
"puts",
"putchar",
"getchar",
"malloc",
"calloc",
"realloc",
"free",
"memcpy",
"memmove",
"memset",
"memcmp",
"memchr",
"strlen",
"strcmp",
"strncmp",
"strcpy",
"strncpy",
"strcat",
"strncat",
"strchr",
"strrchr",
"strstr",
"strtok",
"atoi",
"atof",
"strtol",
"strtod",
"abs",
"qsort",
"exit",
"fopen",
"fclose",
"fread",
"fwrite",
"fprintf",
];
pub struct CHighlighter {
syntaxes: SyntaxSet,
syntax: SyntaxReference,
theme: Theme,
color: bool,
}
impl CHighlighter {
pub fn new(color: bool) -> Self {
let syntaxes = SyntaxSet::load_defaults_newlines();
let syntax = syntaxes
.find_syntax_by_extension("c")
.unwrap_or_else(|| syntaxes.find_syntax_plain_text())
.clone();
let theme = ThemeSet::load_defaults().themes["base16-ocean.dark"].clone();
CHighlighter {
syntaxes,
syntax,
theme,
color,
}
}
}
pub struct CCompleter {
idents: Arc<Mutex<Vec<String>>>,
}
impl CCompleter {
pub fn new(idents: Arc<Mutex<Vec<String>>>) -> Self {
CCompleter { idents }
}
}
pub struct CValidator;
pub struct CPrompt {
pub n: usize,
}
fn completion_candidates(line: &str, pos: usize, idents: &[String]) -> (usize, Vec<String>) {
let b = line.as_bytes();
let mut start = pos;
while start > 0 && (b[start - 1].is_ascii_alphanumeric() || b[start - 1] == b'_') {
start -= 1;
}
if start > 0 && b[start - 1] == b'%' && line[..start - 1].trim_start().is_empty() {
let word = &line[start - 1..pos];
let m = MAGICS
.iter()
.filter(|c| c.starts_with(word))
.map(|s| s.to_string())
.collect();
return (start - 1, m);
}
let word = &line[start..pos];
if word.is_empty() || word.as_bytes()[0].is_ascii_digit() {
return (pos, Vec::new());
}
if !lex::scan(line).code.get(start).copied().unwrap_or(true) {
return (pos, Vec::new());
}
let mut out: Vec<String> = C_WORDS
.iter()
.filter(|k| k.starts_with(word))
.map(|s| s.to_string())
.chain(idents.iter().filter(|k| k.starts_with(word)).cloned())
.collect();
out.sort();
out.dedup();
(start, out)
}
impl Highlighter for CHighlighter {
fn highlight(&self, line: &str, _cursor: usize) -> StyledText {
let mut styled = StyledText::new();
if !self.color {
styled.push((NuStyle::new(), line.to_string()));
return styled;
}
if line.trim_start().starts_with('%') {
styled.push((NuStyle::new().fg(NuColor::Cyan), line.to_string()));
return styled;
}
for (i, physical) in line.split('\n').enumerate() {
if i > 0 {
styled.push((NuStyle::new(), "\n".to_string()));
}
let mut h = HighlightLines::new(&self.syntax, &self.theme);
match h.highlight_line(physical, &self.syntaxes) {
Ok(ranges) => {
for (syn, text) in ranges {
let c = syn.foreground;
let style = NuStyle::new().fg(NuColor::Rgb(c.r, c.g, c.b));
styled.push((style, text.to_string()));
}
}
Err(_) => styled.push((NuStyle::new(), physical.to_string())),
}
}
styled
}
}
pub fn is_structurally_incomplete(input: &str) -> bool {
let t = input.trim();
if t.is_empty() || t.starts_with('%') {
return false;
}
let sc = lex::scan(input);
if t.starts_with('#') {
return sc.unterminated
|| input.trim_end().ends_with('\\')
|| preprocessor_depth(input) > 0;
}
sc.depth > 0
|| sc.unterminated
|| input.trim_end().ends_with('\\')
|| lex::awaits_body(input)
|| control_header_awaits_body(input)
|| do_awaits_while(input)
|| tag_definition_awaits_semicolon(input)
}
pub fn is_incomplete(input: &str) -> bool {
if is_structurally_incomplete(input) {
return true;
}
needs_blank_confirmation(input) && !ends_in_blank_line(input)
}
pub fn can_accept_else(input: &str) -> bool {
!input.trim_start().starts_with('#')
&& !is_structurally_incomplete(input)
&& first_code_word(input).as_deref() == Some("if")
}
pub fn starts_with_else(input: &str) -> bool {
!input.trim_start().starts_with('#') && first_code_word(input).as_deref() == Some("else")
}
fn needs_blank_confirmation(input: &str) -> bool {
!input.trim_start().starts_with('#') && first_code_word(input).as_deref() == Some("if")
}
fn ends_in_blank_line(input: &str) -> bool {
input
.rfind('\n')
.is_some_and(|i| input[i + 1..].trim().is_empty())
}
fn preprocessor_depth(input: &str) -> i32 {
let mut depth = 0i32;
for line in input.lines() {
let Some(directive) = line.trim_start().strip_prefix('#') else {
continue;
};
let word = directive
.trim_start()
.split(|c: char| c.is_whitespace() || c == '(')
.next()
.unwrap_or("");
match word {
"if" | "ifdef" | "ifndef" => depth += 1,
"endif" => depth -= 1,
_ => {}
}
}
depth
}
fn first_code_word(input: &str) -> Option<String> {
lex::identifiers(input).into_iter().next()
}
fn control_header_awaits_body(input: &str) -> bool {
let Some(first) = first_code_word(input) else {
return false;
};
if !matches!(first.as_str(), "if" | "for" | "while" | "switch") {
return false;
}
let sc = lex::scan(input);
let b = input.as_bytes();
let Some(open) = (0..b.len()).find(|&i| sc.code[i] && b[i] == b'(') else {
return true;
};
let mut depth = 0i32;
let mut close = None;
for (i, &byte) in b.iter().enumerate().skip(open) {
if !sc.code[i] {
continue;
}
match byte {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
close = Some(i);
break;
}
}
_ => {}
}
}
let Some(close) = close else { return true };
!(close + 1..b.len()).any(|i| sc.code[i] && !b[i].is_ascii_whitespace())
}
fn do_awaits_while(input: &str) -> bool {
if first_code_word(input).as_deref() != Some("do") {
return false;
}
let sc = lex::scan(input);
let b = input.as_bytes();
let mut depth = 0i32;
let mut saw_while = false;
let mut i = 0;
while i < b.len() {
if !sc.code[i] {
i += 1;
continue;
}
match b[i] {
b'(' | b'[' | b'{' => depth += 1,
b')' | b']' | b'}' => depth -= 1,
_ if depth == 0 && (b[i] == b'_' || b[i].is_ascii_alphabetic()) => {
let start = i;
while i < b.len() && sc.code[i] && (b[i] == b'_' || b[i].is_ascii_alphanumeric()) {
i += 1;
}
if start > 0 && &input[start..i] == "while" {
saw_while = true;
}
continue;
}
_ => {}
}
i += 1;
}
let ends_with_semicolon = (0..b.len())
.rev()
.find(|&i| sc.code[i] && !b[i].is_ascii_whitespace())
.is_some_and(|i| b[i] == b';');
!saw_while || !ends_with_semicolon
}
fn tag_definition_awaits_semicolon(input: &str) -> bool {
const TAGS: &[&str] = &["struct", "union", "enum"];
let sc = lex::scan(input);
if sc.depth != 0 || sc.unterminated {
return false;
}
let b = input.as_bytes();
let Some(first) = next_code_nonspace(b, &sc.code, 0) else {
return false;
};
let Some((first_word, first_end)) = code_identifier(input, &sc.code, first) else {
return false;
};
let Some(last) = (0..b.len())
.rev()
.find(|&i| sc.code[i] && !b[i].is_ascii_whitespace())
else {
return false;
};
if b[last] == b';' {
return false;
}
if TAGS.contains(&first_word) {
return tag_word_is_followed_by_brace(input, &sc.code, first_end);
}
if first_word != "typedef" {
return false;
}
let mut i = first_end;
while let Some(next) = next_code_nonspace(b, &sc.code, i) {
let Some((word, end)) = code_identifier(input, &sc.code, next) else {
i = next + 1;
continue;
};
if TAGS.contains(&word) && tag_word_is_followed_by_brace(input, &sc.code, end) {
return true;
}
i = end;
}
false
}
fn tag_word_is_followed_by_brace(input: &str, code: &[bool], word_end: usize) -> bool {
let b = input.as_bytes();
let Some(mut next) = next_code_nonspace(b, code, word_end) else {
return false;
};
if let Some((_, end)) = code_identifier(input, code, next) {
let Some(after_name) = next_code_nonspace(b, code, end) else {
return false;
};
next = after_name;
}
b.get(next) == Some(&b'{')
}
fn next_code_nonspace(bytes: &[u8], code: &[bool], mut from: usize) -> Option<usize> {
while from < bytes.len() && (!code[from] || bytes[from].is_ascii_whitespace()) {
from += 1;
}
(from < bytes.len()).then_some(from)
}
fn code_identifier<'a>(input: &'a str, code: &[bool], start: usize) -> Option<(&'a str, usize)> {
let b = input.as_bytes();
if !code.get(start).copied().unwrap_or(false)
|| !(b[start] == b'_' || b[start].is_ascii_alphabetic())
{
return None;
}
let mut end = start + 1;
while end < b.len() && code[end] && (b[end] == b'_' || b[end].is_ascii_alphanumeric()) {
end += 1;
}
Some((&input[start..end], end))
}
impl Validator for CValidator {
fn validate(&self, line: &str) -> ValidationResult {
if is_incomplete(line) {
ValidationResult::Incomplete
} else {
ValidationResult::Complete
}
}
}
impl Completer for CCompleter {
fn complete(&mut self, line: &str, pos: usize) -> Vec<Suggestion> {
let idents = self.idents.lock().expect("ident vocabulary");
let (start, candidates) = completion_candidates(line, pos, &idents);
candidates
.into_iter()
.map(|value| Suggestion {
value,
span: Span::new(start, pos),
..Suggestion::default()
})
.collect()
}
}
impl Prompt for CPrompt {
fn render_prompt_left(&self) -> Cow<'_, str> {
Cow::Owned(Ui::prompt_label(self.n))
}
fn render_prompt_right(&self) -> Cow<'_, str> {
Cow::Borrowed("")
}
fn render_prompt_indicator(&self, _mode: PromptEditMode) -> Cow<'_, str> {
Cow::Borrowed(" ")
}
fn render_prompt_multiline_indicator(&self) -> Cow<'_, str> {
Cow::Owned(" ".repeat(Ui::prompt_width(self.n)))
}
fn render_prompt_history_search_indicator(
&self,
history_search: PromptHistorySearch,
) -> Cow<'_, str> {
let prefix = match history_search.status {
PromptHistorySearchStatus::Passing => "",
PromptHistorySearchStatus::Failing => "failing ",
};
Cow::Owned(format!(
"({prefix}reverse-search: {}) ",
history_search.term
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn completes_magic_commands_at_line_start_only() {
let (start, c) = completion_candidates("%h", 2, &[]);
assert_eq!(start, 0);
assert!(c.contains(&"%help".to_string()) && c.contains(&"%header".to_string()));
assert_eq!(
completion_candidates("%he", 3, &[]).1,
vec!["%help", "%header"]
);
assert_eq!(completion_candidates("%cl", 3, &[]).1, vec!["%clear"]);
assert_eq!(completion_candidates("%ed", 3, &[]).1, vec!["%edit"]);
assert_eq!(completion_candidates("%ty", 3, &[]).1, vec!["%type"]);
let (_, c) = completion_candidates("a %h", 4, &[]);
assert!(c.is_empty());
}
#[test]
fn completes_keywords_and_session_names_merged() {
let idents = vec!["siz_total".to_string()];
let (start, c) = completion_candidates("x = siz", 7, &idents);
assert_eq!(start, 4);
assert_eq!(c, vec!["siz_total", "size_t", "sizeof"]);
}
#[test]
fn an_ambiguous_prefix_offers_every_match_not_just_the_first() {
let (start, c) = completion_candidates("str", 3, &[]);
assert_eq!(start, 0);
for expected in ["strcat", "strcmp", "strcpy", "strlen", "strstr"] {
assert!(
c.contains(&expected.to_string()),
"{expected} missing: {c:?}"
);
}
assert!(
c.windows(2).all(|w| w[0] < w[1]),
"candidates not sorted: {c:?}"
);
}
#[test]
fn no_candidates_inside_numbers_or_empty() {
assert!(completion_candidates("x = 0x1", 7, &[]).1.is_empty());
assert!(completion_candidates("", 0, &[]).1.is_empty());
}
#[test]
fn completes_only_names_in_code_position() {
assert!(
completion_candidates("printf(\"strc", 12, &[]).1.is_empty(),
"completed inside a string literal"
);
assert!(
completion_candidates("char c = 'a", 11, &[]).1.is_empty(),
"completed inside a char literal"
);
assert!(completion_candidates("strcat(", 7, &[]).1.is_empty());
assert!(
completion_candidates("strcat(s, str", 13, &[])
.1
.contains(&"strcat".to_string()),
"suppressed a real identifier in argument position"
);
}
#[test]
fn if_waits_for_else_or_blank_confirmation() {
let closed = "if (x) {\n puts(\"yes\");\n}";
assert!(!is_structurally_incomplete(closed));
assert!(is_incomplete(closed));
assert!(!is_incomplete(&format!("{closed}\n ")));
let with_else = format!("{closed}\nelse {{\n puts(\"no\");\n}}");
assert!(is_incomplete(&with_else));
assert!(!is_incomplete(&format!("{with_else}\n ")));
}
#[test]
fn control_header_and_do_while_are_structurally_incomplete() {
assert!(is_structurally_incomplete("if (x)"));
assert!(is_structurally_incomplete("for (int i = 0; i < 2; ++i)"));
assert!(is_structurally_incomplete("do { puts(\"x\"); }"));
assert!(!is_structurally_incomplete(
"do { puts(\"x\"); } while (0);"
));
}
#[test]
fn conditional_preprocessor_group_waits_for_endif() {
assert!(is_structurally_incomplete("#if FEATURE"));
assert!(is_structurally_incomplete("#if FEATURE\n#if INNER\n#endif"));
assert!(!is_structurally_incomplete("#if 0\n{\n#endif"));
assert!(!is_structurally_incomplete("#if FEATURE\n#endif"));
}
#[test]
fn function_submits_at_its_closing_brace() {
let function = "int f(void)\n{\n return 1;\n}";
assert!(!is_structurally_incomplete(function));
assert!(!is_incomplete(function));
assert!(!is_incomplete("struct P *make(void) {\n return 0;\n}"));
assert!(!is_incomplete("while (0) {\n}"));
assert!(!is_incomplete("do {\n} while (0);"));
}
#[test]
fn braced_tag_definitions_wait_for_their_semicolon() {
let definition = "struct P { int x; int y; }";
assert!(is_structurally_incomplete(definition));
assert!(is_incomplete(definition));
assert!(!is_incomplete(&format!("{definition};")));
assert!(is_incomplete("typedef union { int i; double d; } Value"));
assert!(is_incomplete("enum Color { RED, GREEN }"));
assert!(!is_incomplete("(struct P){ 1, 2 }"));
assert!(!is_incomplete(
"struct P make(void) { return (struct P){0}; }"
));
}
#[test]
fn completer_maps_candidates_to_reedline_spans() {
let mut completer = CCompleter::new(Arc::new(Mutex::new(vec!["siz_total".to_string()])));
let suggestions = completer.complete("x = siz", 7);
assert_eq!(
suggestions.iter().map(|s| &s.value).collect::<Vec<_>>(),
["siz_total", "size_t", "sizeof"]
);
assert!(
suggestions.iter().all(|s| s.span == Span::new(4, 7)),
"completion spans must cover the typed prefix: {suggestions:?}"
);
}
}