Skip to main content

perl_lsp_import_management/
lib.rs

1#![warn(missing_docs)]
2//! Perl import-management helpers for LSP code actions.
3//!
4//! This crate intentionally focuses on a single responsibility:
5//! collecting, classifying, and rewriting `use`/`require` statements.
6
7/// Guess a module name for a common function.
8#[must_use]
9pub fn guess_module_for_function(func: &str) -> Option<String> {
10    match func {
11        "dumper" => Some("Data::Dumper"),
12        "encode" | "decode" => Some("Encode"),
13        "basename" | "dirname" => Some("File::Basename"),
14        "mkpath" | "rmtree" => Some("File::Path"),
15        "slurp" => Some("File::Slurp"),
16        "decode_json" | "encode_json" => Some("JSON"),
17        _ => None,
18    }
19    .map(str::to_string)
20}
21
22/// Collect import statements (`use` and `require`) from source lines.
23#[must_use]
24pub fn collect_imports(lines: &[String]) -> Vec<String> {
25    let mut imports = Vec::new();
26
27    for line in lines {
28        let trimmed = line.trim();
29        if trimmed.starts_with("use ") || trimmed.starts_with("require ") {
30            imports.push(line.clone());
31        }
32    }
33
34    imports
35}
36
37/// Sort imports by category: pragmas, core, CPAN-style, then local.
38///
39/// Duplicates are removed (keeping the first occurrence). Categories are
40/// ordered: pragmas -> core -> CPAN -> local, each sorted alphabetically.
41#[must_use]
42pub fn sort_imports(imports: Vec<String>) -> Vec<String> {
43    let mut pragmas = Vec::new();
44    let mut core = Vec::new();
45    let mut cpan = Vec::new();
46    let mut local = Vec::new();
47    let mut seen = std::collections::HashSet::new();
48
49    for import in imports {
50        let trimmed = import.trim().to_string();
51        if !seen.insert(trimmed.clone()) {
52            continue;
53        }
54
55        if trimmed.contains("strict")
56            || trimmed.contains("warnings")
57            || trimmed.contains("utf8")
58            || trimmed.contains("feature")
59        {
60            pragmas.push(trimmed);
61        } else if trimmed.contains("::") {
62            cpan.push(trimmed);
63        } else if trimmed.starts_with("use lib") || trimmed.contains("./") {
64            local.push(trimmed);
65        } else {
66            core.push(trimmed);
67        }
68    }
69
70    pragmas.sort();
71    core.sort();
72    cpan.sort();
73    local.sort();
74
75    let mut result = Vec::new();
76    result.extend(pragmas);
77    result.extend(core);
78    result.extend(cpan);
79    result.extend(local);
80
81    result
82}
83
84/// Find the byte range containing the contiguous import block boundaries.
85#[must_use]
86pub fn find_imports_range(source: &str, lines: &[String]) -> Option<(usize, usize)> {
87    let imports = collect_imports(lines);
88    if imports.is_empty() {
89        return None;
90    }
91
92    let first = source.find(imports.first()?)?;
93    let last_line = imports.last()?;
94    let last = source.rfind(last_line)?;
95    let last_end = last + last_line.len();
96
97    Some((first, last_end))
98}