xi_core_lib/
syntax.rs

1// Copyright 2017 The xi-editor Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Very basic syntax detection.
16
17use std::borrow::Borrow;
18use std::collections::{BTreeMap, HashMap};
19use std::path::Path;
20use std::sync::Arc;
21
22use crate::config::Table;
23
24/// The canonical identifier for a particular `LanguageDefinition`.
25#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Hash, PartialOrd, Ord)]
26pub struct LanguageId(Arc<String>);
27
28/// Describes a `LanguageDefinition`. Although these are provided by plugins,
29/// they are a fundamental concept in core, used to determine things like
30/// plugin activations and active user config tables.
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct LanguageDefinition {
33    pub name: LanguageId,
34    pub extensions: Vec<String>,
35    pub first_line_match: Option<String>,
36    pub scope: String,
37    #[serde(skip)]
38    pub default_config: Option<Table>,
39}
40
41/// A repository of all loaded `LanguageDefinition`s.
42#[derive(Debug, Default)]
43pub struct Languages {
44    // NOTE: BTreeMap is used for sorting the languages by name alphabetically
45    named: BTreeMap<LanguageId, Arc<LanguageDefinition>>,
46    extensions: HashMap<String, Arc<LanguageDefinition>>,
47}
48
49impl Languages {
50    pub fn new(language_defs: &[LanguageDefinition]) -> Self {
51        let mut named = BTreeMap::new();
52        let mut extensions = HashMap::new();
53        for lang in language_defs.iter() {
54            let lang_arc = Arc::new(lang.clone());
55            named.insert(lang.name.clone(), lang_arc.clone());
56            for ext in &lang.extensions {
57                extensions.insert(ext.clone(), lang_arc.clone());
58            }
59        }
60        Languages { named, extensions }
61    }
62
63    pub fn language_for_path(&self, path: &Path) -> Option<Arc<LanguageDefinition>> {
64        path.extension()
65            .or_else(|| path.file_name())
66            .and_then(|ext| self.extensions.get(ext.to_str().unwrap_or_default()))
67            .map(Arc::clone)
68    }
69
70    pub fn language_for_name<S>(&self, name: S) -> Option<Arc<LanguageDefinition>>
71    where
72        S: AsRef<str>,
73    {
74        self.named.get(name.as_ref()).map(Arc::clone)
75    }
76
77    /// Returns a Vec of any `LanguageDefinition`s which exist
78    /// in `self` but not `other`.
79    pub fn difference(&self, other: &Languages) -> Vec<Arc<LanguageDefinition>> {
80        self.named
81            .iter()
82            .filter(|(k, _)| !other.named.contains_key(*k))
83            .map(|(_, v)| v.clone())
84            .collect()
85    }
86
87    pub fn iter(&self) -> impl Iterator<Item = &Arc<LanguageDefinition>> {
88        self.named.values()
89    }
90}
91
92impl AsRef<str> for LanguageId {
93    fn as_ref(&self) -> &str {
94        self.0.as_ref()
95    }
96}
97
98// let's us use &str to query a HashMap with `LanguageId` keys
99impl Borrow<str> for LanguageId {
100    fn borrow(&self) -> &str {
101        &self.0.as_ref()
102    }
103}
104
105impl<'a> From<&'a str> for LanguageId {
106    fn from(src: &'a str) -> LanguageId {
107        LanguageId(Arc::new(src.into()))
108    }
109}
110
111// for testing
112#[cfg(test)]
113impl LanguageDefinition {
114    pub(crate) fn simple(name: &str, exts: &[&str], scope: &str, config: Option<Table>) -> Self {
115        LanguageDefinition {
116            name: name.into(),
117            extensions: exts.iter().map(|s| (*s).into()).collect(),
118            first_line_match: None,
119            scope: scope.into(),
120            default_config: config,
121        }
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128
129    #[test]
130    pub fn language_for_path() {
131        let ld_rust = LanguageDefinition {
132            name: LanguageId::from("Rust"),
133            extensions: vec![String::from("rs")],
134            scope: String::from("source.rust"),
135            first_line_match: None,
136            default_config: None,
137        };
138        let ld_commit_msg = LanguageDefinition {
139            name: LanguageId::from("Git Commit"),
140            extensions: vec![
141                String::from("COMMIT_EDITMSG"),
142                String::from("MERGE_MSG"),
143                String::from("TAG_EDITMSG"),
144            ],
145            scope: String::from("text.git.commit"),
146            first_line_match: None,
147            default_config: None,
148        };
149        let languages = Languages::new(&[ld_rust.clone(), ld_commit_msg.clone()]);
150
151        assert_eq!(
152            ld_rust.name,
153            languages.language_for_path(Path::new("/path/test.rs")).unwrap().name
154        );
155        assert_eq!(
156            ld_commit_msg.name,
157            languages.language_for_path(Path::new("/path/COMMIT_EDITMSG")).unwrap().name
158        );
159        assert_eq!(
160            ld_commit_msg.name,
161            languages.language_for_path(Path::new("/path/MERGE_MSG")).unwrap().name
162        );
163        assert_eq!(
164            ld_commit_msg.name,
165            languages.language_for_path(Path::new("/path/TAG_EDITMSG")).unwrap().name
166        );
167    }
168}