Initial commit for fern
This commit is contained in:
113
src/prompt/highlight.rs
Normal file
113
src/prompt/highlight.rs
Normal 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
46
src/prompt/mod.rs
Normal 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
90
src/prompt/readline.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user