Skip to main content

declutter/
lang.rs

1use std::borrow::Cow;
2use std::path::Path;
3
4use tree_sitter::Language;
5
6/// A language whose comments declutter can find.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Lang {
9    Swift,
10    TypeScript,
11    Tsx,
12    /// Parsed with the TSX grammar, which accepts plain JavaScript and JSX.
13    JavaScript,
14    Python,
15}
16
17impl Lang {
18    pub fn from_path(path: &str) -> Option<Lang> {
19        let ext = Path::new(path).extension()?.to_str()?;
20        match ext {
21            "swift" => Some(Lang::Swift),
22            "ts" | "mts" | "cts" => Some(Lang::TypeScript),
23            "tsx" => Some(Lang::Tsx),
24            "js" | "mjs" | "cjs" | "jsx" => Some(Lang::JavaScript),
25            "py" | "pyi" => Some(Lang::Python),
26            _ => None,
27        }
28    }
29
30    pub fn name(self) -> &'static str {
31        match self {
32            Lang::Swift => "Swift",
33            Lang::TypeScript => "TypeScript",
34            Lang::Tsx => "TSX",
35            Lang::JavaScript => "JavaScript",
36            Lang::Python => "Python",
37        }
38    }
39
40    pub fn grammar(self) -> Language {
41        match self {
42            Lang::Swift => tree_sitter_swift::LANGUAGE.into(),
43            Lang::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
44            Lang::Tsx | Lang::JavaScript => tree_sitter_typescript::LANGUAGE_TSX.into(),
45            Lang::Python => tree_sitter_python::LANGUAGE.into(),
46        }
47    }
48
49    pub fn is_comment(self, kind: &str) -> bool {
50        match self {
51            Lang::Swift => matches!(kind, "comment" | "multiline_comment"),
52            Lang::TypeScript | Lang::Tsx | Lang::JavaScript => {
53                matches!(kind, "comment" | "html_comment")
54            }
55            Lang::Python => kind == "comment",
56        }
57    }
58}
59
60/// The text handed to the parser in place of `src`: always the same length, byte for
61/// byte, so every range found in it is a range in `src` too.
62///
63/// It papers over constructs tree-sitter-swift 0.7 cannot parse, each of which leaves
64/// an error that can swallow the rest of a function:
65///
66/// - the empty tuple `()` used as a value (`.success(())`, `resume(returning: ())`,
67///   `value ?? ()`) becomes the empty array literal `[]`, which parses there;
68/// - `await` opening an `if`/`while`/`guard` condition (`if await a != b`) is blanked;
69/// - `nonisolated(unsafe)` loses its `(unsafe)`.
70///
71/// None of these touch a comment, an import or a logging call's extent.
72pub fn parsable(lang: Lang, src: &str) -> Cow<'_, str> {
73    if lang != Lang::Swift {
74        return Cow::Borrowed(src);
75    }
76    let mut out = src.as_bytes().to_vec();
77    let mut blank = |range: std::ops::Range<usize>, with: &[u8]| {
78        out[range.clone()].copy_from_slice(&with[..range.len()]);
79    };
80
81    for (at, _) in src.match_indices("()") {
82        let before = src[..at].trim_end();
83        let after = src[at + 2..].trim_start();
84        let value_position = matches!(before.chars().last(), Some('(' | ',' | ':' | '=' | '?'))
85            && !before.ends_with("->")
86            && !["->", "throws", "async"]
87                .iter()
88                .any(|word| after.starts_with(word));
89        if value_position {
90            blank(at..at + 2, b"[]");
91        }
92    }
93    for (at, _) in src.match_indices("await ") {
94        let before = src[..at].trim_end();
95        let opens_condition = ["if", "while", "guard"].iter().any(|keyword| {
96            before.ends_with(keyword)
97                && !before[..before.len() - keyword.len()]
98                    .ends_with(|c: char| c.is_alphanumeric() || c == '_')
99        });
100        if opens_condition {
101            blank(at..at + 5, b"     ");
102        }
103    }
104    for (at, _) in src.match_indices("nonisolated(unsafe)") {
105        blank(at + 11..at + 19, b"        ");
106    }
107
108    if out == src.as_bytes() {
109        return Cow::Borrowed(src);
110    }
111    // Only ASCII bytes were swapped for ASCII bytes, so the text is still valid UTF-8.
112    Cow::Owned(String::from_utf8(out).unwrap_or_else(|_| src.to_string()))
113}