artichoke_readline/
lib.rs

1#![warn(clippy::all, clippy::pedantic, clippy::undocumented_unsafe_blocks)]
2#![allow(
3    clippy::let_underscore_untyped,
4    reason = "https://github.com/rust-lang/rust-clippy/pull/10442#issuecomment-1516570154"
5)]
6#![allow(
7    clippy::question_mark,
8    reason = "https://github.com/rust-lang/rust-clippy/issues/8281"
9)]
10#![allow(clippy::manual_let_else, reason = "manual_let_else was very buggy on release")]
11#![allow(clippy::missing_errors_doc, reason = "A lot of existing code fails this lint")]
12#![allow(
13    clippy::unnecessary_lazy_evaluations,
14    reason = "https://github.com/rust-lang/rust-clippy/issues/8109"
15)]
16#![cfg_attr(
17    test,
18    allow(clippy::non_ascii_literal, reason = "tests sometimes require UTF-8 string content")
19)]
20#![allow(unknown_lints)]
21#![warn(
22    missing_copy_implementations,
23    missing_debug_implementations,
24    missing_docs,
25    rust_2024_compatibility,
26    trivial_casts,
27    trivial_numeric_casts,
28    unused_qualifications,
29    variant_size_differences
30)]
31#![forbid(unsafe_code)]
32// Enable feature callouts in generated documentation:
33// https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html
34//
35// This approach is borrowed from tokio.
36#![cfg_attr(docsrs, feature(doc_cfg))]
37#![cfg_attr(docsrs, feature(doc_alias))]
38
39//! Helpers for integrating REPLs with GNU Readline.
40//!
41//! This crate can be used to parse Readline editing mode from the standard set
42//! of GNU Readline config files.
43//!
44//! # Examples
45//!
46//! ```
47//! use artichoke_readline::{get_readline_edit_mode, rl_read_init_file, EditMode};
48//!
49//! if let Some(config) = rl_read_init_file() {
50//!     if matches!(get_readline_edit_mode(config), Some(EditMode::Vi)) {
51//!         println!("You have chosen wisely");
52//!     }
53//! }
54//! ```
55//!
56//! # Crate Features
57//!
58//! The **rustyline** feature (enabled by default) adds trait implementations to
59//! allow [`EditMode`] to interoperate with the corresponding enum in the
60//! `rustyline` crate.
61
62// Ensure code blocks in `README.md` compile
63#[cfg(doctest)]
64#[doc = include_str!("../README.md")]
65mod readme {}
66
67use std::env;
68use std::fs;
69use std::path::PathBuf;
70
71use bstr::ByteSlice;
72
73/// Readline editing mode.
74#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq)]
75pub enum EditMode {
76    /// Emacs keymap.
77    ///
78    /// Emacs is the default keymap.
79    #[default]
80    Emacs,
81    /// Vi keymap.
82    Vi,
83}
84
85#[cfg(feature = "rustyline")]
86#[cfg_attr(docsrs, doc(cfg(feature = "rustyline")))]
87impl From<EditMode> for rustyline::config::EditMode {
88    fn from(mode: EditMode) -> Self {
89        match mode {
90            EditMode::Emacs => Self::Emacs,
91            EditMode::Vi => Self::Vi,
92        }
93    }
94}
95
96/// Read inputrc contents according to the GNU Readline hierarchy of config
97/// files.
98///
99/// This function will try each file in the config hierarchy (with the addition
100/// of `%USERPROFILE%\_inputrc` on Windows). This function returns the contents
101/// of the first file that exists and is successfully read. If no config file is
102/// found, [`None`] is returned.
103///
104/// # Upstream Implementation
105///
106/// This routine is ported from GNU Readline's `rl_read_init_file` function as
107/// of commit [`7274faabe97ce53d6b464272d7e6ab6c1392837b`][upstream], which has
108/// the following documentation:
109///
110/// > Do key bindings from a file. If FILENAME is NULL it defaults to the first
111/// > non-null filename from this list:
112/// >
113/// > 1. the filename used for the previous call
114/// > 2. the value of the shell variable `INPUTRC`
115/// > 3. `~/.inputrc`
116/// > 4. `/etc/inputrc`
117/// >
118/// > If the file existed and could be opened and read, 0 is returned, otherwise
119/// > errno is returned.
120///
121/// The routine is also documented in [section 8.3 of the bash manual][readline-man].
122///
123/// [upstream]: https://git.savannah.gnu.org/cgit/readline.git/tree/bind.c?h=7274faabe97ce53d6b464272d7e6ab6c1392837b#n1032
124/// [readline-man]: https://www.gnu.org/software/bash/manual/bash.html#Readline-Init-File
125#[must_use]
126pub fn rl_read_init_file() -> Option<Vec<u8>> {
127    if let Some(inputrc) = env::var_os("INPUTRC") {
128        return fs::read(inputrc).ok();
129    }
130
131    let home_dir = home_dir();
132    if let Some(ref home_dir) = home_dir {
133        let inputrc = home_dir.join(".inputrc");
134        if let Ok(content) = fs::read(inputrc) {
135            return Some(content);
136        }
137    }
138
139    if let Ok(content) = fs::read("/etc/inputrc") {
140        return Some(content);
141    }
142
143    if cfg!(windows) {
144        if let Some(home_dir) = home_dir {
145            let inputrc = home_dir.join("_inputrc");
146            if let Ok(content) = fs::read(inputrc) {
147                return Some(content);
148            }
149        }
150    }
151
152    None
153}
154
155#[cfg(not(any(unix, windows)))]
156fn home_dir() -> Option<PathBuf> {
157    None
158}
159
160#[cfg(unix)]
161fn home_dir() -> Option<PathBuf> {
162    // `std::env::home_dir` does not have problematic behavior on `unix`
163    // targets, which includes all apple target OSes and Redox. Per the docs:
164    //
165    // > Deprecated since 1.29.0: This function's behavior may be unexpected on
166    // > Windows. Consider using a crate from crates.io instead.
167    // >
168    // > -- https://doc.rust-lang.org/1.69.0/std/env/fn.home_dir.html
169    //
170    // Additionally, the `home` crate on crates.io, which is owned by the
171    // @rust-lang organization and used in Rustup and Cargo, uses `std::env::home_dir`
172    // to implement `home::home_dir` on `unix` and `target_os = "redox"` targets:
173    //
174    // https://docs.rs/home/0.5.5/src/home/lib.rs.html#71-75
175    #[allow(deprecated)]
176    env::home_dir()
177}
178
179#[cfg(windows)]
180fn home_dir() -> Option<PathBuf> {
181    use known_folders::{KnownFolder, get_known_folder_path};
182
183    get_known_folder_path(KnownFolder::Profile)
184}
185
186/// Parse readline editing mode from the given byte content, which should be
187/// the contents of an inputrc config file.
188///
189/// See [`rl_read_init_file`] for how to retrieve the contents of the effective
190/// inputrc file.
191///
192/// This function looks for the `editing-mode` variable in the given inputrc
193/// bytes. Per the [manual, section 8.3.1]:
194///
195/// > `editing-mode`
196/// >
197/// > The `editing-mode` variable controls which default set of key bindings is
198/// > used. By default, Readline starts up in Emacs editing mode, where the
199/// > keystrokes are most similar to Emacs. This variable can be set to either
200/// > '`emacs`' or '`vi`'.
201///
202/// # Examples
203///
204/// ```
205/// use artichoke_readline::{get_readline_edit_mode, EditMode};
206///
207/// const INPUTRC: &str = "
208/// # Vi mode
209/// set editing-mode vi
210/// ";
211///
212/// assert_eq!(get_readline_edit_mode(INPUTRC), Some(EditMode::Vi));
213/// ```
214///
215/// # Implementation Notes
216///
217/// This parser does not support GNU Readline's [conditional init constructs]
218/// (the `$if` construct).
219///
220/// [manual]: https://www.gnu.org/software/bash/manual/bash.html#Readline-Init-File-Syntax
221/// [conditional init constructs]: https://www.gnu.org/software/bash/manual/bash.html#Conditional-Init-Constructs
222#[must_use]
223pub fn get_readline_edit_mode(contents: impl AsRef<[u8]>) -> Option<EditMode> {
224    fn inner(contents: &[u8]) -> Option<EditMode> {
225        let mut edit_mode = None; // Stores the last encountered editing mode
226
227        for line in contents.lines() {
228            // Skip leading whitespace.
229            let line = trim_whitespace_front(line);
230
231            // If the line is not a comment, then parse it.
232            if matches!(line.first(), Some(b'#') | None) {
233                continue;
234            }
235
236            // If this is a command to set a variable, then do that.
237            let line = match line.strip_prefix(b"set") {
238                Some(rest) => rest,
239                None => continue,
240            };
241            // Skip leading whitespace.
242            let line = trim_whitespace_front(line);
243
244            // Per the manual, section 8.3.1:
245            // https://www.gnu.org/software/bash/manual/bash.html#Readline-Init-File-Syntax
246            //
247            // > Variable names and values, where appropriate, are recognized
248            // > without regard to case. Unrecognized variable names are ignored.
249            //
250            // In this case `editing-mode` is a variable name.
251            let line = match line {
252                [
253                    b'e' | b'E',
254                    b'd' | b'D',
255                    b'i' | b'I',
256                    b't' | b'T',
257                    b'i' | b'I',
258                    b'n' | b'N',
259                    b'g' | b'G',
260                    b'-',
261                    b'm' | b'M',
262                    b'o' | b'O',
263                    b'd' | b'D',
264                    b'e' | b'E',
265                    rest @ ..,
266                ] => rest,
267                _ => continue,
268            };
269            // Skip leading whitespace.
270            let line = trim_whitespace_front(line);
271
272            // Per the manual, section 8.3.1:
273            // https://www.gnu.org/software/bash/manual/bash.html#Readline-Init-File-Syntax
274            //
275            // > Variable names and values, where appropriate, are recognized
276            // > without regard to case. Unrecognized variable names are ignored.
277            //
278            // The values 'vi' and 'emacs' in the 'set' directive are case
279            // insensitive as they are the variable value.
280            match line {
281                [b'v' | b'V', b'i' | b'I'] => {
282                    // Last occurrence of editing mode directive takes effect.
283                    edit_mode = Some(EditMode::Vi);
284                }
285                [b'e' | b'E', b'm' | b'M', b'a' | b'A', b'c' | b'C', b's' | b'S'] => {
286                    // Last occurrence of editing mode directive takes effect.
287                    edit_mode = Some(EditMode::Emacs);
288                }
289                [b'v' | b'V', b'i' | b'I', next, ..] if posix_space::is_space(*next) => {
290                    // Last occurrence of editing mode directive takes effect.
291                    edit_mode = Some(EditMode::Vi);
292                }
293                [
294                    b'e' | b'E',
295                    b'm' | b'M',
296                    b'a' | b'A',
297                    b'c' | b'C',
298                    b's' | b'S',
299                    next,
300                    ..,
301                ] if posix_space::is_space(*next) => {
302                    // Last occurrence of editing mode directive takes effect.
303                    edit_mode = Some(EditMode::Emacs);
304                }
305                // Ignore unrecognized or invalid lines.
306                // Lines without a valid editing mode directive are skipped.
307                _ => {}
308            }
309        }
310
311        edit_mode
312    }
313
314    inner(contents.as_ref())
315}
316
317/// Skip leading POSIX whitespace.
318fn trim_whitespace_front(mut s: &[u8]) -> &[u8] {
319    loop {
320        if let Some((&head, tail)) = s.split_first() {
321            if posix_space::is_space(head) {
322                s = tail;
323                continue;
324            }
325        }
326        break s;
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::{EditMode, get_readline_edit_mode};
333
334    #[test]
335    fn test_default_edit_mode_is_emacs() {
336        assert_eq!(EditMode::default(), EditMode::Emacs);
337    }
338
339    #[test]
340    #[cfg(feature = "rustyline")]
341    fn test_edit_mode_rustyline_into() {
342        assert_eq!(rustyline::config::EditMode::Emacs, EditMode::Emacs.into());
343        assert_eq!(rustyline::config::EditMode::Vi, EditMode::Vi.into());
344    }
345
346    #[test]
347    fn test_get_readline_edit_mode_vi_mode() {
348        let config = "set editing-mode vi";
349        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
350    }
351
352    #[test]
353    fn test_get_readline_edit_mode_emacs_mode() {
354        let config = "set editing-mode emacs";
355        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
356    }
357
358    #[test]
359    fn test_get_readline_edit_mode_parse_empty_and_blank_lines() {
360        let test_cases = [
361            "",
362            "              ",
363            "\t\t\t",
364            "          \n              ",
365            "\n",
366            "\r\n",
367            "              \r\n           ",
368        ];
369        for contents in test_cases {
370            assert_eq!(get_readline_edit_mode(contents), None);
371        }
372    }
373
374    #[test]
375    fn test_get_readline_edit_mode_whitespace_only_lines() {
376        let contents = "
377            \t
378            \n
379            \r
380        ";
381        assert_eq!(get_readline_edit_mode(contents), None);
382    }
383
384    #[test]
385    fn test_get_readline_edit_mode_empty_contents() {
386        let contents = "";
387        assert_eq!(get_readline_edit_mode(contents), None);
388    }
389
390    #[test]
391    fn test_get_readline_edit_mode_no_set_directive() {
392        let contents = "editing-mode vi";
393        assert_eq!(get_readline_edit_mode(contents), None);
394    }
395
396    #[test]
397    fn test_get_readline_edit_mode_comment_lines() {
398        let contents = "
399            # This is a comment
400            # set editing-mode vi
401            # set editing-mode emacs
402        ";
403        assert_eq!(get_readline_edit_mode(contents), None);
404    }
405
406    #[test]
407    fn test_get_readline_edit_mode_set_editing_mode_with_space_before_variable_name() {
408        let test_cases = [
409            ("set     editing-mode vi", EditMode::Vi),
410            ("set     editing-mode emacs", EditMode::Emacs),
411        ];
412
413        for (config, expected_mode) in test_cases {
414            assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
415        }
416    }
417
418    #[test]
419    fn test_get_readline_edit_mode_set_editing_mode_with_space_after_variable_name() {
420        let test_cases = [
421            ("set editing-mode    vi", EditMode::Vi),
422            ("set editing-mode    emacs", EditMode::Emacs),
423        ];
424
425        for (config, expected_mode) in test_cases {
426            assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
427        }
428    }
429
430    #[test]
431    fn test_get_readline_edit_mode_excess_whitespace() {
432        let test_cases = [
433            ("set editing-mode  \t vi  \t \r\n", EditMode::Vi),
434            ("set editing-mode  \t emacs  \t \r\n", EditMode::Emacs),
435            ("set editing-mode   vi  \t \n", EditMode::Vi),
436            ("set editing-mode   emacs  \t \n", EditMode::Emacs),
437        ];
438
439        for (config, expected_mode) in test_cases {
440            assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
441        }
442    }
443
444    #[test]
445    fn test_get_readline_edit_mode_ignore_trailing_garbage() {
446        let test_cases = [
447            ("set editing-mode vi 1234", EditMode::Vi),
448            ("set editing-mode emacs 1234", EditMode::Emacs),
449            ("set editing-mode vi this-is-extra-content", EditMode::Vi),
450            ("set editing-mode emacs this-is-extra-content", EditMode::Emacs),
451        ];
452
453        for (config, expected_mode) in test_cases {
454            assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
455        }
456    }
457
458    #[test]
459    fn test_get_readline_edit_mode_requires_lowercase_set() {
460        let test_cases = [
461            "SET editing-mode vi",
462            "SET editing-mode emacs",
463            "Set editing-mode vi",
464            "Set editing-mode emacs",
465            "sET editing-mode vi",
466            "sET editing-mode emacs",
467        ];
468
469        for config in test_cases {
470            assert_eq!(get_readline_edit_mode(config), None);
471        }
472    }
473
474    #[test]
475    fn test_get_readline_editing_mode_variable_name_case_insensitive() {
476        let test_cases = [
477            // Lowercase
478            ("set editing-mode vi", EditMode::Vi),
479            ("set editing-mode emacs", EditMode::Emacs),
480            // Uppercase
481            ("set EDITING-MODE emacs", EditMode::Emacs),
482            ("set EDITING-MODE vi", EditMode::Vi),
483            // Mixed case
484            ("set Editing-Mode vi", EditMode::Vi),
485            ("set Editing-Mode emacs", EditMode::Emacs),
486            ("set EdItInG-MoDe vi", EditMode::Vi),
487            ("set EdItInG-MoDe emacs", EditMode::Emacs),
488            ("set eDiTiNg-mOdE vi", EditMode::Vi),
489            ("set eDiTiNg-mOdE emacs", EditMode::Emacs),
490        ];
491
492        for (config, expected_mode) in test_cases {
493            assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
494        }
495    }
496
497    #[test]
498    fn test_get_readline_editing_mode_variable_value_case_insensitive() {
499        let test_cases = [
500            // Lowercase
501            ("set editing-mode vi", EditMode::Vi),
502            ("set editing-mode emacs", EditMode::Emacs),
503            // Uppercase
504            ("set editing-mode VI", EditMode::Vi),
505            ("set editing-mode EMACS", EditMode::Emacs),
506            // Mixed case
507            ("set editing-mode Vi", EditMode::Vi),
508            ("set editing-mode vI", EditMode::Vi),
509            ("set editing-mode eMaCs", EditMode::Emacs),
510            ("set editing-mode EmAcS", EditMode::Emacs),
511            ("set editing-mode emACS", EditMode::Emacs),
512            ("set editing-mode EMacs", EditMode::Emacs),
513        ];
514
515        for (config, expected_mode) in test_cases {
516            assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
517        }
518    }
519
520    #[test]
521    fn test_get_readline_editing_mode_ignores_unrecognized_variable_names() {
522        // Test handling unrecognized variable names
523        let input = "set unknown-variable foo";
524        assert_eq!(get_readline_edit_mode(input), None);
525    }
526
527    #[test]
528    fn test_get_readline_edit_mode_multiple_lines_with_comments() {
529        let contents = "
530            # This is a comment
531            set some-other-setting 123
532
533            # Another comment
534            set editing-mode vi
535
536            # One more comment
537            set another-setting true
538        ";
539        assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
540    }
541
542    #[test]
543    fn test_get_readline_edit_mode_no_mode_directive() {
544        let config = "set blink-matching-paren on\n";
545        assert_eq!(get_readline_edit_mode(config), None);
546    }
547
548    #[test]
549    fn test_get_readline_edit_mode_multiple_lines() {
550        let config = "set editing-mode vi\nset blink-matching-paren on\n";
551        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
552    }
553
554    #[test]
555    fn test_get_readline_edit_mode_invalid_variable_value() {
556        let config = "set editing-mode vim\n";
557        assert_eq!(get_readline_edit_mode(config), None);
558    }
559
560    #[test]
561    fn test_get_readline_edit_mode_invalid_characters_variable_value() {
562        let config = "set editing-mode vī\n";
563        assert_eq!(get_readline_edit_mode(config), None);
564    }
565
566    #[test]
567    fn test_get_readline_edit_mode_with_posix_spaces() {
568        let test_cases = [
569            ("set editing-mode     vi", EditMode::Vi),
570            ("set editing-mode     emacs", EditMode::Emacs),
571            ("set editing-mode\tvi", EditMode::Vi),
572            ("set editing-mode\temacs", EditMode::Emacs),
573            ("set editing-mode     \t \tvi", EditMode::Vi),
574            ("set editing-mode     \t \temacs", EditMode::Emacs),
575            ("set editing-mode\t\t\t\t\tvi", EditMode::Vi),
576            ("set editing-mode\t\t\t\t\temacs", EditMode::Emacs),
577        ];
578
579        for (config, expected_mode) in test_cases {
580            assert_eq!(get_readline_edit_mode(config), Some(expected_mode));
581        }
582    }
583
584    #[test]
585    fn test_get_readline_edit_mode_vi_mode_with_multibyte_utf8() {
586        let config = "set editing-mode vī\n";
587        assert_eq!(get_readline_edit_mode(config), None);
588    }
589
590    #[test]
591    fn test_get_readline_edit_mode_emacs_mode_with_multibyte_utf8() {
592        let config = "set editing-mode eĦmacs\n";
593        assert_eq!(get_readline_edit_mode(config), None);
594    }
595
596    #[test]
597    fn test_get_readline_edit_mode_vi_mode_with_trailing_invalid_utf8() {
598        let config = b"set editing-mode vi \x80\n";
599        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
600    }
601
602    #[test]
603    fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode() {
604        let config = b"set editing-mode v\xFFi\n";
605        assert_eq!(get_readline_edit_mode(config), None);
606    }
607
608    #[test]
609    fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode() {
610        let config = b"set editing-mode e\xEFmacs\n";
611        assert_eq!(get_readline_edit_mode(config), None);
612    }
613
614    #[test]
615    fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_with_trailing_content() {
616        let config = b"set editing-mode vi \xFF\xFF\xFF\n";
617        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
618    }
619
620    #[test]
621    fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_with_trailing_content() {
622        let config = b"set editing-mode emacs this-\x80is-extra-content\n";
623        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
624    }
625
626    #[test]
627    fn test_get_readline_edit_mode_invalid_utf8_bytes_multiple_lines() {
628        let config = b"set editing-mode vi\nset blink-matching-paren \xC0\x80on\n";
629        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
630    }
631
632    #[test]
633    fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_excess_whitespace() {
634        let config = b"set editing-mode  \x80emacs  \t \n";
635        assert_eq!(get_readline_edit_mode(config), None);
636    }
637
638    #[test]
639    fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_excess_whitespace() {
640        let config = b"set editing-mode  \x80vi  \t \r\n";
641        assert_eq!(get_readline_edit_mode(config), None);
642    }
643
644    #[test]
645    fn test_get_readline_edit_mode_invalid_utf8_bytes_no_mode_directive() {
646        let config = b"set blink-matching-\x80paren on\n";
647        assert_eq!(get_readline_edit_mode(config), None);
648    }
649
650    #[test]
651    fn test_get_readline_edit_mode_invalid_utf8_bytes_invalid_directive() {
652        let config = b"set editing-\x80mode vim\n";
653        assert_eq!(get_readline_edit_mode(config), None);
654    }
655
656    #[test]
657    fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_with_posix_spaces() {
658        let config = b"set editing-mode     e\xEFmacs\n";
659        assert_eq!(get_readline_edit_mode(config), None);
660    }
661
662    #[test]
663    fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_with_posix_spaces() {
664        let config = b"set editing-\x80mode\tvi\n";
665        assert_eq!(get_readline_edit_mode(config), None);
666    }
667
668    #[test]
669    fn test_get_readline_edit_mode_invalid_utf8_bytes_emacs_mode_with_multiple_posix_spaces() {
670        let config = b"set editing-mode     \t \nem\xF4cs\n";
671        assert_eq!(get_readline_edit_mode(config), None);
672    }
673
674    #[test]
675    fn test_get_readline_edit_mode_invalid_utf8_bytes_vi_mode_with_multiple_posix_spaces() {
676        let config = b"set editing-\xF4mode     \t \nvi\n";
677        assert_eq!(get_readline_edit_mode(config), None);
678    }
679
680    #[test]
681    fn test_get_readline_edit_mode_quotes() {
682        let test_cases = [
683            // Test cases for single quotes
684            "set editing-mode 'emacs'",
685            "set editing-mode 'vi'",
686            // Test cases for double quotes
687            "set editing-mode \"emacs\"",
688            "set editing-mode \"vi\"",
689            // Test cases for mixed quotes
690            "set editing-mode 'emacs\"",
691            "set editing-mode 'vi\"",
692        ];
693
694        for config in test_cases {
695            assert_eq!(get_readline_edit_mode(config), None);
696        }
697    }
698
699    #[test]
700    fn test_get_readline_edit_mode_last_set_directive_vi() {
701        let contents = "
702            set editing-mode emacs
703            set editing-mode vi
704        ";
705        assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
706    }
707
708    #[test]
709    fn test_get_readline_edit_mode_last_set_directive_emacs() {
710        let contents = "
711            set editing-mode vi
712            set editing-mode emacs
713        ";
714        assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Emacs));
715    }
716
717    #[test]
718    fn test_get_readline_edit_mode_last_set_directive_vi_with_whitespace() {
719        let contents = "
720            set editing-mode emacs
721            set editing-mode   vi
722        ";
723        assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
724    }
725
726    #[test]
727    fn test_get_readline_edit_mode_last_set_directive_emacs_with_whitespace() {
728        let contents = "
729            set editing-mode vi
730            set editing-mode    emacs
731        ";
732        assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Emacs));
733    }
734
735    #[test]
736    fn test_get_readline_edit_mode_multiple_set_directives_mixed() {
737        let contents = "
738            set some-other-setting 123
739
740            set editing-mode vi
741
742            set another-setting true
743
744            set editing-mode emacs
745
746            set extra-setting abc
747            set extra-setting xyz
748
749            set editing-mode vi
750        ";
751        assert_eq!(get_readline_edit_mode(contents), Some(EditMode::Vi));
752    }
753
754    #[test]
755    fn test_get_readline_edit_mode_integration_1() {
756        let config = "
757            set blink-matching-paren on
758            set keymap vi-command
759            set editing-mode emacs
760            set completion-ignore-case on
761        ";
762        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
763    }
764
765    #[test]
766    fn test_get_readline_edit_mode_integration_2() {
767        let config = "
768            set blink-matching-paren on
769            set editing-mode vi
770            set completion-ignore-case on
771            set keymap vi-command
772        ";
773        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Vi));
774    }
775
776    #[test]
777    fn test_get_readline_edit_mode_integration_3() {
778        let config = "
779            set blink-matching-paren on
780            set completion-ignore-case on
781            set editing-mode emacs
782            set keymap vi-command
783        ";
784        assert_eq!(get_readline_edit_mode(config), Some(EditMode::Emacs));
785    }
786
787    #[test]
788    fn test_get_readline_edit_mode_integration_4() {
789        let config = "
790            set blink-matching-paren on
791            set keymap vi-command
792            set completion-ignore-case on
793        ";
794        assert_eq!(get_readline_edit_mode(config), None);
795    }
796
797    #[test]
798    fn test_get_readline_edit_mode_integration_5() {
799        let config = "
800            set blink-matching-paren on
801            set completion-ignore-case on
802            set keymap vi-command
803        ";
804        assert_eq!(get_readline_edit_mode(config), None);
805    }
806
807    #[test]
808    fn test_get_readline_edit_mode_integration_6() {
809        let config = "
810            set blink-matching-paren on
811            set keymap vi-command
812        ";
813        assert_eq!(get_readline_edit_mode(config), None);
814    }
815}