ls-plus 0.0.1

Enhanced ls command with modern features - supports both cargo and npm installation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
// 导入依赖项
use crate::error::{LsPlusError, Result}; // 错误处理类型
use serde::{Deserialize, Serialize}; // 序列化和反序列化支持
use std::collections::HashMap; // 哈希映射容器
use std::fs; // 文件系统操作
use std::path::{Path, PathBuf}; // 路径处理

/// 目录描述信息结构体
///
/// 存储单个目录的描述信息,包括路径、描述内容和时间戳。
/// 用于为项目中的目录提供自定义的说明信息,帮助用户理解目录的用途。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DirectoryDescription {
    /// 目录的完整路径
    pub path: PathBuf,
    /// 目录的描述文本
    pub description: String,
    /// 描述创建时间
    pub created_at: chrono::DateTime<chrono::Local>,
    /// 描述最后更新时间
    pub updated_at: chrono::DateTime<chrono::Local>,
}

/// 描述数据库结构体
///
/// 管理所有目录描述的数据库,使用哈希映射来存储路径到描述的映射关系。
/// 支持序列化到 JSON 文件进行持久化存储。
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DescriptionDatabase {
    /// 存储路径字符串到目录描述的映射
    /// Key: 目录的规范化路径字符串
    /// Value: 对应的目录描述信息
    pub descriptions: HashMap<String, DirectoryDescription>,
}

impl DescriptionDatabase {
    /// 创建新的空描述数据库
    ///
    /// # 返回值
    /// * `Self` - 新的空数据库实例
    pub fn new() -> Self {
        Self {
            descriptions: HashMap::new(), // 初始化为空的哈希映射
        }
    }

    /// 获取描述数据库文件路径
    ///
    /// 返回存储目录描述数据的 JSON 文件路径。
    /// 文件位于应用程序配置目录下的 descriptions.json。
    ///
    /// # 返回值
    /// * `Result<PathBuf>` - 数据库文件路径
    ///
    /// # 错误
    /// * 无法确定配置目录时返回错误
    pub fn database_file() -> Result<PathBuf> {
        let config_dir = crate::config::Config::config_dir()?; // 获取配置目录
        Ok(config_dir.join("descriptions.json")) // 拼接数据库文件名
    }

    /// 从文件加载描述数据库
    ///
    /// 从 JSON 文件中加载已保存的描述数据库。如果文件不存在,
    /// 则返回一个新的空数据库。
    ///
    /// # 返回值
    /// * `Result<Self>` - 成功时返回加载的数据库,失败时返回错误
    ///
    /// # 错误
    /// * 文件读取失败
    /// * JSON 格式解析失败
    pub fn load() -> Result<Self> {
        let db_file = Self::database_file()?;

        // 如果数据库文件不存在,返回空数据库
        if !db_file.exists() {
            return Ok(Self::new());
        }

        // 读取 JSON 文件内容
        let content = fs::read_to_string(&db_file)
            .map_err(|e| LsPlusError::system_error(format!("无法读取描述数据库: {e}")))?;

        // 解析 JSON 内容为数据库结构
        let database: Self = serde_json::from_str(&content)
            .map_err(|e| LsPlusError::format_error(format!("描述数据库格式错误: {e}")))?;

        Ok(database)
    }

    /// 保存描述数据库到文件
    ///
    /// 将当前数据库内容序列化为格式化的 JSON 并保存到文件。
    /// 如果配置目录不存在,会自动创建。
    ///
    /// # 返回值
    /// * `Result<()>` - 成功时返回空结果,失败时返回错误
    ///
    /// # 错误
    /// * 配置目录创建失败
    /// * JSON 序列化失败
    /// * 文件写入失败
    pub fn save(&self) -> Result<()> {
        let config_dir = crate::config::Config::config_dir()?;
        let db_file = Self::database_file()?;

        // 确保配置目录存在,如果不存在则创建
        if !config_dir.exists() {
            fs::create_dir_all(&config_dir)
                .map_err(|e| LsPlusError::system_error(format!("无法创建配置目录: {e}")))?;
        }

        // 将数据库序列化为格式化的 JSON 字符串
        let content = serde_json::to_string_pretty(self)
            .map_err(|e| LsPlusError::format_error(format!("无法序列化描述数据库: {e}")))?;

        // 将 JSON 内容写入文件
        fs::write(&db_file, content)
            .map_err(|e| LsPlusError::system_error(format!("无法保存描述数据库: {e}")))?;

        Ok(())
    }

    /// 为目录添加或更新描述
    ///
    /// 为指定路径的目录设置描述信息。如果该目录已有描述,则更新现有描述;
    /// 如果没有,则创建新的描述条目。
    ///
    /// # 参数
    /// * `path` - 目录路径,可以是相对路径或绝对路径
    /// * `description` - 要设置的描述文本
    ///
    /// # 返回值
    /// * `Result<()>` - 成功时返回空结果,失败时返回错误
    ///
    /// # 错误
    /// * 路径不存在或无法访问
    /// * 路径规范化失败
    ///
    /// # 功能说明
    /// - 使用规范化路径作为唯一标识符,避免相对路径和绝对路径的冲突
    /// - 自动更新时间戳,记录创建和最后修改时间
    pub fn set_description<P: AsRef<Path>>(&mut self, path: P, description: String) -> Result<()> {
        let path = path.as_ref();
        // 将路径规范化为绝对路径,解析符号链接
        let canonical_path = path
            .canonicalize()
            .map_err(|_e| LsPlusError::path_not_found(path.to_path_buf()))?;

        // 使用规范化路径的字符串形式作为哈希映射的键
        let path_key = canonical_path.to_string_lossy().to_string();
        let now = chrono::Local::now();

        if let Some(existing) = self.descriptions.get_mut(&path_key) {
            // 更新现有描述
            existing.description = description;
            existing.updated_at = now; // 更新修改时间
        } else {
            // 创建新的描述条目
            let desc = DirectoryDescription {
                path: canonical_path,
                description,
                created_at: now, // 记录创建时间
                updated_at: now, // 记录修改时间
            };
            self.descriptions.insert(path_key, desc);
        }

        Ok(())
    }

    /// 获取目录描述
    ///
    /// 根据给定路径查找对应的目录描述信息。
    ///
    /// # 参数
    /// * `path` - 要查找描述的目录路径
    ///
    /// # 返回值
    /// * `Option<&DirectoryDescription>` - 找到时返回描述的引用,否则返回 None
    ///
    /// # 查找逻辑
    /// - 首先将输入路径规范化为绝对路径
    /// - 如果路径规范化失败(如路径不存在),返回 None
    /// - 在描述数据库中查找匹配的条目
    pub fn get_description<P: AsRef<Path>>(&self, path: P) -> Option<&DirectoryDescription> {
        let path = path.as_ref();
        // 尝试规范化路径,如果失败则返回 None
        if let Ok(canonical_path) = path.canonicalize() {
            let path_key = canonical_path.to_string_lossy().to_string();
            self.descriptions.get(&path_key) // 在哈希映射中查找
        } else {
            None // 路径无效或不存在
        }
    }

    /// 删除目录描述
    ///
    /// 从数据库中删除指定路径的目录描述。
    ///
    /// # 参数
    /// * `path` - 要删除描述的目录路径
    ///
    /// # 返回值
    /// * `Result<bool>` - 成功时返回是否实际删除了条目,失败时返回错误
    ///   - `true`: 成功找到并删除了描述
    ///   - `false`: 指定路径没有对应的描述
    ///
    /// # 错误
    /// * 路径不存在或无法访问
    /// * 路径规范化失败
    pub fn remove_description<P: AsRef<Path>>(&mut self, path: P) -> Result<bool> {
        let path = path.as_ref();
        // 规范化路径以确保键的一致性
        let canonical_path = path
            .canonicalize()
            .map_err(|_e| LsPlusError::path_not_found(path.to_path_buf()))?;

        let path_key = canonical_path.to_string_lossy().to_string();
        // 尝试从哈希映射中删除条目,返回是否成功删除
        Ok(self.descriptions.remove(&path_key).is_some())
    }

    /// 列出所有描述
    ///
    /// 获取数据库中所有目录描述的列表,按路径排序。
    ///
    /// # 返回值
    /// * `Vec<&DirectoryDescription>` - 按路径排序的所有描述列表
    ///
    /// # 排序规则
    /// - 按目录路径的字典序排序
    /// - 确保输出的一致性和可预测性
    pub fn list_all(&self) -> Vec<&DirectoryDescription> {
        // 收集所有描述的引用
        let mut descriptions: Vec<&DirectoryDescription> = self.descriptions.values().collect();
        // 按路径排序,提供一致的显示顺序
        descriptions.sort_by(|a, b| a.path.cmp(&b.path));
        descriptions
    }

    /// 搜索描述(按描述内容和路径)
    ///
    /// 在所有目录描述中搜索包含指定查询字符串的条目。
    /// 搜索范围包括描述内容和目录路径。
    ///
    /// # 参数
    /// * `query` - 搜索查询字符串
    ///
    /// # 返回值
    /// * `Vec<&DirectoryDescription>` - 匹配的描述列表
    ///
    /// # 搜索规则
    /// - 不区分大小写的模糊匹配
    /// - 同时搜索描述内容和目录路径
    /// - 只要任一字段包含查询字符串就算匹配
    pub fn search_descriptions(&self, query: &str) -> Vec<&DirectoryDescription> {
        let query_lower = query.to_lowercase(); // 转为小写以实现不区分大小写搜索
        self.descriptions
            .values()
            .filter(|desc| {
                // 在描述内容中搜索
                desc.description.to_lowercase().contains(&query_lower) ||
                // 在路径中搜索
                desc.path.to_string_lossy().to_lowercase().contains(&query_lower)
            })
            .collect()
    }

    /// 清理不存在的目录描述
    ///
    /// 扫描数据库中的所有描述,删除那些指向不存在目录的条目。
    /// 这有助于保持数据库的整洁,避免无效的描述信息累积。
    ///
    /// # 返回值
    /// * `usize` - 被删除的无效描述条目数量
    ///
    /// # 清理逻辑
    /// - 遍历所有描述条目
    /// - 检查每个条目对应的目录路径是否仍然存在
    /// - 删除指向不存在目录的描述
    /// - 返回删除的条目数量
    pub fn cleanup_missing_directories(&mut self) -> usize {
        let mut to_remove = Vec::new();

        // 扫描所有描述,标记需要删除的条目
        for (key, desc) in &self.descriptions {
            if !desc.path.exists() {
                // 检查目录是否仍然存在
                to_remove.push(key.clone()); // 记录需要删除的键
            }
        }

        let removed_count = to_remove.len();
        // 删除所有标记的条目
        for key in to_remove {
            self.descriptions.remove(&key);
        }

        removed_count // 返回删除的条目数量
    }
}

/// 管理目录描述的主要接口
///
/// 这是应用程序与描述数据库交互的主要接口,提供了高级的描述管理功能。
/// 它封装了底层的数据库操作,并添加了额外的验证和便利功能。
///
/// # 功能特性
/// - 自动加载和保存数据库
/// - 路径和类型验证
/// - 事务性操作(操作后自动保存)
/// - 用户友好的错误处理
pub struct DescriptionManager {
    /// 底层的描述数据库实例
    database: DescriptionDatabase,
}

impl DescriptionManager {
    /// 创建新的描述管理器实例
    ///
    /// 自动从文件加载现有的描述数据库,如果数据库文件不存在,
    /// 则创建一个空的数据库。
    ///
    /// # 返回值
    /// * `Result<Self>` - 成功时返回管理器实例,失败时返回错误
    ///
    /// # 错误
    /// * 数据库文件读取失败
    /// * JSON 格式解析失败
    /// * 配置目录访问失败
    pub fn new() -> Result<Self> {
        let database = DescriptionDatabase::load()?; // 加载现有数据库或创建新的
        Ok(Self { database })
    }

    /// 为目录设置描述
    ///
    /// 为指定目录设置描述信息,包含完整的验证和自动保存功能。
    ///
    /// # 参数
    /// * `path` - 目录路径
    /// * `description` - 描述文本
    ///
    /// # 返回值
    /// * `Result<()>` - 成功时返回空结果,失败时返回错误
    ///
    /// # 错误
    /// * 路径不存在
    /// * 路径不是目录
    /// * 数据库保存失败
    ///
    /// # 验证规则
    /// - 路径必须存在
    /// - 路径必须是目录(不能是文件)
    /// - 设置成功后自动保存到文件
    pub fn set_description<P: AsRef<Path>>(&mut self, path: P, description: String) -> Result<()> {
        let path = path.as_ref();

        // 验证路径存在
        if !path.exists() {
            return Err(LsPlusError::path_not_found(path.to_path_buf()));
        }

        // 验证路径是目录而不是文件
        if !path.is_dir() {
            return Err(LsPlusError::format_error("只能为目录添加描述"));
        }

        // 在数据库中设置描述
        self.database.set_description(path, description)?;
        // 自动保存数据库到文件
        self.database.save()?;

        Ok(())
    }

    /// 获取目录描述
    ///
    /// 查找指定目录的描述信息。
    ///
    /// # 参数
    /// * `path` - 目录路径
    ///
    /// # 返回值
    /// * `Option<&DirectoryDescription>` - 找到时返回描述引用,否则返回 None
    pub fn get_description<P: AsRef<Path>>(&self, path: P) -> Option<&DirectoryDescription> {
        self.database.get_description(path)
    }

    /// 删除目录描述
    ///
    /// 删除指定目录的描述信息,并在成功删除时自动保存数据库。
    ///
    /// # 参数
    /// * `path` - 目录路径
    ///
    /// # 返回值
    /// * `Result<bool>` - 成功时返回是否实际删除了描述
    ///   - `true`: 找到并删除了描述
    ///   - `false`: 指定目录没有描述
    ///
    /// # 错误
    /// * 路径处理失败
    /// * 数据库保存失败
    pub fn remove_description<P: AsRef<Path>>(&mut self, path: P) -> Result<bool> {
        let removed = self.database.remove_description(path)?;
        // 只有在实际删除了描述时才保存数据库
        if removed {
            self.database.save()?;
        }
        Ok(removed)
    }

    /// 列出所有描述
    ///
    /// 获取数据库中所有目录描述的列表,按路径排序。
    ///
    /// # 返回值
    /// * `Vec<&DirectoryDescription>` - 所有描述的排序列表
    pub fn list_all(&self) -> Vec<&DirectoryDescription> {
        self.database.list_all()
    }

    /// 搜索描述
    ///
    /// 在所有描述中搜索包含指定查询字符串的条目。
    ///
    /// # 参数
    /// * `query` - 搜索查询字符串
    ///
    /// # 返回值
    /// * `Vec<&DirectoryDescription>` - 匹配的描述列表
    pub fn search(&self, query: &str) -> Vec<&DirectoryDescription> {
        self.database.search_descriptions(query)
    }

    /// 清理不存在的目录
    ///
    /// 删除所有指向不存在目录的描述条目,并在有删除时自动保存数据库。
    ///
    /// # 返回值
    /// * `Result<usize>` - 成功时返回删除的条目数量,失败时返回错误
    ///
    /// # 错误
    /// * 数据库保存失败
    pub fn cleanup(&mut self) -> Result<usize> {
        let removed_count = self.database.cleanup_missing_directories();
        // 只有在实际删除了条目时才保存数据库
        if removed_count > 0 {
            self.database.save()?;
        }
        Ok(removed_count)
    }
}

/// 处理描述相关的 CLI 命令
///
/// 这是描述功能的命令行接口处理函数,根据用户的操作类型执行相应的功能。
/// 支持设置、获取、删除、列出、搜索和清理描述等操作。
///
/// # 参数
/// * `action` - 用户请求的描述操作类型
///
/// # 返回值
/// * `crate::Result<()>` - 成功时返回空结果,失败时返回错误
///
/// # 支持的操作
/// - `Set`: 为目录设置描述
/// - `Get`: 获取目录的描述信息
/// - `Remove`: 删除目录的描述
/// - `List`: 列出所有描述
/// - `Search`: 搜索包含指定内容的描述
/// - `Cleanup`: 清理无效的描述条目
pub async fn handle_describe_command(action: crate::cli::DescribeAction) -> crate::Result<()> {
    use crate::cli::DescribeAction;
    use console::style;

    match action {
        // 设置目录描述
        DescribeAction::Set { path, description } => {
            let mut manager = DescriptionManager::new()?;
            manager.set_description(&path, description.clone())?;

            // 显示成功消息,使用颜色突出显示路径和描述
            println!(
                "✅ 已为目录 {} 设置描述: {}",
                style(path.display()).cyan(), // 目录路径用青色显示
                style(&description).green()   // 描述内容用绿色显示
            );
        }

        // 获取目录描述
        DescribeAction::Get { path } => {
            let manager = DescriptionManager::new()?;

            if let Some(desc) = manager.get_description(&path) {
                // 显示详细的描述信息,包括路径、内容和时间戳
                println!("📁 {}", style(&desc.path.display()).cyan().bold()); // 目录路径(加粗青色)
                println!("📝 {}", desc.description); // 描述内容
                println!(
                    "🕐 创建时间: {}",
                    desc.created_at.format("%Y-%m-%d %H:%M:%S")
                ); // 创建时间
                println!(
                    "🕑 更新时间: {}",
                    desc.updated_at.format("%Y-%m-%d %H:%M:%S")
                ); // 更新时间
            } else {
                // 没有找到描述时的提示
                println!("❌ 目录 {} 没有描述", style(path.display()).yellow());
            }
        }

        // 删除目录描述
        DescribeAction::Remove { path } => {
            let mut manager = DescriptionManager::new()?;

            if manager.remove_description(&path)? {
                // 成功删除描述
                println!("✅ 已删除目录 {} 的描述", style(path.display()).cyan());
            } else {
                // 没有找到可删除的描述
                println!("❌ 目录 {} 没有描述可删除", style(path.display()).yellow());
            }
        }

        // 列出所有描述
        DescribeAction::List => {
            let manager = DescriptionManager::new()?;
            let descriptions = manager.list_all();

            if descriptions.is_empty() {
                // 没有任何描述时的提示
                println!("📭 暂无目录描述");
            } else {
                // 显示所有描述的标题和数量
                println!("📋 所有目录描述 ({} 个):", descriptions.len());
                println!();

                // 遍历并显示每个描述
                for desc in descriptions {
                    println!("📁 {}", style(&desc.path.display()).cyan().bold()); // 目录路径
                    println!("   📝 {}", desc.description); // 描述内容(缩进显示)
                    println!("   🕐 {}", desc.updated_at.format("%Y-%m-%d %H:%M:%S")); // 更新时间
                    println!(); // 空行分隔
                }
            }
        }

        // 搜索描述
        DescribeAction::Search { query } => {
            let manager = DescriptionManager::new()?;
            let results = manager.search(&query);

            if results.is_empty() {
                // 没有找到匹配结果时的提示
                println!("🔍 未找到包含 \"{}\" 的描述", style(&query).yellow());
            } else {
                // 显示搜索结果的标题和数量
                println!("🔍 搜索结果 ({} 个):", results.len());
                println!();

                // 遍历并显示每个匹配的描述
                for desc in results {
                    println!("📁 {}", style(&desc.path.display()).cyan().bold()); // 目录路径

                    // 高亮显示匹配的文本(在描述中突出显示搜索词)
                    let highlighted_desc = highlight_search_term(&desc.description, &query);
                    println!("   📝 {highlighted_desc}"); // 高亮的描述内容
                    println!("   🕐 {}", desc.updated_at.format("%Y-%m-%d %H:%M:%S")); // 更新时间
                    println!(); // 空行分隔
                }
            }
        }

        // 清理无效描述
        DescribeAction::Cleanup => {
            let mut manager = DescriptionManager::new()?;
            let removed_count = manager.cleanup()?;

            if removed_count > 0 {
                // 有删除无效描述时的提示
                println!("✅ 已清理 {removed_count} 个不存在目录的描述");
            } else {
                // 没有需要清理的描述时的提示
                println!("✨ 没有需要清理的描述");
            }
        }
    }

    Ok(())
}

/// 在搜索结果中高亮显示匹配的文本
///
/// 在给定文本中查找搜索词并用颜色高亮显示,提供更好的视觉反馈。
/// 搜索是不区分大小写的,但会保持原文本的大小写格式。
///
/// # 参数
/// * `text` - 要进行高亮处理的原始文本
/// * `query` - 要高亮的搜索词
///
/// # 返回值
/// * `String` - 包含高亮标记的文本字符串
///
/// # 高亮规则
/// - 使用黄色粗体显示匹配的文本
/// - 保持原文本的大小写格式
/// - 如果没有找到匹配,返回原始文本
///
/// # 实现细节
/// - 使用不区分大小写的搜索来查找匹配位置
/// - 在找到的每个匹配位置应用终端颜色样式
/// - 保持非匹配部分的原始格式
fn highlight_search_term(text: &str, query: &str) -> String {
    let query_lower = query.to_lowercase(); // 转为小写用于搜索
    let mut result = String::new();
    let mut last_end = 0;

    // 查找所有匹配的位置
    for (start, part) in text.match_indices(&query_lower) {
        // 添加匹配前的普通文本
        result.push_str(&text[last_end..start]);

        // 添加高亮的匹配文本(黄色粗体)
        result.push_str(
            &console::style(&text[start..start + part.len()])
                .yellow()
                .bold()
                .to_string(),
        );

        last_end = start + part.len();
    }

    // 添加剩余的普通文本
    result.push_str(&text[last_end..]);

    // 如果没有找到任何匹配(可能因为大小写问题),返回原文本
    if result.is_empty() {
        text.to_string()
    } else {
        result
    }
}