дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
121 рядки
4.0 KiB
Rust
121 рядки
4.0 KiB
Rust
//! Compiles a [`SearchQuery`] into a ripgrep matcher.
|
|
|
|
use crate::SearchQuery;
|
|
use grep_matcher::LineTerminator;
|
|
use grep_regex::{RegexMatcher, RegexMatcherBuilder};
|
|
use ide_core::{IdeError, IdeResult};
|
|
|
|
/// A compiled query: the matcher plus whether matches may span lines.
|
|
pub(crate) struct Compiled {
|
|
pub matcher: RegexMatcher,
|
|
pub multi_line: bool,
|
|
}
|
|
|
|
/// The line terminator shared by the matcher and the searcher. CRLF mode lets
|
|
/// `$` match before `\r\n` and keeps `\r` out of single-line matches.
|
|
pub(crate) fn line_terminator() -> LineTerminator {
|
|
LineTerminator::crlf()
|
|
}
|
|
|
|
pub(crate) fn compile(query: &SearchQuery) -> IdeResult<Compiled> {
|
|
let multi_line = needs_multi_line(query);
|
|
match build(query, multi_line) {
|
|
Ok(matcher) => Ok(Compiled { matcher, multi_line }),
|
|
// Patterns such as `[\n]` or `\x0A` can only match across lines;
|
|
// retry in multi-line mode before reporting an error.
|
|
Err(err) if !multi_line => match build(query, true) {
|
|
Ok(matcher) => Ok(Compiled { matcher, multi_line: true }),
|
|
Err(_) => Err(invalid_pattern(query, &err)),
|
|
},
|
|
Err(err) => Err(invalid_pattern(query, &err)),
|
|
}
|
|
}
|
|
|
|
fn build(query: &SearchQuery, multi_line: bool) -> Result<RegexMatcher, grep_regex::Error> {
|
|
let mut b = RegexMatcherBuilder::new();
|
|
b.case_smart(false)
|
|
.case_insensitive(!query.case_sensitive)
|
|
.word(query.whole_word)
|
|
.fixed_strings(!query.is_regex)
|
|
.unicode(true)
|
|
// `^`/`$` always mean start/end of line, as in IDEA and ripgrep.
|
|
.multi_line(true)
|
|
.dot_matches_new_line(false)
|
|
.crlf(true);
|
|
if multi_line {
|
|
// Must come after `crlf`, which sets a line terminator.
|
|
b.line_terminator(None);
|
|
}
|
|
b.build(&query.pattern)
|
|
}
|
|
|
|
fn invalid_pattern(query: &SearchQuery, err: &grep_regex::Error) -> IdeError {
|
|
let reason = short_reason(&err.to_string());
|
|
if query.is_regex {
|
|
IdeError::invalid(format!("Invalid regular expression: {reason}"))
|
|
} else {
|
|
IdeError::invalid(format!("Invalid search string: {reason}"))
|
|
}
|
|
}
|
|
|
|
/// The regex crate reports a multi-line diagnostic quoting the pattern as the
|
|
/// builder rewrote it (`(?:…)`); the UI shows one line, so keep the reason.
|
|
fn short_reason(message: &str) -> String {
|
|
message.lines().rev().find_map(|l| l.trim().strip_prefix("error: ")).unwrap_or(message.trim()).to_string()
|
|
}
|
|
|
|
/// Multi-line search is enabled when the pattern itself contains a newline or,
|
|
/// for regexes, an unescaped `\n`.
|
|
fn needs_multi_line(query: &SearchQuery) -> bool {
|
|
if query.pattern.contains('\n') {
|
|
return true;
|
|
}
|
|
if !query.is_regex {
|
|
return false;
|
|
}
|
|
let mut chars = query.pattern.chars();
|
|
while let Some(c) = chars.next() {
|
|
if c == '\\' && chars.next() == Some('n') {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::SearchScope;
|
|
|
|
fn q(pattern: &str, is_regex: bool) -> SearchQuery {
|
|
SearchQuery {
|
|
pattern: pattern.into(),
|
|
is_regex,
|
|
case_sensitive: false,
|
|
whole_word: false,
|
|
include: vec![],
|
|
exclude: vec![],
|
|
scope: SearchScope::Project,
|
|
max_matches: 0,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn multi_line_detection() {
|
|
assert!(needs_multi_line(&q("a\nb", false)));
|
|
assert!(!needs_multi_line(&q("a\\nb", false)));
|
|
assert!(needs_multi_line(&q("a\\nb", true)));
|
|
assert!(!needs_multi_line(&q("a\\\\nb", true)));
|
|
assert!(compile(&q("a[\\n]b", true)).unwrap().multi_line);
|
|
assert!(!compile(&q("a\\sb", true)).unwrap().multi_line);
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_regex_is_invalid_input() {
|
|
let err = compile(&q("foo(", true)).err().unwrap();
|
|
assert_eq!(err.kind, ide_core::error::ErrorKind::InvalidInput);
|
|
assert_eq!(err.message, "Invalid regular expression: unclosed group");
|
|
assert!(compile(&q("foo(", false)).is_ok());
|
|
}
|
|
}
|