lumesh/
repl.rs

1use rustyline::highlight::CmdKind;
2use rustyline::validate::ValidationResult;
3use rustyline::{
4    Changeset,
5    Editor,
6    // Event::KeySeq,
7    Helper,
8    KeyEvent,
9    completion::{Completer, FilenameCompleter, Pair},
10    config::CompletionType,
11    error::ReadlineError,
12    highlight::Highlighter,
13    hint::{Hinter, HistoryHinter},
14    history::FileHistory,
15    line_buffer::LineBuffer,
16    validate::Validator,
17};
18use rustyline::{Cmd, EditMode, Modifiers, Movement};
19
20use common_macros::hash_set;
21use std::borrow::Cow;
22use std::collections::{HashMap, HashSet};
23use std::path::PathBuf;
24
25use crate::Expression;
26use crate::ai::{AIClient, MockAIClient, init_ai};
27use crate::cmdhelper::{
28    PATH_COMMANDS, should_trigger_cmd_completion, should_trigger_path_completion,
29};
30use crate::expression::alias::get_alias_tips;
31use crate::keyhandler::{LumeAbbrHandler, LumeKeyHandler, LumeMoveHandler};
32use crate::modules::get_builtin_tips;
33use crate::syntax::{get_ayu_dark_theme, get_dark_theme, get_light_theme, get_merged_theme};
34
35use crate::runtime::check;
36use crate::{Environment, highlight, parse_and_eval, prompt::get_prompt_engine};
37
38// use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
39
40use std::sync::{Arc, Mutex};
41
42// ANSI 转义码
43const DEFAULT: &str = "";
44const GREEN_BOLD: &str = "\x1b[1;32m";
45// const GRAY: &str = "\x1b[38;5;246m";
46// const GRAY2: &str = "\x1b[38;5;249m";
47// const RED: &str = "\x1b[31m";
48const RESET: &str = "\x1b[0m";
49// 使用 Arc<Mutex> 包装编辑器
50
51pub fn run_repl(env: &mut Environment) {
52    // state::register_signal_handler();
53
54    match env.get("LUME_WELCOME") {
55        Some(wel) => {
56            println!("{wel}");
57            env.undefine("LUME_WELCOME");
58        }
59        _ => println!("Welcome to Lumesh {}", env!("CARGO_PKG_VERSION")),
60    }
61
62    // init_config(env);
63    //
64    let no_history = match env.get("LUME_NO_HISTORY") {
65        Some(Expression::Boolean(t)) => {
66            env.undefine("LUME_NO_HISTORY");
67            t
68        }
69        _ => false,
70    };
71    let history_file = match env.get("LUME_HISTORY_FILE") {
72        Some(hf) => hf.to_string(),
73        _ => {
74            let c_dir = match dirs::cache_dir() {
75                Some(c) => c,
76                _ => PathBuf::new(),
77            };
78            #[cfg(unix)]
79            let path = c_dir.join(".lume_history");
80            #[cfg(windows)]
81            let path = c_dir.join("lume_history.log");
82            if !path.exists() {
83                match std::fs::File::create(&path) {
84                    Ok(_) => {}
85                    Err(e) => eprint!("Failed to create cache directory: {e}"),
86                }
87            }
88            path.into_os_string().into_string().unwrap()
89        }
90    };
91    // ai config
92    let ai_config = env.get("LUME_AI_CONFIG");
93    env.undefine("LUME_AI_CONFIG");
94    let vi_mode = match env.get("LUME_VI_MODE") {
95        Some(Expression::Boolean(true)) => {
96            env.undefine("LUME_AI_CONFIG");
97            true
98        }
99        _ => false,
100    };
101
102    // theme
103    let theme_base = env.get("LUME_THEME");
104    env.undefine("LUME_THEME");
105    let theme = match theme_base {
106        Some(Expression::String(t)) => match t.as_ref() {
107            "light" => get_light_theme(),
108            "ayu_dark" => get_ayu_dark_theme(),
109            _ => get_dark_theme(),
110        },
111        _ => get_dark_theme(),
112    };
113
114    let theme_config = env.get("LUME_THEME_CONFIG");
115    env.undefine("LUME_THEME_CONFIG");
116    let theme_merged = match theme_config {
117        Some(Expression::Map(m)) => get_merged_theme(theme, m.as_ref()),
118        _ => theme,
119    };
120
121    // 使用 Arc<Mutex> 保护编辑器
122    let rl = Arc::new(Mutex::new(new_editor(ai_config, vi_mode, theme_merged)));
123
124    match rl.lock().unwrap().load_history(&history_file) {
125        Ok(_) => {}
126        Err(e) => println!("No previous history {e}"),
127    }
128
129    let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
130
131    // 设置信号处理 (Unix 系统)
132    // #[cfg(unix)]
133    // {
134    let rl_clone = Arc::clone(&rl);
135    let running_clone = Arc::clone(&running);
136    if no_history {
137        ctrlc::set_handler(move || {
138            running_clone.store(false, std::sync::atomic::Ordering::SeqCst);
139            std::process::exit(0);
140        })
141        .expect("Error setting Ctrl-C handler");
142    } else {
143        let hist = history_file.clone();
144        ctrlc::set_handler(move || {
145            running_clone.store(false, std::sync::atomic::Ordering::SeqCst);
146            let _ = rl_clone.lock().unwrap().save_history(&hist);
147            std::process::exit(0);
148        })
149        .expect("Error setting Ctrl-C handler");
150    }
151    // }
152
153    // =======key binding=======
154    // 1. edit
155    rl.lock()
156        .unwrap()
157        .bind_sequence(KeyEvent::ctrl('j'), LumeMoveHandler::new(1));
158    rl.lock()
159        .unwrap()
160        .bind_sequence(KeyEvent::alt('j'), LumeMoveHandler::new(0));
161    rl.lock().unwrap().bind_sequence(
162        KeyEvent::ctrl('o'),
163        Cmd::Replace(Movement::WholeBuffer, Some(String::from(""))),
164    );
165    let hotkey_sudo = match env.get("LUME_SUDO_CMD") {
166        Some(s) => {
167            env.undefine("LUME_SUDO_CMD");
168            s.to_string()
169        }
170        _ => "sudo".to_string(),
171    };
172    rl.lock().unwrap().bind_sequence(
173        KeyEvent::alt('s'),
174        Cmd::Replace(Movement::BeginningOfLine, Some(hotkey_sudo)),
175    );
176
177    // 2. custom hotkey
178    let hotkey_modifier = env.get("LUME_HOT_MODIFIER");
179    env.undefine("LUME_HOT_MODIFIER");
180    let modifier: u8 = match hotkey_modifier {
181        Some(Expression::Integer(bits)) => {
182            // if bits == 0 {
183            //     0
184            // } else
185            if (bits as u8 & (Modifiers::CTRL | Modifiers::ALT | Modifiers::SHIFT).bits()) == 0 {
186                eprintln!("invalid LUME_HOT_MODIFIER {bits}");
187                4
188            } else {
189                bits as u8
190            }
191        }
192        _ => 4,
193    };
194
195    let hotkey_config = env.get("LUME_HOT_KEYS");
196    env.undefine("LUME_HOT_KEYS");
197
198    if let Some(Expression::Map(keys)) = hotkey_config {
199        let mut rl_unlocked = rl.lock().unwrap();
200        for (k, cmd) in keys.iter() {
201            if let Some(c) = k.chars().next() {
202                rl_unlocked.bind_sequence(
203                    // KeySeq(vec![
204                    //     KeyEvent::alt('z'),
205                    KeyEvent::new(c, Modifiers::from_bits_retain(modifier)),
206                    // ]),
207                    LumeKeyHandler::new(cmd.to_string()),
208                );
209            }
210        }
211    }
212    // 3. abbr
213    let abbr = env.get("LUME_ABBREVIATIONS");
214    env.undefine("LUME_ABBREVIATIONS");
215
216    if let Some(Expression::Map(ab)) = abbr {
217        let abmap = ab
218            .iter()
219            .map(|m| (m.0.to_string(), m.1.to_string()))
220            .collect::<HashMap<String, String>>();
221        rl.lock().unwrap().bind_sequence(
222            KeyEvent::new(' ', Modifiers::NONE),
223            LumeAbbrHandler::new(abmap),
224        );
225    }
226    // =======key binding end=======
227
228    // main loop
229    let pe = get_prompt_engine(
230        env.get("LUME_PROMPT_SETTINGS"),
231        env.get("LUME_PROMPT_TEMPLATE"),
232    );
233    env.undefine("LUME_PROMPT_SETTINGS");
234    env.undefine("LUME_PROMPT_TEMPLATE");
235
236    // let mut repl_env = env.fork();
237    while running.load(std::sync::atomic::Ordering::SeqCst) {
238        let prompt = pe.get_prompt();
239
240        // 在锁的保护下执行 readline
241        let line = match rl.lock().unwrap().readline(prompt.as_str()) {
242            Ok(line) => line,
243            Err(ReadlineError::Interrupted) => {
244                println!("CTRL-C");
245                // state::set_signal(); // 更新共享状态
246                continue;
247            }
248            // Err(ReadlineError::Signal(sig)) => {
249            //     if sig == rustyline::error::Signal::Interrupt {
250            //         println!("[Interrupt]");
251            //         state::set_signal(); // 更新共享状态
252            //     }
253            //     continue;
254            // }
255            Err(ReadlineError::Eof) => {
256                println!("CTRL-D");
257                continue;
258            }
259            Err(err) => {
260                println!("Error: {err:?}");
261                break;
262            }
263        };
264
265        match line.trim() {
266            "" => {}
267            "exit" => break,
268            "history" => {
269                for (i, entry) in rl.lock().unwrap().history().iter().enumerate() {
270                    println!("{}{}:{} {}", GREEN_BOLD, i + 1, RESET, entry);
271                }
272            }
273            _ => {
274                if parse_and_eval(&line, env)
275                // && !no_history
276                {
277                    match rl.lock().unwrap().add_history_entry(&line) {
278                        Ok(_) => {}
279                        Err(e) => eprintln!("add history err: {e}"),
280                    };
281                }
282            }
283        }
284    }
285
286    // 保存历史记录
287    if !no_history {
288        match rl.lock().unwrap().save_history(&history_file) {
289            Ok(_) => {}
290            Err(e) => eprintln!("save history err: {e}"),
291        };
292    }
293}
294
295// 确保 helper 也是线程安全的
296#[derive(Clone)]
297struct LumeHelper {
298    completer: Arc<FilenameCompleter>,
299    hinter: Arc<HistoryHinter>,
300    highlighter: Arc<SyntaxHighlighter>,
301    ai_client: Option<Arc<MockAIClient>>,
302    cmds: HashSet<String>,
303}
304
305fn new_editor(
306    ai_config: Option<Expression>,
307    vi_mode: bool,
308    theme: HashMap<String, String>,
309) -> Editor<LumeHelper, FileHistory> {
310    let config = rustyline::Config::builder()
311        .history_ignore_space(true)
312        .completion_type(CompletionType::Circular)
313        .edit_mode(if vi_mode {
314            EditMode::Vi
315        } else {
316            EditMode::Emacs
317        })
318        .history_ignore_dups(true)
319        .unwrap()
320        .build();
321
322    let mut rl = Editor::with_config(config).unwrap_or_else(|_| Editor::new().unwrap());
323    let ai = ai_config.map(|ai_cfg| Arc::new(init_ai(ai_cfg)));
324    // 预定义命令列表(带权重排序)
325    // TODO add builtin cmds
326    let mut cmds: HashSet<String> = hash_set! {
327        "cd ./".into(),
328        "ls -l --color ./".into(),
329        "clear".into(),
330        "rm ".into(),
331        "cp -r".into(),
332        "let ".into(),
333        "fn ".into(),
334        "if ".into(),
335        "else {".into(),
336        "match ".into(),
337        "while (".into(),
338        "for i in ".into(),
339        "loop {\n".into(),
340        "break".into(),
341        "return".into(),
342        "history".into(),
343        "del ".into(),
344        "use ".into(),
345    };
346    cmds.extend(get_builtin_tips());
347    cmds.extend(PATH_COMMANDS.lock().unwrap().iter().cloned());
348    cmds.extend(get_alias_tips());
349    let helper = LumeHelper {
350        completer: Arc::new(FilenameCompleter::new()),
351        hinter: Arc::new(HistoryHinter::new()),
352        highlighter: Arc::new(SyntaxHighlighter::new(theme)),
353        ai_client: ai,
354        cmds,
355    };
356    rl.set_helper(Some(helper));
357    rl
358}
359
360impl Helper for LumeHelper {}
361// impl LumeHelper {
362//     fn set_prompt(&mut self, prompt: impl ToString) {
363//         self.colored_prompt = prompt.to_string();
364//     }
365
366//     fn update_env(&mut self, env: &Environment) {
367//         self.env = env.clone();
368//     }
369// }
370
371impl Completer for LumeHelper {
372    type Candidate = Pair;
373
374    fn complete(
375        &self,
376        line: &str,
377        pos: usize,
378        ctx: &rustyline::Context<'_>,
379    ) -> Result<(usize, Vec<Self::Candidate>), ReadlineError> {
380        match self.detect_completion_type(line, pos) {
381            LumeCompletionType::Path => self.path_completion(line, pos, ctx),
382            LumeCompletionType::Command => self.cmd_completion(line, pos),
383            LumeCompletionType::AI => self.ai_completion(line, pos),
384            LumeCompletionType::None => Ok((pos, Vec::new())),
385        }
386    }
387
388    fn update(&self, line: &mut LineBuffer, start: usize, elected: &str, cl: &mut Changeset) {
389        // 直接使用标准替换逻辑
390        let end = line.pos();
391        line.replace(start..end, elected, cl);
392    }
393}
394
395// 扩展实现
396impl LumeHelper {
397    /// 新增补全类型检测
398    fn detect_completion_type(&self, line: &str, pos: usize) -> LumeCompletionType {
399        if should_trigger_path_completion(line, pos) {
400            LumeCompletionType::Path
401        } else if should_trigger_cmd_completion(line, pos) {
402            LumeCompletionType::Command
403        } else if self.should_trigger_ai(line) {
404            LumeCompletionType::AI
405        } else {
406            LumeCompletionType::None
407        }
408    }
409
410    /// 路径补全逻辑
411    fn path_completion(
412        &self,
413        line: &str,
414        pos: usize,
415        ctx: &rustyline::Context<'_>,
416    ) -> Result<(usize, Vec<Pair>), ReadlineError> {
417        self.completer.complete(line, pos, ctx)
418    }
419
420    /// 命令补全逻辑
421    fn cmd_completion(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>), ReadlineError> {
422        // 计算起始位置
423        let input = &line[..pos];
424        let start = input.rfind(' ').map(|i| i + 1).unwrap_or(0);
425        let prefix = &input[start..];
426        // dbg!(&input, &start, &prefix);
427        // 过滤以prefix开头的命令
428        let cpl_color = self
429            .highlighter
430            .theme
431            .get("completion_cmd")
432            .map_or(DEFAULT, |c| c.as_str());
433        let mut candidates: Vec<Pair> = self
434            .cmds
435            .iter()
436            .filter(|cmd| cmd.starts_with(prefix))
437            .map(|cmd| {
438                // dbg!(&cmd);
439                Pair {
440                    display: format!("{cpl_color}{cmd}{RESET}"),
441                    replacement: cmd.clone(),
442                }
443            })
444            .collect();
445        // 按显示名称的长度升序排序,较短的优先
446        candidates.sort_by(|a, b| a.display.len().cmp(&b.display.len()));
447
448        Ok((start, candidates))
449    }
450
451    /// AI 补全逻辑
452    fn ai_completion(&self, line: &str, pos: usize) -> Result<(usize, Vec<Pair>), ReadlineError> {
453        let prompt = line.trim();
454        let suggestion = self
455            .ai_client
456            .as_ref()
457            .and_then(|ai| ai.complete(prompt).ok())
458            .unwrap_or_default();
459
460        let pair = Pair {
461            display: format!(
462                "{}{}{}",
463                self.highlighter
464                    .theme
465                    .get("completion_ai")
466                    .map_or(DEFAULT, |c| c.as_str()),
467                suggestion,
468                RESET
469            ), // 保持ANSI颜色
470            replacement: suggestion,
471        };
472        Ok((pos, vec![pair]))
473    }
474
475    /// AI补全触发条件
476    fn should_trigger_ai(&self, line: &str) -> bool {
477        self.ai_client.is_some() && line.split_whitespace().count() > 1
478    }
479}
480
481// 扩展补全类型枚举
482#[derive(Debug, PartialEq)]
483enum LumeCompletionType {
484    Path,
485    Command,
486    AI,
487    None,
488}
489
490impl Validator for LumeHelper {
491    fn validate(
492        &self,
493        ctx: &mut rustyline::validate::ValidationContext<'_>,
494    ) -> rustyline::Result<ValidationResult> {
495        // self.validator.validate(ctx)
496        // check_balanced(ctx.input())
497        if ctx.input().ends_with("\n\n") || check(ctx.input()) {
498            return Ok(ValidationResult::Valid(None));
499        };
500        Ok(ValidationResult::Incomplete)
501    }
502}
503
504impl Highlighter for LumeHelper {
505    fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool {
506        self.highlighter.highlight_char(line, pos, kind)
507    }
508    fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> {
509        self.highlighter.highlight(line, pos)
510    }
511    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
512        self.highlighter.highlight_hint(hint)
513    }
514    // fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
515    //     &'s self,
516    //     prompt: &'p str,
517    //     default: bool,
518    // ) -> Cow<'b, str> {
519    //     self.highlighter.highlight_prompt(prompt, default)
520    // }
521    // fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool {
522    //     MatchingBracketHighlighter::highlight_char(line, pos, kind)
523    // }
524}
525
526// struct InputValidator;
527
528// impl Validator for InputValidator {
529//     fn validate(
530//         &self,
531//         ctx: &mut rustyline::validate::ValidationContext<'_>,
532//     ) -> rustyline::Result<ValidationResult> {
533//         // dbg!(
534//         //     ctx.input(),
535//         //     ctx.input().ends_with("\n\n"),
536//         //     check_balanced(ctx.input()),
537//         //     check(ctx.input())
538//         // );
539
540//     }
541// }
542// 实现历史提示
543// Define a concrete Hint type for HistoryHinter
544// pub struct HistoryHint(String);
545
546// impl Hint for HistoryHint {
547//     fn display(&self) -> &str {
548//         &self.0
549//     }
550
551//     fn completion(&self) -> Option<&str> {
552//         Some(&self.0)
553//     }
554// }
555impl Hinter for LumeHelper {
556    type Hint = String;
557
558    fn hint(&self, line: &str, pos: usize, ctx: &rustyline::Context<'_>) -> Option<String> {
559        // 提取光标前的连续非分隔符片段
560        let mut segment = String::new();
561        if !line.is_empty() {
562            for (i, ch) in line.chars().enumerate() {
563                // 扩展分隔符列表(根据需要调整)
564                if matches!(ch, ';' | '|' | '(' | '{' | '`' | '\n') {
565                    segment.clear();
566                } else if segment.is_empty() && ch.is_ascii_whitespace() {
567                } else {
568                    segment.push(ch);
569                }
570                if i == pos {
571                    break;
572                }
573            }
574        }
575        // 仅当有有效片段时进行匹配
576        if !segment.is_empty() {
577            // 按权重排序匹配结果
578            let mut matches: Vec<_> = self
579                .cmds
580                .iter()
581                .filter(|cmd| cmd.starts_with(&segment))
582                .collect();
583
584            // 权重降序, 较短的优先
585            matches.sort_by(|a, b| a.len().cmp(&b.len()));
586            // dbg!(&matches);
587            if let Some(matched) = matches.first() {
588                let suffix = &matched[segment.len()..];
589                // dbg!(&segment, &segment.len(), &matched, &suffix, &suffix.len());
590                if !suffix.is_empty() {
591                    return Some(suffix.to_string());
592                }
593            }
594        }
595
596        // 无匹配时回退默认提示
597        self.hinter.hint(line, pos, ctx)
598    }
599}
600
601struct SyntaxHighlighter {
602    theme: HashMap<String, String>,
603}
604
605impl SyntaxHighlighter {
606    pub fn new(theme: HashMap<String, String>) -> Self {
607        Self { theme }
608    }
609}
610impl Highlighter for SyntaxHighlighter {
611    fn highlight_char(&self, line: &str, pos: usize, kind: CmdKind) -> bool {
612        let _s = (line, pos, kind);
613        kind != CmdKind::MoveCursor
614    }
615
616    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
617        let mut parts = line.splitn(2, |c: char| c.is_whitespace());
618        let cmd = parts.next().unwrap_or("");
619        let rest = parts.next().unwrap_or("");
620        if cmd.is_empty() {
621            return Cow::Borrowed(line);
622        }
623
624        let (color, is_valid) = if is_valid_command(cmd) {
625            (
626                self.theme
627                    .get("command_valid")
628                    .map_or(DEFAULT, |c| c.as_str()),
629                true,
630            )
631        // } else if !cmd.is_empty() {
632        //     (RED, false)
633        } else {
634            // 无命令,直接返回语法高亮
635            return Cow::Owned(highlight(line, &self.theme));
636        };
637
638        // 高亮命令部分,剩余部分调用 syntax_highlight
639        let highlighted_rest = highlight(rest, &self.theme);
640        let colored_line = if is_valid {
641            format!("{color}{cmd}{RESET} {highlighted_rest}")
642        } else {
643            format!("{color}{cmd}{RESET} {highlighted_rest}")
644        };
645        Cow::Owned(colored_line)
646    }
647
648    fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> {
649        // dbg!(&hint);
650        // 如果提示为空或已经包含颜色代码,直接返回借用
651        if hint.is_empty() || hint.contains('\x1b') {
652            return Cow::Borrowed(hint);
653        }
654        Cow::Owned(format!(
655            "{}{}{}",
656            self.theme.get("hint").map_or(DEFAULT, |c| c.as_str()),
657            hint,
658            RESET
659        ))
660    }
661
662    // fn highlight_prompt<'b, 's: 'b, 'p: 'b>(
663    //     &'s self,
664    //     prompt: &'p str,
665    //     default: bool,
666    // ) -> Cow<'b, str> {
667    //     // dbg!(&prompt);
668    //     let _ = default;
669    //     // Borrowed(prompt)
670    //     Cow::Owned(format!("{}{}{}", GREEN_BOLD, prompt, RESET))
671    // }
672}
673// fn check_balanced(input: &str) -> bool {
674//     let mut stack = Vec::new();
675//     for c in input.chars() {
676//         match c {
677//             '(' | '[' | '{' => stack.push(c),
678//             ')' => {
679//                 if stack.pop() != Some('(') {
680//                     return false;
681//                 }
682//             }
683//             ']' => {
684//                 if stack.pop() != Some('[') {
685//                     return false;
686//                 }
687//             }
688//             '}' => {
689//                 if stack.pop() != Some('{') {
690//                     return false;
691//                 }
692//             }
693//             _ => {}
694//         }
695//     }
696//     stack.is_empty()
697// }
698
699// fn readline(prompt: impl ToString, rl: &mut Editor<LumeHelper, FileHistory>) -> String {
700//     let prompt = prompt.to_string();
701//     loop {
702//         // if let Some(helper) = rl.helper_mut() {
703//         //     helper.set_prompt(&prompt);
704//         // }
705
706//         match rl.readline(&strip_ansi_escapes(&prompt)) {
707//             Ok(line) => return line,
708//             Err(ReadlineError::Interrupted) => return String::new(),
709//             Err(ReadlineError::Eof) => exit(0),
710//             Err(err) => eprintln!("Error: {:?}", err),
711//         }
712//     }
713// }
714
715// pub fn read_user_input(prompt: impl ToString) -> String {
716//     let mut rl = new_editor(None);
717//     readline(prompt, &mut rl)
718// }
719
720fn is_valid_command(cmd: &str) -> bool {
721    PATH_COMMANDS.lock().unwrap().contains(cmd)
722}