754 lines · 29.4 KB · Rust
RawDownload
// Copyright 2026 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::ops::Range;use std::sync::LazyLock; use bstr::BStr;use bstr::ByteSlice as _;use regex::bytes::Regex; // Built-in source symbol patterns for common languages. This stop-gap only// selects patterns from common file names/extensions.// TODO: Replace this with gix support once available, including// .gitattributes-based driver selection and custom hunk-header regexes.#[derive(Clone, Copy, Debug)]pub(crate) enum SourceLanguage {    BibTeX,    CLike,    CSharp,    Dts,    Elixir,    Go,    Html,    Ini,    Java,    JavaScript,    Kotlin,    Markdown,    Matlab,    MatlabOrObjC,    ObjC,    CLikeOrObjC,    Pascal,    Perl,    Php,    Python,    R,    Ruby,    Rust,    Scheme,    Shell,    Tex,} impl SourceLanguage {    pub(crate) fn from_file_name(file_name: &str) -> Option<Self> {        let extension = file_name.rsplit_once('.').map(|(_, extension)| extension);        match extension {            Some("bib") => Some(Self::BibTeX),            Some("c" | "cc" | "cpp" | "cxx" | "hh" | "hpp" | "hxx") => Some(Self::CLike),            Some("h") => Some(Self::CLikeOrObjC),            Some("cs") => Some(Self::CSharp),            Some("dts" | "dtsi") => Some(Self::Dts),            Some("ex" | "exs") => Some(Self::Elixir),            Some("go") => Some(Self::Go),            Some("htm" | "html" | "xhtml") => Some(Self::Html),            Some("cfg" | "conf" | "config" | "ini") => Some(Self::Ini),            Some("java") => Some(Self::Java),            Some("js" | "jsx" | "mjs" | "cjs" | "ts" | "tsx") => Some(Self::JavaScript),            Some("kt" | "kts") => Some(Self::Kotlin),            Some("markdown" | "md" | "mdown") => Some(Self::Markdown),            // `.m` is shared by MATLAB and Objective-C. Objective-C++ `.mm`            // and Objective-C headers can also contain ordinary C-like symbols.            Some("m") => Some(Self::MatlabOrObjC),            Some("matlab") => Some(Self::Matlab),            Some("mm") => Some(Self::CLikeOrObjC),            Some("p" | "pas") => Some(Self::Pascal),            Some("pl" | "pm" | "pod" | "psgi" | "t") => Some(Self::Perl),            Some("php" | "php3" | "php4" | "php5" | "phtml") => Some(Self::Php),            Some("py" | "pyw") => Some(Self::Python),            Some("r" | "R") => Some(Self::R),            Some("rb" | "rake" | "gemspec") => Some(Self::Ruby),            Some("rs") => Some(Self::Rust),            Some("scm" | "scheme" | "ss" | "lisp" | "lsp") => Some(Self::Scheme),            Some("sh" | "bash" | "zsh" | "fish") => Some(Self::Shell),            Some("tex" | "ltx" | "latex") => Some(Self::Tex),            _ => match file_name {                "Gemfile" | "Rakefile" => Some(Self::Ruby),                _ => None,            },        }    }     fn is_source_symbol(self, line: &[u8]) -> bool {        static BIBTEX_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(r#"^@[A-Za-z]+[ \t]*\{?[ \t]*[^ \t"@',\\#}{~%]*"#).unwrap()        });        static C_LIKE_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:[A-Za-z_][\w:<>,~]*|[*&]|\[[^\]]*\]|\s+)+[A-Za-z_~][\w:~]*\s*\([^;{}]*",            )            .unwrap()        });        static CSHARP_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:(?:[\w@_.]+(?:<[\w@_, \t<>]+>)?)(?:\s+[\w@_.]+(?:<[\w@_, \t<>]+>)?)+\s*\([^;]*|(?:(?:static|public|internal|private|protected|new|unsafe|sealed|abstract|partial)\s+)*(?:class|enum|interface|struct|record)\s+.*|namespace\s+.*)$",            )            .unwrap()        });        static DTS_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^(?:/[ \t]*\{|&?[A-Za-z_].*)").unwrap());        static ELIXIR_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(r"^(?:def(?:macro|module|impl|protocol|p)?|test)\s+.*").unwrap()        });        static GO_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(r"^(?:func\s*.*(?:\{\s*)?|type\s+.*(?:struct|interface)\s*(?:\{\s*)?)$")                .unwrap()        });        static HTML_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^<[Hh][1-6](?:\s.*)?>.*").unwrap());        static INI_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^\[[^]]+\]").unwrap());        static JAVA_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:(?:[a-z-]+\s+)*(?:class|enum|interface|record)\s+.*|(?:[A-Za-z_<>&][\]?&<>.,A-Za-z_0-9]*\s+)+[A-Za-z_][A-Za-z_0-9]*\s*\([^;]*)$",            )            .unwrap()        });        static JAVASCRIPT_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:(?:export|default|async|static|get|set)\s+)*(?:function\b|class\b|[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{|[A-Za-z_$][\w$]*\s*[:=]\s*(?:async\s*)?(?:function\b|\([^)]*\)\s*=>))",            )            .unwrap()        });        static KOTLIN_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^(?:[a-z]+\s+)*(?:fun|class|interface)\s+.*").unwrap());        static MARKDOWN_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^#{1,6}\s+.*").unwrap());        static MATLAB_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(r"^(?:(?:classdef|function)\s+.*|(?:%%%?|##)\s+.*)$").unwrap()        });        static OBJC_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:[-+]\s*\(\s*[A-Za-z_][A-Za-z_0-9* \t]*\)\s*[A-Za-z_].*|(?:[A-Za-z_][A-Za-z_0-9]*\s+)+[A-Za-z_][A-Za-z_0-9]*\s*\([^;]*|@(?:implementation|interface|protocol)\s+.*)$",            )            .unwrap()        });        static PASCAL_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:(?:(?:class\s+)?(?:procedure|function)|constructor|destructor|interface|implementation|initialization|finalization)\s*.*|.*=\s*(?:class|record).*)$",            )            .unwrap()        });        static PERL_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:package .*|sub [[:alnum:]_':]+[ \t]*(?:\([^)]*\)[ \t]*)?(?::[^;#]*)?(?:\{[ \t]*)?(?:#.*)?|(?:BEGIN|END|INIT|CHECK|UNITCHECK|AUTOLOAD|DESTROY)[ \t]*(?:\{[ \t]*)?(?:#.*)?|=head[0-9] .*)$",            )            .unwrap()        });        static PHP_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:(?:(?:public|protected|private|static|abstract|final)\s+)*function.*|(?:(?:(?:final|abstract)\s+)?class|enum|interface|trait).*)$",            )            .unwrap()        });        static PYTHON_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^(?:async\s+def|def|class)\s+\w+").unwrap());        static R_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(r"^[A-Za-z][A-Za-z0-9_.]*\s*(?:<-|=)\s*function.*$").unwrap()        });        static RUBY_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^(?:class|module|def)\s+.*").unwrap());        static RUST_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r#"^(?:(?:pub(?:\([^)]*\))?|const|async|unsafe|extern(?:\s+"[^"]+")?)\s+)*(?:(?:fn|struct|enum|impl|trait|mod|type|union)\b|macro_rules!\s*)"#,            )            .unwrap()        });        static SCHEME_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:\((?:(?:define|def(?:struct|syntax|class|method|rules|record|proto|alias)?)[-*/ \t]|(?:library|module|struct|class)[*+ \t]).*|[Dd][Ee][Ff].*)$",            )            .unwrap()        });        static SHELL_FUNCTION_RE: LazyLock<Regex> = LazyLock::new(|| {            Regex::new(                r"^(?:[A-Za-z_][A-Za-z0-9_]*\s*\(\s*\)|function\s+[A-Za-z_][A-Za-z0-9_]*(?:(?:\s*\(\s*\))|\s+)).*$",            )            .unwrap()        });        static TEX_FUNCTION_RE: LazyLock<Regex> =            LazyLock::new(|| Regex::new(r"^\\(?:(?:sub)*section|chapter|part)\*?\{.*").unwrap());         match self {            Self::BibTeX => BIBTEX_FUNCTION_RE.is_match(line),            Self::CLike => {                let before_paren = line.split(|byte| *byte == b'(').next().unwrap_or(line);                !line.first().is_some_and(u8::is_ascii_whitespace)                    && !before_paren.contains_str(": ")                    && !starts_with_keyword(line, &["do", "for", "if", "switch", "while"])                    && C_LIKE_FUNCTION_RE.is_match(line)            }            Self::CSharp => {                !starts_with_keyword(                    line,                    &[                        "do", "while", "for", "foreach", "if", "else", "new", "default", "return",                        "switch", "case", "throw", "catch", "using", "lock", "fixed",                    ],                ) && CSHARP_FUNCTION_RE.is_match(line)            }            Self::Dts => {                !line.contains(&b';') && !line.contains(&b'=') && DTS_FUNCTION_RE.is_match(line)            }            Self::Elixir => ELIXIR_FUNCTION_RE.is_match(line),            Self::Go => GO_FUNCTION_RE.is_match(line),            Self::Html => HTML_FUNCTION_RE.is_match(line),            Self::Ini => INI_FUNCTION_RE.is_match(line),            Self::Java => {                !starts_with_keyword(                    line,                    &[                        "catch",                        "do",                        "for",                        "if",                        "instanceof",                        "new",                        "return",                        "switch",                        "throw",                        "while",                    ],                ) && JAVA_FUNCTION_RE.is_match(line)            }            Self::JavaScript => {                !starts_with_keyword(line, &["catch", "for", "if", "switch", "while", "with"])                    && JAVASCRIPT_FUNCTION_RE.is_match(line)            }            Self::Kotlin => KOTLIN_FUNCTION_RE.is_match(line),            Self::Markdown => MARKDOWN_FUNCTION_RE.is_match(line),            Self::Matlab => MATLAB_FUNCTION_RE.is_match(line),            Self::MatlabOrObjC => {                Self::Matlab.is_source_symbol(line) || Self::ObjC.is_source_symbol(line)            }            Self::ObjC => {                !starts_with_keyword(                    line,                    &["do", "for", "if", "else", "return", "switch", "while"],                ) && OBJC_FUNCTION_RE.is_match(line)            }            Self::CLikeOrObjC => {                Self::CLike.is_source_symbol(line) || Self::ObjC.is_source_symbol(trim_line(line))            }            Self::Pascal => PASCAL_FUNCTION_RE.is_match(line),            Self::Perl => PERL_FUNCTION_RE.is_match(line),            Self::Php => PHP_FUNCTION_RE.is_match(line),            Self::Python => PYTHON_FUNCTION_RE.is_match(line),            Self::R => R_FUNCTION_RE.is_match(line),            Self::Ruby => RUBY_FUNCTION_RE.is_match(line),            Self::Rust => RUST_FUNCTION_RE.is_match(line),            Self::Scheme => SCHEME_FUNCTION_RE.is_match(line),            Self::Shell => SHELL_FUNCTION_RE.is_match(line),            Self::Tex => TEX_FUNCTION_RE.is_match(line),        }    }} fn starts_with_keyword(line: &[u8], keywords: &[&str]) -> bool {    keywords.iter().any(|keyword| {        line.strip_prefix(keyword.as_bytes()).is_some_and(|rest| {            rest.first()                .is_none_or(|byte| byte.is_ascii_whitespace() || matches!(byte, b'(' | b'{' | b';'))        })    })} fn strip_line_ending(line: &[u8]) -> &[u8] {    let line = line.strip_suffix(b"\n").unwrap_or(line);    line.strip_suffix(b"\r").unwrap_or(line)} fn trim_line(line: &[u8]) -> &[u8] {    strip_line_ending(line).trim_ascii()} fn is_c_style_comment_line(line: &[u8]) -> bool {    // A leading `*` can also belong to a pointer-returning function declaration.    line.starts_with(b"/*") || line.starts_with(b"//")} fn is_comment_line(line: &[u8], language: SourceLanguage) -> bool {    match language {        SourceLanguage::CLike        | SourceLanguage::CLikeOrObjC        | SourceLanguage::CSharp        | SourceLanguage::Go        | SourceLanguage::Java        | SourceLanguage::JavaScript        | SourceLanguage::Kotlin        | SourceLanguage::ObjC        | SourceLanguage::Php        | SourceLanguage::Rust => is_c_style_comment_line(line),        SourceLanguage::MatlabOrObjC => is_c_style_comment_line(line),        SourceLanguage::Elixir        | SourceLanguage::Perl        | SourceLanguage::Python        | SourceLanguage::R        | SourceLanguage::Ruby        | SourceLanguage::Shell => line.starts_with(b"#"),        SourceLanguage::Tex => line.starts_with(b"%"),        SourceLanguage::BibTeX        | SourceLanguage::Dts        | SourceLanguage::Html        | SourceLanguage::Ini        | SourceLanguage::Markdown        | SourceLanguage::Matlab        | SourceLanguage::Pascal        | SourceLanguage::Scheme => false,    }} fn source_symbols(    content: &BStr,    language: SourceLanguage,) -> impl Iterator<Item = (usize, &[u8])> {    content        .split_inclusive(|byte| *byte == b'\n')        .enumerate()        .filter_map(move |(line_number, line)| {            let line = strip_line_ending(line);            let trimmed_line = trim_line(line);            let line_to_match = match language {                // Keep leading whitespace so the C-like matcher excludes indented                // calls/control flow inside a function. This heuristic intentionally                // also excludes indented C++ class methods and namespace members.                SourceLanguage::CLike | SourceLanguage::CLikeOrObjC => line,                _ => trimmed_line,            };            (!is_comment_line(trimmed_line, language) && language.is_source_symbol(line_to_match))                .then_some((line_number, trimmed_line))        })} /// Finds source symbols near line ranges in one version of a file.////// A scanner operates on a single file content so it can also be used for/// diffs with more than two sides.pub(crate) struct SourceSymbolScanner<'a> {    symbols: Vec<(usize, &'a [u8])>,} impl<'a> SourceSymbolScanner<'a> {    pub(crate) fn new(language: SourceLanguage, content: &'a BStr) -> Self {        Self {            symbols: source_symbols(content, language).collect(),        }    }     /// Finds a symbol on the first line of a nonempty range, or the closest    /// symbol strictly before the range.    ///    /// If there is no preceding symbol, the first symbol in the changed range    /// is returned. This is useful for newly-added sections.    pub(crate) fn find(&self, line_range: &Range<usize>) -> Option<&'a [u8]> {        let preceding_end = self            .symbols            .partition_point(|(line, _)| *line < line_range.start);        let first_in_range = self            .symbols            .get(preceding_end)            .filter(|(line, _)| *line < line_range.end);        if let Some((line, symbol)) = first_in_range            && *line == line_range.start        {            return Some(symbol);        }         self.symbols[..preceding_end]            .last()            .or(first_in_range)            .map(|(_, symbol)| *symbol)    }} #[cfg(test)]mod tests {    use super::*;     fn collect_source_symbols(content: &BStr, language: SourceLanguage) -> Vec<(usize, &[u8])> {        source_symbols(content, language).collect()    }     #[test]    fn test_source_symbol_patterns() {        let samples: &[(SourceLanguage, &[&str])] = &[            (                SourceLanguage::BibTeX,                &[                    "@article{example,",                    "@string { name = value }",                    "plain text",                ],            ),            (                SourceLanguage::CLike,                &[                    "static int example(int value)",                    "void *pointer(void)",                    "    call(value)",                    "if (condition)",                    "label: call()",                ],            ),            (                SourceLanguage::CSharp,                &[                    "public class Example",                    "namespace Example.Core",                    "public async Task<int> Run(int value)",                    "if (condition)",                ],            ),            (                SourceLanguage::Dts,                &["/ {", "device_node {", "property = <1>;"],            ),            (                SourceLanguage::Elixir,                &["defmodule Example do", "defp run(value) do", "if ready do"],            ),            (                SourceLanguage::Go,                &[                    "func Example(value int) int {",                    "type Example struct {",                    "var example = func() {}",                ],            ),            (                SourceLanguage::Html,                &[                    "<h2 class=\"title\">Example</h2>",                    "<H1>Title</H1>",                    "<p>text</p>",                ],            ),            (                SourceLanguage::Ini,                &["[section]", "[section.subsection] value", "key = value"],            ),            (                SourceLanguage::Java,                &[                    "public class Example",                    "public static String run(int value)",                    "if (condition)",                ],            ),            (                SourceLanguage::JavaScript,                &[                    "export async function example() {",                    "class Example {",                    "handler = async (event) => {",                    "if (condition) {",                ],            ),            (                SourceLanguage::Kotlin,                &[                    "public fun example(value: Int)",                    "data class Example",                    "if (condition)",                ],            ),            (                SourceLanguage::Markdown,                &["# Title", "###### Detail", "plain text", "#missing-space"],            ),            (                SourceLanguage::Matlab,                &[                    "function y = example(x)",                    "classdef Example",                    "%% Section",                    "y = x + 1;",                ],            ),            (                SourceLanguage::ObjC,                &[                    "- (void)example:(id)value",                    "@interface Example : NSObject",                    "if (condition)",                ],            ),            (                SourceLanguage::Pascal,

Lines 1–500 of 754.