cooklang_language_server/
state.rs1use std::path::Path;
2use std::sync::RwLock;
3
4use dashmap::DashMap;
5use tower_lsp::lsp_types::Url;
6
7use crate::document::Document;
8
9#[derive(Debug, Clone)]
11pub struct AisleIngredient {
12 pub name: String,
14 pub common_name: String,
16 pub category: String,
18}
19
20#[derive(Debug, Default)]
22pub struct AisleConfig {
23 pub ingredients: Vec<AisleIngredient>,
25}
26
27impl AisleConfig {
28 pub fn parse(content: &str) -> Option<Self> {
31 let result = cooklang::aisle::parse_lenient(content);
32 let (aisle_conf, warnings) = result.into_result().ok()?;
33
34 for warning in warnings.iter() {
36 tracing::warn!("aisle.conf warning: {}", warning);
37 }
38
39 let mut ingredients = Vec::new();
40 for category in &aisle_conf.categories {
41 for ingredient in &category.ingredients {
42 if let Some(common_name) = ingredient.names.first() {
43 for name in &ingredient.names {
44 ingredients.push(AisleIngredient {
45 name: name.to_string(),
46 common_name: common_name.to_string(),
47 category: category.name.to_string(),
48 });
49 }
50 }
51 }
52 }
53 Some(AisleConfig { ingredients })
54 }
55
56 pub fn load_from_workspace(workspace_path: &Path) -> Option<Self> {
58 let config_path = workspace_path.join("config").join("aisle.conf");
60 if config_path.exists() {
61 if let Ok(content) = std::fs::read_to_string(&config_path) {
62 tracing::info!("Loading aisle.conf from {:?}", config_path);
63 return Self::parse(&content);
64 }
65 }
66
67 let root_path = workspace_path.join("aisle.conf");
69 if root_path.exists() {
70 if let Ok(content) = std::fs::read_to_string(&root_path) {
71 tracing::info!("Loading aisle.conf from {:?}", root_path);
72 return Self::parse(&content);
73 }
74 }
75
76 None
77 }
78}
79
80pub struct ServerState {
82 pub documents: DashMap<Url, Document>,
83 pub aisle_config: RwLock<Option<AisleConfig>>,
85}
86
87impl ServerState {
88 pub fn new() -> Self {
89 Self {
90 documents: DashMap::new(),
91 aisle_config: RwLock::new(None),
92 }
93 }
94
95 pub fn load_aisle_config(&self, workspace_path: &Path) {
97 if let Some(config) = AisleConfig::load_from_workspace(workspace_path) {
98 let count = config.ingredients.len();
99 if let Ok(mut guard) = self.aisle_config.write() {
100 *guard = Some(config);
101 tracing::info!("Loaded {} ingredients from aisle.conf", count);
102 }
103 }
104 }
105
106 pub fn get_aisle_ingredients(&self) -> Vec<AisleIngredient> {
108 if let Ok(guard) = self.aisle_config.read() {
109 if let Some(ref config) = *guard {
110 return config.ingredients.clone();
111 }
112 }
113 Vec::new()
114 }
115
116 pub fn open_document(&self, uri: Url, version: i32, content: String) {
117 let doc = Document::new(uri.clone(), version, content);
118 self.documents.insert(uri, doc);
119 }
120
121 pub fn update_document(&self, uri: &Url, version: i32, content: String) {
122 if let Some(mut doc) = self.documents.get_mut(uri) {
123 doc.update(version, content);
124 }
125 }
126
127 pub fn close_document(&self, uri: &Url) {
128 self.documents.remove(uri);
129 }
130
131 pub fn get_document(&self, uri: &Url) -> Option<dashmap::mapref::one::Ref<'_, Url, Document>> {
132 self.documents.get(uri)
133 }
134}
135
136impl Default for ServerState {
137 fn default() -> Self {
138 Self::new()
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use super::*;
145
146 #[test]
147 fn test_aisle_config_parse() {
148 let content = r#"
149[produce]
150potatoes
151carrots
152onions|yellow onion|white onion
153
154[dairy]
155milk
156butter
157cheese|cheddar|parmesan
158"#;
159 let config = AisleConfig::parse(content).unwrap();
160 assert!(!config.ingredients.is_empty());
161
162 let potatoes = config
164 .ingredients
165 .iter()
166 .find(|i| i.name == "potatoes")
167 .unwrap();
168 assert_eq!(potatoes.category, "produce");
169 assert_eq!(potatoes.common_name, "potatoes");
170
171 let yellow_onion = config
173 .ingredients
174 .iter()
175 .find(|i| i.name == "yellow onion")
176 .unwrap();
177 assert_eq!(yellow_onion.category, "produce");
178 assert_eq!(yellow_onion.common_name, "onions");
179
180 let cheddar = config
182 .ingredients
183 .iter()
184 .find(|i| i.name == "cheddar")
185 .unwrap();
186 assert_eq!(cheddar.category, "dairy");
187 assert_eq!(cheddar.common_name, "cheese");
188 }
189
190 #[test]
191 fn test_aisle_config_lenient_parsing() {
192 let content = r#"
194orphan ingredient before any category
195[produce]
196apple
197banana
198apple
199
200[dairy]
201milk
202
203[produce]
204carrot
205"#;
206 let config = AisleConfig::parse(content).unwrap();
208
209 assert!(!config.ingredients.is_empty());
211
212 let apple = config.ingredients.iter().find(|i| i.name == "apple");
214 assert!(apple.is_some());
215 assert_eq!(apple.unwrap().category, "produce");
216
217 let banana = config.ingredients.iter().find(|i| i.name == "banana");
219 assert!(banana.is_some());
220
221 let milk = config.ingredients.iter().find(|i| i.name == "milk");
223 assert!(milk.is_some());
224 assert_eq!(milk.unwrap().category, "dairy");
225
226 let apple_count = config.ingredients.iter().filter(|i| i.name == "apple").count();
228 assert_eq!(apple_count, 1);
229 }
230}