1use crate::ignorefile;
2use regex::Regex;
3use serde::{Deserialize, Serialize};
4use std::collections::HashSet;
5use std::env;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::vec::Vec;
9
10#[derive(Deserialize, Debug, Serialize)]
11pub struct Config {
12 #[serde(default = "default_lang")]
13 pub lang: String,
14}
15
16fn default_lang() -> String {
17 "en".to_string()
18}
19impl Default for Config {
20 fn default() -> Self {
21 Config {
22 lang: default_lang(),
23 }
24 }
25}
26
27#[derive(PartialEq)]
28pub enum Status {
29 Ignored,
30 GlobalIgnored,
31 Misspelled,
32}
33
34impl Status {
35 pub fn as_str(&self) -> &'static str {
36 match self {
37 Status::Ignored => "ignored",
38 Status::GlobalIgnored => "global_ignored",
39 Status::Misspelled => "misspelled",
40 }
41 }
42}
43
44pub struct WordBad {
46 pub word: String,
47 pub file: String,
48 #[allow(dead_code)] pub line_num: usize,
50}
51
52const WORD_PATTERN: &str = r"https?://[^\s]+|\b[a-zA-Z]+(?:[''][a-zA-Z]+)*\b";
54
55fn is_url(s: &str) -> bool {
56 s.starts_with("http://") || s.starts_with("https://")
57}
58
59pub fn get_home_dir() -> PathBuf {
60 let owl_dir = env::home_dir()
61 .expect("no home directory?")
62 .join(".config/owl");
63 let dict_dir = owl_dir.join("dicts");
64 fs::create_dir_all(&dict_dir).expect("could not create home dir");
66
67 owl_dir
68}
69
70fn load_config() -> Config {
71 let path = get_home_dir().join("config.toml");
72 let maybe_config = fs::read_to_string(path);
73
74 let cfg = match maybe_config {
75 Ok(config_content) => toml::from_str(&config_content).unwrap_or_default(),
76 _ => Config::default(),
77 };
78 cfg
79}
80
81pub struct Engine {
82 dict: spellbook::Dictionary, pub word_regex: Regex, pub local_ignore_file: PathBuf, pub local_ignored_words: HashSet<String>, pub global_ignore_file: PathBuf, pub global_ignored_words: HashSet<String>, }
89
90impl Engine {
91 pub fn new(path: &Path) -> Engine {
93 const DICT_MSG: &str = "\nRun with `--init en` to download a dictionary.";
94 let home_dir = get_home_dir();
95
96 let local_ignore_file = ignorefile::get_ignorefile(path);
97 let local_ignored_words = ignorefile::load_words(&local_ignore_file);
98 let global_ignore_file = home_dir.join("spellignore").to_path_buf();
99 let global_ignored_words = ignorefile::load_words(&global_ignore_file);
100
101 let dict_dir = home_dir.join("dicts");
102
103 let cfg = load_config();
104
105 let aff_path = dict_dir.join(format!("{}.aff", cfg.lang));
106 let aff = std::fs::read_to_string(&aff_path);
107 let dic_path = dict_dir.join(format!("{}.dic", cfg.lang));
108 let dic = std::fs::read_to_string(&dic_path);
109 let dict = match (aff, dic) {
110 (Err(err), _) => {
111 panic!("Could not load {:?}: {}\n{}", aff_path, err, DICT_MSG);
112 }
113 (_, Err(err)) => {
114 panic!("Could not load {:?}: {}\n{}", dic_path, err, DICT_MSG);
115 }
116 (Ok(aff_txt), Ok(dic_txt)) => match spellbook::Dictionary::new(&aff_txt, &dic_txt) {
117 Ok(dict) => dict,
118 Err(err) => {
119 panic!(
120 "Could not load dictionary {:?} {:?}: {}\n{}",
121 aff_path, dic_path, err, DICT_MSG
122 );
123 }
124 },
125 };
126
127 Self {
128 word_regex: Regex::new(WORD_PATTERN).expect("Invalid hard-coded regex!"),
129 dict,
130 global_ignore_file,
131 global_ignored_words,
132 local_ignore_file,
133 local_ignored_words,
134 }
135 }
136
137 pub fn get_misspelled(&self, path: &str) -> Vec<WordBad> {
141 let content = fs::read_to_string(path).expect("Cannot read file");
142 let mut bad = vec![];
143
144 let lines: Vec<_> = content.lines().map(|line| line.to_string()).collect();
145
146 for (line_num, line) in lines.iter().enumerate() {
147 for word in self.word_regex.find_iter(line) {
148 if !is_url(word.as_str()) && !self.dict.check(word.as_str()) {
149 bad.push(WordBad {
150 word: word.as_str().to_string(),
151 line_num,
152 file: path.to_string(),
153 });
154 }
155 }
156 }
157 bad
158 }
159
160 pub fn get_ignore_status(&self, word: &str) -> Status {
163 if self.local_ignored_words.contains(word) {
164 Status::Ignored
165 } else if self.global_ignored_words.contains(word) {
166 Status::GlobalIgnored
167 } else {
168 Status::Misspelled
169 }
170 }
171
172 pub fn suggest(&self, word: &str, suggestions: &mut Vec<String>) {
176 self.dict.suggest(word, suggestions);
177 }
178
179 pub fn check(&self, word: &str) -> bool {
180 self.dict.check(word)
181 }
182}