Initial commit for fern

This commit is contained in:
2025-03-02 16:26:28 -05:00
parent 706767ce4c
commit e7a84f1edd
40 changed files with 5281 additions and 0 deletions

113
src/prompt/highlight.rs Normal file
View File

@@ -0,0 +1,113 @@
use rustyline::highlight::Highlighter;
use sys::get_bin_path;
use crate::prelude::*;
use super::readline::SynHelper;
impl<'a> Highlighter for SynHelper<'a> {
fn highlight<'l>(&self, line: &'l str, pos: usize) -> std::borrow::Cow<'l, str> {
let mut result = String::new();
let mut tokens = Lexer::new(Rc::new(line.to_string())).lex().into_iter();
let mut is_command = true;
let mut in_array = false;
while let Some(token) = tokens.next() {
let raw = token.to_string();
match token.rule() {
TkRule::Comment => {
let styled = style_text(&raw, Style::BrightBlack);
result.push_str(&styled);
}
TkRule::ErrPipeOp |
TkRule::OrOp |
TkRule::AndOp |
TkRule::PipeOp |
TkRule::RedirOp |
TkRule::BgOp => {
is_command = true;
let styled = style_text(&raw, Style::Cyan);
result.push_str(&styled);
}
TkRule::Keyword => {
if &raw == "for" {
in_array = true;
}
let styled = style_text(&raw, Style::Yellow);
result.push_str(&styled);
}
TkRule::Subshell => {
let body = &raw[1..raw.len() - 1];
let highlighted = self.highlight(body, 0).to_string();
let styled_o_paren = style_text("(", Style::BrightBlue);
let styled_c_paren = style_text(")", Style::BrightBlue);
let rebuilt = format!("{styled_o_paren}{highlighted}{styled_c_paren}");
is_command = false;
result.push_str(&rebuilt);
}
TkRule::Ident => {
if in_array {
if &raw == "in" {
let styled = style_text(&raw, Style::Yellow);
result.push_str(&styled);
} else {
let styled = style_text(&raw, Style::Magenta);
result.push_str(&styled);
}
} else if is_command {
if get_bin_path(&token.to_string(), self.shenv).is_some() ||
self.shenv.logic().get_alias(&raw).is_some() ||
self.shenv.logic().get_function(&raw).is_some() ||
BUILTINS.contains(&raw.as_str()) {
let styled = style_text(&raw, Style::Green);
result.push_str(&styled);
} else {
let styled = style_text(&raw, Style::Red | Style::Bold);
result.push_str(&styled);
}
is_command = false;
} else {
result.push_str(&raw);
}
}
TkRule::Sep => {
is_command = true;
in_array = false;
result.push_str(&raw);
}
_ => {
result.push_str(&raw);
}
}
}
std::borrow::Cow::Owned(result)
}
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
&'s self,
prompt: &'p str,
default: bool,
) -> std::borrow::Cow<'b, str> {
let _ = default;
std::borrow::Cow::Borrowed(prompt)
}
fn highlight_hint<'h>(&self, hint: &'h str) -> std::borrow::Cow<'h, str> {
std::borrow::Cow::Borrowed(hint)
}
fn highlight_candidate<'c>(
&self,
candidate: &'c str, // FIXME should be Completer::Candidate
completion: rustyline::CompletionType,
) -> std::borrow::Cow<'c, str> {
let _ = completion;
std::borrow::Cow::Borrowed(candidate)
}
fn highlight_char(&self, line: &str, pos: usize, kind: rustyline::highlight::CmdKind) -> bool {
let _ = (line, pos, kind);
true
}
}

46
src/prompt/mod.rs Normal file
View File

@@ -0,0 +1,46 @@
use crate::prelude::*;
use readline::SynHelper;
use rustyline::{config::Configurer, history::DefaultHistory, ColorMode, CompletionType, Config, DefaultEditor, EditMode, Editor};
pub mod readline;
pub mod highlight;
fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor<SynHelper<'a>, DefaultHistory> {
let hist_path = std::env::var("FERN_HIST").unwrap_or_default();
let mut config = Config::builder()
.max_history_size(1000).unwrap()
.history_ignore_dups(true).unwrap()
.completion_prompt_limit(100)
.edit_mode(EditMode::Vi)
.color_mode(ColorMode::Enabled)
.tab_stop(2)
.build();
let mut editor = Editor::with_config(config).unwrap();
editor.set_completion_type(CompletionType::List);
editor.set_helper(Some(SynHelper::new(shenv)));
if !hist_path.is_empty() {
editor.load_history(&PathBuf::from(hist_path)).unwrap();
}
editor
}
pub fn read_line<'a>(shenv: &'a mut ShEnv) -> ShResult<String> {
log!(TRACE, "Entering prompt");
let prompt = &style_text("$ ", Style::Green | Style::Bold);
let mut editor = init_rl(shenv);
match editor.readline(prompt) {
Ok(line) => Ok(line),
Err(rustyline::error::ReadlineError::Eof) => {
kill(Pid::this(), Signal::SIGQUIT)?;
Ok(String::new())
}
Err(rustyline::error::ReadlineError::Interrupted) => {
Ok(String::new())
}
Err(e) => {
log!(ERROR, e);
Err(e.into())
}
}
}

90
src/prompt/readline.rs Normal file
View File

@@ -0,0 +1,90 @@
use rustyline::{completion::{Completer, FilenameCompleter}, highlight::Highlighter, hint::{Hint, Hinter}, history::{History, SearchDirection}, validate::{ValidationResult, Validator}, Helper};
use crate::prelude::*;
pub struct SynHelper<'a> {
file_comp: FilenameCompleter,
pub shenv: &'a mut ShEnv,
pub commands: Vec<String>
}
impl<'a> Helper for SynHelper<'a> {}
impl<'a> SynHelper<'a> {
pub fn new(shenv: &'a mut ShEnv) -> Self {
Self {
file_comp: FilenameCompleter::new(),
shenv,
commands: vec![]
}
}
pub fn hist_search(&self, term: &str, hist: &dyn History) -> Option<String> {
let limit = hist.len();
let mut latest_match = None;
for i in 0..limit {
if let Some(hist_entry) = hist.get(i, SearchDirection::Forward).ok()? {
if hist_entry.entry.starts_with(term) {
latest_match = Some(hist_entry.entry.into_owned())
}
}
}
latest_match
}
}
impl<'a> Validator for SynHelper<'a> {
fn validate(&self, ctx: &mut rustyline::validate::ValidationContext) -> rustyline::Result<rustyline::validate::ValidationResult> {
Ok(ValidationResult::Valid(None))
}
}
impl<'a> Completer for SynHelper<'a> {
type Candidate = String;
fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
Ok((0,vec![]))
}
}
pub struct SynHint {
text: String,
styled: String
}
impl SynHint {
pub fn new(text: String) -> Self {
let styled = style_text(&text, Style::BrightBlack);
Self { text, styled }
}
pub fn empty() -> Self {
Self { text: String::new(), styled: String::new() }
}
}
impl Hint for SynHint {
fn display(&self) -> &str {
&self.styled
}
fn completion(&self) -> Option<&str> {
if !self.text.is_empty() {
Some(&self.text)
} else {
None
}
}
}
impl<'a> Hinter for SynHelper<'a> {
type Hint = SynHint;
fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<Self::Hint> {
return Some(SynHint::empty());
if line.is_empty() {
return None
}
let history = ctx.history();
let result = self.hist_search(line, history)?;
let window = result[line.len()..].to_string();
Some(SynHint::new(window))
}
}