mg_settings/
lib.rs

1/*
2 * Copyright (c) 2016-2017 Boucher, Antoni <bouanto@zoho.com>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy of
5 * this software and associated documentation files (the "Software"), to deal in
6 * the Software without restriction, including without limitation the rights to
7 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8 * the Software, and to permit persons to whom the Software is furnished to do so,
9 * subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in all
12 * copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 */
21
22//! Parse config files.
23//!
24//! # Usage
25//!
26//! Call the `parse` function on the input.
27
28/*
29 * TODO: prevent mutual includes.
30 * TODO: auto-include files.
31 * TODO: support set = without spaces around =.
32 * TODO: Add array type.
33 */
34
35pub mod errors;
36mod file;
37pub mod key;
38#[doc(hidden)]
39pub mod position;
40pub mod settings;
41mod string;
42
43use std::collections::HashMap;
44use std::io::{BufRead, BufReader};
45use std::marker::PhantomData;
46use std::path::{Path, PathBuf};
47
48use errors::{Error, ParseError, Result};
49use errors::ErrorType::{MissingArgument, NoCommand, Parse, UnknownCommand};
50use key::{Key, parse_keys};
51use position::Pos;
52use string::{StrExt, check_ident, maybe_word, word, words};
53
54use Command::*;
55use Value::*;
56
57#[macro_export]
58macro_rules! rtry {
59    ($parse_result:expr, $result:expr) => {
60        rtry_no_return!($parse_result, $result, { return $parse_result; });
61    };
62}
63
64#[macro_export]
65macro_rules! rtry_no_return {
66    ($parse_result:expr, $result:expr, $error_block:block) => {
67        match $result {
68            Ok(result) => result,
69            Err(error) => {
70                $parse_result.errors.push(error.into());
71                $error_block
72            },
73        }
74    };
75}
76
77/// Trait to specify the completion values for a type.
78pub trait CompletionValues {
79    /// Get the completion values for the type.
80    fn completion_values() -> Vec<String>;
81}
82
83impl CompletionValues for bool {
84    fn completion_values() -> Vec<String> {
85        vec!["true".to_string(), "false".to_string()]
86    }
87}
88
89impl CompletionValues for i64 {
90    fn completion_values() -> Vec<String> {
91        vec![]
92    }
93}
94
95impl CompletionValues for String {
96    fn completion_values() -> Vec<String> {
97        vec![]
98    }
99}
100
101/// The `EnumFromStr` trait is used to specify how to construct an enum value from a string.
102pub trait EnumFromStr
103    where Self: Sized
104{
105    /// Create the enum value from the `variant` string and an `argument` string.
106    fn create(variant: &str, argument: &str, prefix: Option<u32>) -> std::result::Result<Self, String>;
107
108    /// Check wether the enum variant has an argument.
109    fn has_argument(variant: &str) -> std::result::Result<bool, String>;
110}
111
112/// Tre `EnumMetaData` trait is used to get associated meta-data for the enum variants.
113/// The meta-data is specified using the following attributes:
114/// ``` ignore
115/// #[completion(hidden)]
116/// #[special_command]
117/// #[help(Command help)]
118/// ```
119pub trait EnumMetaData {
120    /// Get the metadata associated with the enum.
121    fn get_metadata() -> HashMap<String, MetaData>;
122}
123
124/// Command/setting meta-data coming from the attributes.
125/// See `EnumMetaData` to see the list of supported attributes.
126#[derive(Debug)]
127pub struct MetaData {
128    /// Whether this command/setting should be shown in the completion or not.
129    pub completion_hidden: bool,
130    /// The help text associated with this command/setting.
131    pub help_text: String,
132    /// Whether this is a special command or not.
133    /// This is not applicable to settings.
134    pub is_special_command: bool,
135}
136
137/// The commands and errors from parsing a config file.
138pub struct ParseResult<T> {
139    /// The parsed commands.
140    pub commands: Vec<Command<T>>,
141    /// The errors resulting from the parsing.
142    pub errors: Vec<Error>,
143}
144
145impl<T> ParseResult<T> {
146    #[allow(unknown_lints, new_without_default_derive)]
147    /// Create a new empty parser result.
148    pub fn new() -> Self {
149        ParseResult {
150            commands: vec![],
151            errors: vec![],
152        }
153    }
154
155    fn new_with_command(command: Command<T>) -> Self {
156        ParseResult {
157            commands: vec![command],
158            errors: vec![],
159        }
160    }
161
162    fn merge(&mut self, mut parse_result: ParseResult<T>) {
163        self.commands.append(&mut parse_result.commands);
164        self.errors.append(&mut parse_result.errors);
165    }
166}
167
168/// Trait specifying the value completions for settings.
169pub trait SettingCompletion {
170    /// Get the value completions of all the setting.
171    fn get_value_completions() -> HashMap<String, Vec<String>>;
172}
173
174/// The `Command` enum represents a command from a config file.
175#[derive(Debug, PartialEq)]
176pub enum Command<T> {
177    /// A command from the application library.
178    App(String),
179    /// A custom command.
180    Custom(T),
181    /// A map command creates a new key mapping.
182    Map {
183        /// The action that will be executed when the `keys` are pressed.
184        action: String,
185        /// The key shortcut to trigger the action.
186        keys: Vec<Key>,
187        /// The mode in which this mapping is available.
188        mode: String,
189    },
190    /// A set command sets a value to an option.
191    Set(String, Value),
192    /// An unmap command removes a key mapping.
193    Unmap {
194        /// The key shortcut to remove.
195        keys: Vec<Key>,
196        /// The mode in which this mapping is available.
197        mode: String,
198    },
199}
200
201/// The parsing configuration.
202#[derive(Default)]
203pub struct Config {
204    /// The application library commands.
205    pub application_commands: Vec<&'static str>,
206    /// The available mapping modes for the map command.
207    pub mapping_modes: Vec<&'static str>,
208}
209
210/// The config parser.
211pub struct Parser<T> {
212    column: usize,
213    config: Config,
214    include_path: PathBuf,
215    line: usize,
216    _phantom: PhantomData<T>,
217}
218
219impl<T: EnumFromStr> Parser<T> {
220    #[allow(unknown_lints, new_without_default_derive)]
221    /// Create a new parser without config.
222    pub fn new() -> Self {
223        Parser {
224            column: 1,
225            config: Config::default(),
226            include_path: Path::new("./").to_path_buf(),
227            line: 1,
228            _phantom: PhantomData,
229        }
230    }
231
232    /// Create a new parser with config.
233    pub fn new_with_config(config: Config) -> Self {
234        Parser {
235            column: 1,
236            config: config,
237            include_path: Path::new("./").to_path_buf(),
238            line: 1,
239            _phantom: PhantomData,
240        }
241    }
242
243    /// Check that we reached the end of the line.
244    fn check_eol(&self, line: &str, index: usize) -> Result<()> {
245        if line.len() > index {
246            let rest = &line[index..];
247            if let Some(word) = maybe_word(rest) {
248                let index = word.index;
249                return Err(ParseError::new(
250                    Parse,
251                    rest.to_string(),
252                    "<end of line>".to_string(),
253                    Pos::new(self.line, self.column + index),
254                ));
255            }
256        }
257        Ok(())
258    }
259
260    /// Parse a custom command or return an error if it does not exist.
261    fn custom_command(&self, line: &str, word: &str, start_index: usize, index: usize, prefix: Option<u32>)
262        -> Result<Command<T>>
263    {
264        let args =
265            if line.len() > start_index {
266                line[start_index..].trim()
267            }
268            else if let Ok(true) = T::has_argument(word) {
269                return Err(self.missing_args(start_index));
270            }
271            else {
272                ""
273            };
274        if let Ok(command) = T::create(word, args, prefix) {
275            Ok(Custom(command))
276        }
277        else if self.config.application_commands.contains(&word) {
278            Ok(App(word.to_string()))
279        }
280        else {
281            return Err(ParseError::new(
282                UnknownCommand,
283                word.to_string(),
284                "command or comment".to_string(),
285                Pos::new(self.line, index + 1)
286            ))
287        }
288    }
289
290    /// Get the rest of the line, starting at `column`, returning an error if the column is greater
291    /// than the line's length.
292    fn get_rest<'a>(&self, line: &'a str, column: usize) -> Result<&'a str> {
293        if line.len() > column {
294            Ok(&line[column..])
295        }
296        else {
297            Err(self.missing_args(column))
298        }
299    }
300
301    /// Parse a line.
302    fn line(&mut self, line: &str, prefix: Option<u32>) -> ParseResult<T> {
303        let mut result = ParseResult::new();
304        if let Some(word) = maybe_word(line) {
305            let index = word.index;
306            let word = word.word;
307            let start_index = index + word.len() + 1;
308            self.column = start_index + 1;
309
310            let (start3, end3) = word.rsplit_at(3);
311            let (start5, end5) = word.rsplit_at(5);
312            if word.starts_with('#') {
313                return result;
314            }
315
316            if word == "include" {
317                let rest = rtry!(result, self.get_rest(line, start_index));
318                self.include_command(rest)
319            }
320            else {
321                let command =
322                    if word == "set" {
323                        let rest = rtry!(result, self.get_rest(line, start_index));
324                        self.set_command(rest)
325                    }
326                    else if end3 == "map" && self.config.mapping_modes.contains(&start3) {
327                        let rest = rtry!(result, self.get_rest(line, start_index));
328                        self.map_command(rest, start3)
329                    }
330                    else if end5 == "unmap" && self.config.mapping_modes.contains(&start5) {
331                        let rest = rtry!(result, self.get_rest(line, start_index));
332                        self.unmap_command(rest, start5)
333                    }
334                    else {
335                        self.custom_command(line, word, start_index, index, prefix)
336                    };
337                let command = rtry!(result, command);
338                ParseResult::new_with_command(command)
339            }
340        }
341        else {
342            result
343        }
344    }
345
346    /// Parse an include command.
347    fn include_command(&mut self, line: &str) -> ParseResult<T> {
348        let word = word(line);
349        let index = word.index;
350        let word = word.word;
351        let after_index = index + word.len() + 1;
352        self.column += after_index;
353        let mut result = ParseResult::new();
354        rtry_no_return!(result, self.check_eol(line, after_index), {});
355        let path = Path::new(&self.include_path).join(word);
356        let file = rtry!(result, file::open(&path));
357        let buf_reader = BufReader::new(file);
358        result.merge(self.parse(buf_reader, None));
359        result
360    }
361
362    /// Parse a map command.
363    fn map_command(&self, line: &str, mode: &str) -> Result<Command<T>> {
364        let word = word(line);
365        let index = word.index;
366        let word = word.word;
367        let rest = &line[index + word.len() ..].trim();
368        if !rest.is_empty() {
369            Ok(Map {
370                action: rest.to_string(),
371                keys: parse_keys(word, self.line, self.column + index)?,
372                mode: mode.to_string(),
373            })
374        }
375        else {
376            Err(ParseError::new(
377                Parse,
378                "<end of line>".to_string(),
379                "mapping action".to_string(),
380                Pos::new(self.line, self.column + line.len())
381            ))
382        }
383    }
384
385    /// Get an missing arguments error.
386    fn missing_args(&self, column: usize) -> Error {
387        ParseError::new(
388            MissingArgument,
389            "<end of line>".to_string(),
390            "command arguments".to_string(),
391            Pos::new(self.line, column)
392        )
393    }
394
395    /// Parse settings.
396    pub fn parse<R: BufRead>(&mut self, input: R, prefix: Option<u32>) -> ParseResult<T> {
397        let mut result = ParseResult::new();
398        for (line_num, input_line) in input.lines().enumerate() {
399            self.line = line_num + 1;
400            let input_line = rtry_no_return!(result, input_line, { continue });
401            result.merge(self.line(&input_line, prefix));
402        }
403        result
404    }
405
406    /// Parse a single line of settings.
407    pub fn parse_line(&mut self, line: &str, prefix: Option<u32>) -> ParseResult<T> {
408        let mut result = self.parse(line.as_bytes(), prefix);
409        if result.commands.is_empty() && result.errors.is_empty() {
410            result.errors.push(ParseError::new(
411                NoCommand,
412                "comment or <end of line>".to_string(),
413                "command".to_string(),
414                Pos::new(self.line, 1)
415            ));
416        }
417        result
418    }
419
420    /// Parse a set command.
421    fn set_command(&mut self, line: &str) -> Result<Command<T>> {
422        if let Some(words) = words(line, 2) {
423            let index = words[0].index;
424            let word =  words[0].word;
425            let identifier = check_ident(word.to_string(), &Pos::new(self.line, self.column + index))?;
426
427            let operator = words[1].word;
428            let operator_index = words[1].index;
429            if operator == "=" {
430                let rest = &line[operator_index + 1..];
431                self.column += operator_index + 1;
432                Ok(Set(identifier.to_string(), self.value(rest)?))
433            }
434            else {
435                return Err(ParseError::new(
436                    Parse,
437                    operator.to_string(),
438                    "=".to_string(),
439                    Pos::new(self.line, self.column + operator_index)
440                ))
441            }
442        }
443        else {
444            return Err(ParseError::new(
445                Parse,
446                "<end of line>".to_string(),
447                "=".to_string(),
448                Pos::new(self.line, self.column + line.len()),
449            ))
450        }
451    }
452
453    /// Set the directory where the include command will look for files to include.
454    pub fn set_include_path<P: AsRef<Path>>(&mut self, directory: P) {
455        self.include_path = directory.as_ref().to_path_buf();
456    }
457
458    /// Parse an unmap command.
459    fn unmap_command(&mut self, line: &str, mode: &str) -> Result<Command<T>> {
460        let word = word(line);
461        let index = word.index;
462        let word = word.word;
463        let after_index = index + word.len() + 1;
464        self.column += after_index;
465        self.check_eol(line, after_index)?;
466        Ok(Unmap {
467            keys: parse_keys(word, self.line, self.column + index)?,
468            mode: mode.to_string(),
469        })
470    }
471
472    /// Parse a value.
473    fn value(&self, input: &str) -> Result<Value> {
474        let string: String = input.chars().take_while(|&character| character != '#').collect();
475        let string = string.trim();
476        match string {
477            "" => Err(ParseError::new(
478                      Parse,
479                      "<end of line>".to_string(),
480                      "value".to_string(),
481                      Pos::new(self.line, self.column + string.len())
482                  )),
483            "true" => Ok(Bool(true)),
484            "false" => Ok(Bool(false)),
485            _ => {
486                if string.chars().all(|character| character.is_digit(10)) {
487                    // NOTE: the string only contains digit, hence unwrap.
488                    Ok(Int(string.parse().unwrap()))
489                }
490                else if string.chars().all(|character| character.is_digit(10) || character == '.') {
491                    // NOTE: the string only contains digit or dot, hence unwrap.
492                    Ok(Float(string.parse().unwrap()))
493                }
494                else {
495                    Ok(Str(input.trim().to_string()))
496                }
497            },
498        }
499    }
500}
501
502/// Trait for converting an identifier like "/" to a special command.
503pub trait SpecialCommand
504    where Self: Sized
505{
506    /// Convert an identifier like "/" to a special command.
507    fn identifier_to_command(identifier: char, input: &str) -> std::result::Result<Self, String>;
508
509    /// Check if a character is a special command identifier.
510    fn is_identifier(character: char) -> bool;
511
512    /// Check if the identifier is declared `always`.
513    /// The always option means that the command is activated every time a character is typed (like
514    /// incremental search).
515    fn is_incremental(identifier: char) -> bool;
516}
517
518/// The `Value` enum represents a value along with its type.
519#[derive(Debug, PartialEq)]
520pub enum Value {
521    /// Boolean value.
522    Bool(bool),
523    /// Floating-point value.
524    Float(f64),
525    /// Integer value.
526    Int(i64),
527    /// String value.
528    Str(String),
529}
530
531impl Value {
532    /// Get a string representation of the value.
533    pub fn to_type(&self) -> &str {
534        match *self {
535            Bool(_) => "bool",
536            Float(_) => "float",
537            Int(_) => "int",
538            Str(_) => "string",
539        }
540    }
541}