1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use markdown_lsp;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::path::Path;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct LspConfig {
10 #[serde(default)]
12 pub servers: HashMap<String, LspServerConfig>,
13}
14
15impl Default for LspConfig {
16 fn default() -> Self {
17 let mut servers = HashMap::new();
18
19 servers.insert(
21 "rust".to_string(),
22 LspServerConfig {
23 command: "rust-analyzer".to_string(),
24 args: vec![],
25 embedded: false,
26 enable_completion: true,
27 enable_diagnostics: true,
28 enable_goto_definition: true,
29 },
30 );
31
32 servers.insert(
34 "python".to_string(),
35 LspServerConfig {
36 command: "pyright-langserver".to_string(),
37 args: vec!["--stdio".to_string()],
38 embedded: false,
39 enable_completion: true,
40 enable_diagnostics: true,
41 enable_goto_definition: true,
42 },
43 );
44
45 servers.insert(
47 "mq".to_string(),
48 LspServerConfig {
49 command: "mq-lsp".to_string(),
50 args: vec![],
51 embedded: false,
52 enable_completion: true,
53 enable_diagnostics: true,
54 enable_goto_definition: true,
55 },
56 );
57
58 servers.insert(
60 "markdown".to_string(),
61 LspServerConfig {
62 command: String::new(),
63 args: vec![],
64 embedded: true,
65 enable_completion: true,
66 enable_diagnostics: true,
67 enable_goto_definition: true,
68 },
69 );
70
71 Self { servers }
72 }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct LspServerConfig {
78 #[serde(default)]
81 pub command: String,
82
83 #[serde(default)]
85 pub args: Vec<String>,
86
87 #[serde(default)]
90 pub embedded: bool,
91
92 #[serde(default = "default_true")]
94 pub enable_completion: bool,
95
96 #[serde(default = "default_true")]
98 pub enable_diagnostics: bool,
99
100 #[serde(default = "default_true")]
102 pub enable_goto_definition: bool,
103}
104
105fn default_true() -> bool {
106 true
107}
108
109fn default_false() -> bool {
110 false
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct EditorConfig {
116 #[serde(default = "default_true")]
118 pub show_line_numbers: bool,
119
120 #[serde(default = "default_true")]
122 pub show_current_line_highlight: bool,
123
124 #[serde(default = "default_theme")]
126 pub theme: String,
127
128 #[serde(default = "default_false")]
131 pub use_semantic_tokens: bool,
132}
133
134impl Default for EditorConfig {
135 fn default() -> Self {
136 Self {
137 show_line_numbers: true,
138 show_current_line_highlight: true,
139 theme: default_theme(),
140 use_semantic_tokens: false,
141 }
142 }
143}
144
145fn default_theme() -> String {
146 "base16-ocean.dark".to_string()
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize, Default)]
151pub struct Config {
152 #[serde(default)]
153 pub editor: EditorConfig,
154 pub keybindings: Keybindings,
155 #[serde(default)]
156 pub lsp: LspConfig,
157}
158
159impl Config {
160 pub fn load_from_file(path: impl AsRef<Path>) -> miette::Result<Self> {
162 let content = std::fs::read_to_string(path.as_ref())
163 .map_err(|e| miette::miette!("Failed to read config file: {}", e))?;
164
165 toml::from_str(&content).map_err(|e| miette::miette!("Failed to parse config file: {}", e))
166 }
167
168 pub fn save_to_file(&self, path: impl AsRef<Path>) -> miette::Result<()> {
170 let content = toml::to_string_pretty(self)
171 .map_err(|e| miette::miette!("Failed to serialize config: {}", e))?;
172
173 std::fs::write(path.as_ref(), content)
174 .map_err(|e| miette::miette!("Failed to write config file: {}", e))?;
175
176 Ok(())
177 }
178
179 pub fn load_or_default() -> Self {
181 let config_path = Self::default_config_path();
182
183 if config_path.exists() {
184 Self::load_from_file(&config_path).unwrap_or_default()
185 } else {
186 Self::default()
187 }
188 }
189
190 pub fn default_config_path() -> std::path::PathBuf {
192 if let Some(config_dir) = dirs::config_dir() {
193 config_dir.join("mq").join("edit").join("config.toml")
194 } else {
195 std::path::PathBuf::from(".mq-edit.toml")
196 }
197 }
198
199 pub fn lsp_server_configs(&self) -> HashMap<String, markdown_lsp::LspServerConfig> {
201 self.lsp
202 .servers
203 .iter()
204 .map(|(key, config)| {
205 (
206 key.clone(),
207 markdown_lsp::LspServerConfig {
208 command: config.command.clone(),
209 args: config.args.clone(),
210 embedded: config.embedded,
211 enable_completion: config.enable_completion,
212 enable_diagnostics: config.enable_diagnostics,
213 enable_goto_definition: config.enable_goto_definition,
214 },
215 )
216 })
217 .collect()
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
223pub struct Keybindings {
224 pub quit: KeyBinding,
226
227 pub quit_alt: KeyBinding,
229
230 pub save: KeyBinding,
232
233 pub toggle_file_browser: KeyBinding,
235
236 pub toggle_file_browser_alt: KeyBinding,
238
239 pub goto_definition: KeyBinding,
241
242 pub navigate_back: KeyBinding,
244
245 pub navigate_forward: KeyBinding,
247
248 pub search: KeyBinding,
250
251 pub replace: KeyBinding,
253
254 pub undo: KeyBinding,
256
257 pub redo: KeyBinding,
259
260 pub close_browser: KeyBinding,
262
263 pub toggle_line_numbers: KeyBinding,
265
266 pub toggle_current_line_highlight: KeyBinding,
268
269 pub goto_line: KeyBinding,
271
272 pub execute_mq_query: KeyBinding,
274}
275
276impl Default for Keybindings {
277 fn default() -> Self {
278 Self {
279 quit: KeyBinding {
281 code: "q".to_string(),
282 modifiers: vec!["ctrl".to_string()],
283 },
284 quit_alt: KeyBinding {
286 code: "esc".to_string(),
287 modifiers: vec![],
288 },
289 save: KeyBinding {
291 code: "s".to_string(),
292 modifiers: vec!["ctrl".to_string()],
293 },
294 toggle_file_browser: KeyBinding {
296 code: "b".to_string(),
297 modifiers: vec!["alt".to_string()],
298 },
299 toggle_file_browser_alt: KeyBinding {
300 code: "f2".to_string(),
301 modifiers: vec![],
302 },
303 goto_definition: KeyBinding {
305 code: "d".to_string(),
306 modifiers: vec!["ctrl".to_string()],
307 },
308 navigate_back: KeyBinding {
310 code: "b".to_string(),
311 modifiers: vec!["ctrl".to_string()],
312 },
313 navigate_forward: KeyBinding {
315 code: "f".to_string(),
316 modifiers: vec!["ctrl".to_string()],
317 },
318 search: KeyBinding {
320 code: "f3".to_string(),
321 modifiers: vec![],
322 },
323 replace: KeyBinding {
325 code: "f4".to_string(),
326 modifiers: vec![],
327 },
328 undo: KeyBinding {
330 code: "z".to_string(),
331 modifiers: vec!["ctrl".to_string()],
332 },
333 redo: KeyBinding {
335 code: "y".to_string(),
336 modifiers: vec!["ctrl".to_string()],
337 },
338 close_browser: KeyBinding {
340 code: "esc".to_string(),
341 modifiers: vec![],
342 },
343 toggle_line_numbers: KeyBinding {
345 code: "l".to_string(),
346 modifiers: vec!["ctrl".to_string()],
347 },
348 toggle_current_line_highlight: KeyBinding {
350 code: "l".to_string(),
351 modifiers: vec!["ctrl".to_string(), "shift".to_string()],
352 },
353 goto_line: KeyBinding {
355 code: "g".to_string(),
356 modifiers: vec!["ctrl".to_string()],
357 },
358 execute_mq_query: KeyBinding {
360 code: "e".to_string(),
361 modifiers: vec!["ctrl".to_string()],
362 },
363 }
364 }
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
369pub struct KeyBinding {
370 pub code: String,
371 pub modifiers: Vec<String>,
372}
373
374impl KeyBinding {
375 pub fn matches(&self, key: &KeyEvent) -> bool {
377 let code_matches = match self.code.to_lowercase().as_str() {
379 "esc" => matches!(key.code, KeyCode::Esc),
380 "enter" => matches!(key.code, KeyCode::Enter),
381 "backspace" => matches!(key.code, KeyCode::Backspace),
382 "tab" => matches!(key.code, KeyCode::Tab),
383 "space" => matches!(key.code, KeyCode::Char(' ')),
384 "f1" => matches!(key.code, KeyCode::F(1)),
385 "f2" => matches!(key.code, KeyCode::F(2)),
386 "f3" => matches!(key.code, KeyCode::F(3)),
387 "f4" => matches!(key.code, KeyCode::F(4)),
388 "f5" => matches!(key.code, KeyCode::F(5)),
389 "f6" => matches!(key.code, KeyCode::F(6)),
390 "f7" => matches!(key.code, KeyCode::F(7)),
391 "f8" => matches!(key.code, KeyCode::F(8)),
392 "f9" => matches!(key.code, KeyCode::F(9)),
393 "f10" => matches!(key.code, KeyCode::F(10)),
394 "f11" => matches!(key.code, KeyCode::F(11)),
395 "f12" => matches!(key.code, KeyCode::F(12)),
396 s if s.len() == 1 => {
397 if let Some(ch) = s.chars().next() {
398 matches!(key.code, KeyCode::Char(c) if c.to_lowercase().to_string() == ch.to_lowercase().to_string())
399 } else {
400 false
401 }
402 }
403 _ => false,
404 };
405
406 if !code_matches {
407 return false;
408 }
409
410 let mut expected_modifiers = KeyModifiers::empty();
412 for modifier in &self.modifiers {
413 match modifier.to_lowercase().as_str() {
414 "ctrl" | "control" => expected_modifiers |= KeyModifiers::CONTROL,
415 "shift" => expected_modifiers |= KeyModifiers::SHIFT,
416 "alt" => expected_modifiers |= KeyModifiers::ALT,
417 _ => {}
418 }
419 }
420
421 key.modifiers == expected_modifiers
422 }
423
424 pub fn display(&self) -> String {
426 let mut parts = Vec::new();
427
428 for modifier in &self.modifiers {
429 match modifier.to_lowercase().as_str() {
430 "ctrl" | "control" => parts.push("Ctrl".to_string()),
431 "shift" => parts.push("Shift".to_string()),
432 "alt" => parts.push("Alt".to_string()),
433 _ => parts.push(modifier.clone()),
434 }
435 }
436
437 parts.push(self.code.to_uppercase());
438 parts.join("+")
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_keybinding_matches() {
448 let kb = KeyBinding {
449 code: "q".to_string(),
450 modifiers: vec!["ctrl".to_string()],
451 };
452
453 let key = KeyEvent::new(KeyCode::Char('q'), KeyModifiers::CONTROL);
454 assert!(kb.matches(&key));
455
456 let key2 = KeyEvent::new(
457 KeyCode::Char('q'),
458 KeyModifiers::CONTROL | KeyModifiers::SHIFT,
459 );
460 assert!(!kb.matches(&key2));
461 }
462
463 #[test]
464 fn test_keybinding_display() {
465 let kb = KeyBinding {
466 code: "s".to_string(),
467 modifiers: vec!["ctrl".to_string()],
468 };
469 assert_eq!(kb.display(), "Ctrl+S");
470
471 let kb2 = KeyBinding {
472 code: "q".to_string(),
473 modifiers: vec!["ctrl".to_string()],
474 };
475 assert_eq!(kb2.display(), "Ctrl+Q");
476 }
477
478 #[test]
479 fn test_default_config() {
480 let config = Config::default();
481 assert_eq!(config.keybindings.save.code, "s");
482 assert_eq!(config.keybindings.quit.code, "q");
483 assert_eq!(config.keybindings.quit.modifiers.len(), 1);
484 assert_eq!(config.keybindings.quit_alt.code, "esc");
485 assert_eq!(config.keybindings.quit_alt.modifiers.len(), 0);
486 }
487
488 #[test]
489 fn test_lsp_config_default() {
490 let lsp_config = LspConfig::default();
491
492 assert!(lsp_config.servers.contains_key("rust"));
494 assert!(lsp_config.servers.contains_key("python"));
495
496 let rust_config = &lsp_config.servers["rust"];
498 assert_eq!(rust_config.command, "rust-analyzer");
499 assert!(rust_config.args.is_empty());
500 assert!(rust_config.enable_completion);
501 assert!(rust_config.enable_diagnostics);
502 assert!(rust_config.enable_goto_definition);
503
504 let python_config = &lsp_config.servers["python"];
506 assert_eq!(python_config.command, "pyright-langserver");
507 assert_eq!(python_config.args, vec!["--stdio"]);
508 assert!(python_config.enable_completion);
509 assert!(python_config.enable_diagnostics);
510 assert!(python_config.enable_goto_definition);
511 }
512
513 #[test]
514 fn test_lsp_config_serialization() {
515 let mut servers = HashMap::new();
516 servers.insert(
517 "test".to_string(),
518 LspServerConfig {
519 command: "test-lsp".to_string(),
520 args: vec!["--test".to_string()],
521 embedded: false,
522 enable_completion: true,
523 enable_diagnostics: false,
524 enable_goto_definition: true,
525 },
526 );
527
528 let lsp_config = LspConfig { servers };
529
530 let toml_string = toml::to_string(&lsp_config).unwrap();
532
533 let deserialized: LspConfig = toml::from_str(&toml_string).unwrap();
535
536 assert!(deserialized.servers.contains_key("test"));
537 let test_config = &deserialized.servers["test"];
538 assert_eq!(test_config.command, "test-lsp");
539 assert_eq!(test_config.args, vec!["--test"]);
540 assert!(test_config.enable_completion);
541 assert!(!test_config.enable_diagnostics);
542 assert!(test_config.enable_goto_definition);
543 }
544
545 #[test]
546 fn test_config_with_lsp() {
547 let config = Config::default();
548
549 assert!(!config.lsp.servers.is_empty());
551 assert!(config.lsp.servers.contains_key("rust"));
552 }
553
554 #[test]
555 fn test_lsp_server_config_defaults() {
556 let toml = r#"
558 command = "my-lsp"
559 "#;
560
561 let config: LspServerConfig = toml::from_str(toml).unwrap();
562 assert_eq!(config.command, "my-lsp");
563 assert!(config.args.is_empty());
564 assert!(config.enable_completion);
565 assert!(config.enable_diagnostics);
566 assert!(config.enable_goto_definition);
567 }
568}