mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-08 08:52:12 +00:00
170 lines
6.2 KiB
Rust
170 lines
6.2 KiB
Rust
use ide_core::{CancellationToken, IdeError, IdeResult, Pos, TextEdit};
|
|
use ide_search::{
|
|
ReplaceEvent, ReplaceHost, ReplaceTarget, SearchQuery, SearchScope, preserve_case, preview, replace_files,
|
|
replacement_edits,
|
|
};
|
|
use std::collections::HashMap;
|
|
use std::path::{Path, PathBuf};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
fn query(pattern: &str) -> SearchQuery {
|
|
SearchQuery {
|
|
pattern: pattern.into(),
|
|
is_regex: false,
|
|
case_sensitive: false,
|
|
whole_word: false,
|
|
include: vec![],
|
|
exclude: vec![],
|
|
scope: SearchScope::Project,
|
|
max_matches: 0,
|
|
}
|
|
}
|
|
|
|
fn regex(pattern: &str) -> SearchQuery {
|
|
SearchQuery { is_regex: true, ..query(pattern) }
|
|
}
|
|
|
|
/// In-memory documents: each `edit_file` is one atomic step (like one undoable
|
|
/// `DocPatched`), recorded in `steps`.
|
|
#[derive(Default)]
|
|
struct FakeDocs {
|
|
files: Mutex<HashMap<PathBuf, String>>,
|
|
steps: Mutex<Vec<(PathBuf, usize)>>,
|
|
/// Cancel this token after the first edited file.
|
|
cancel_after_first: Option<CancellationToken>,
|
|
}
|
|
|
|
impl FakeDocs {
|
|
fn with(files: &[(&str, &str)]) -> Self {
|
|
let docs = Self::default();
|
|
for (p, t) in files {
|
|
docs.files.lock().unwrap().insert(PathBuf::from(p), (*t).to_owned());
|
|
}
|
|
docs
|
|
}
|
|
fn text(&self, p: &str) -> String {
|
|
self.files.lock().unwrap()[Path::new(p)].clone()
|
|
}
|
|
}
|
|
|
|
impl ReplaceHost for FakeDocs {
|
|
fn edit_file(&self, path: &Path, compute: &mut dyn FnMut(&str) -> IdeResult<Vec<TextEdit>>) -> IdeResult<u32> {
|
|
let mut files = self.files.lock().unwrap();
|
|
let text = files.get(path).cloned().ok_or_else(|| IdeError::not_found("no such file"))?;
|
|
let edits = compute(&text)?;
|
|
if !edits.is_empty() {
|
|
files.insert(path.to_owned(), ide_search::apply_edits(&text, &edits));
|
|
self.steps.lock().unwrap().push((path.to_owned(), edits.len()));
|
|
}
|
|
if let Some(c) = &self.cancel_after_first {
|
|
c.cancel();
|
|
}
|
|
Ok(edits.len() as u32)
|
|
}
|
|
}
|
|
|
|
fn targets(paths: &[&str]) -> Vec<ReplaceTarget> {
|
|
paths.iter().map(|p| ReplaceTarget { path: PathBuf::from(p).into(), only: None }).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn edits_use_utf16_positions() {
|
|
let edits = replacement_edits("é foo\n😀 foo foo", &query("foo"), "bar", false, None).unwrap();
|
|
let starts: Vec<Pos> = edits.iter().map(|e| e.range.start).collect();
|
|
assert_eq!(starts, vec![Pos::new(0, 2), Pos::new(1, 3), Pos::new(1, 7)]);
|
|
assert_eq!(edits[0].range.end, Pos::new(0, 5));
|
|
}
|
|
|
|
#[test]
|
|
fn only_selected_matches_are_replaced() {
|
|
let text = "foo foo\nfoo";
|
|
let only = [Pos::new(0, 4), Pos::new(1, 0), Pos::new(5, 5)];
|
|
let p = preview(text, &query("foo"), "x", false, Some(&only)).unwrap();
|
|
assert_eq!(p.after, "foo x\nx");
|
|
assert_eq!(p.edits.len(), 2);
|
|
assert_eq!(p.before, text);
|
|
}
|
|
|
|
#[test]
|
|
fn regex_groups_and_escapes() {
|
|
let p = preview("let a = 1;\nlet bb = 22;", ®ex(r"(\w+) = (\d+)"), r"$2 = $1\t", false, None).unwrap();
|
|
assert_eq!(p.after, "let 1 = a\t;\nlet 22 = bb\t;");
|
|
let p = preview("k=v", ®ex("(?P<key>\\w)=(?P<val>\\w)"), "${val}=${key}", false, None).unwrap();
|
|
assert_eq!(p.after, "v=k");
|
|
}
|
|
|
|
#[test]
|
|
fn preserve_case_rules() {
|
|
assert_eq!(preserve_case("FOO", "bar"), "BAR");
|
|
assert_eq!(preserve_case("Foo", "bar"), "Bar");
|
|
assert_eq!(preserve_case("foo", "Bar"), "bar");
|
|
assert_eq!(preserve_case("F", "bar"), "Bar");
|
|
assert_eq!(preserve_case("123", "bar"), "bar");
|
|
let p = preview("foo Foo FOO", &query("foo"), "bar", true, None).unwrap();
|
|
assert_eq!(p.after, "bar Bar BAR");
|
|
}
|
|
|
|
#[test]
|
|
fn replace_files_one_step_per_file_with_progress() {
|
|
let docs = FakeDocs::with(&[("a", "foo foo"), ("b", "nothing"), ("c", "x foo")]);
|
|
let events = Arc::new(Mutex::new(Vec::new()));
|
|
let ev = events.clone();
|
|
let summary = replace_files(
|
|
&docs,
|
|
&query("foo"),
|
|
"bar",
|
|
false,
|
|
&targets(&["a", "b", "c", "missing"]),
|
|
&CancellationToken::new(),
|
|
&move |e| ev.lock().unwrap().push(e),
|
|
)
|
|
.unwrap();
|
|
assert_eq!(docs.text("a"), "bar bar");
|
|
assert_eq!(docs.text("c"), "x bar");
|
|
assert_eq!(*docs.steps.lock().unwrap(), vec![(PathBuf::from("a"), 2), (PathBuf::from("c"), 1)]);
|
|
assert_eq!((summary.files, summary.replacements, summary.failed, summary.cancelled), (2, 3, 1, false));
|
|
let events = events.lock().unwrap();
|
|
assert_eq!(events.len(), 4);
|
|
assert!(matches!(&events[0], ReplaceEvent::File { replacements: 2, done: 1, total: 4, .. }));
|
|
assert!(matches!(&events[1], ReplaceEvent::File { replacements: 0, done: 2, .. }));
|
|
assert!(matches!(&events[3], ReplaceEvent::Failed { done: 4, .. }));
|
|
}
|
|
|
|
#[test]
|
|
fn replace_files_honours_cancellation() {
|
|
let cancel = CancellationToken::new();
|
|
let docs = FakeDocs { cancel_after_first: Some(cancel.clone()), ..FakeDocs::with(&[("a", "foo"), ("b", "foo")]) };
|
|
let summary = replace_files(&docs, &query("foo"), "bar", false, &targets(&["a", "b"]), &cancel, &|_| {}).unwrap();
|
|
assert!(summary.cancelled);
|
|
assert_eq!(summary.files, 1);
|
|
assert_eq!(docs.text("a"), "bar");
|
|
assert_eq!(docs.text("b"), "foo");
|
|
}
|
|
|
|
#[test]
|
|
fn replace_files_runs_off_thread_and_can_be_cancelled_from_outside() {
|
|
let cancel = CancellationToken::new();
|
|
cancel.cancel();
|
|
let docs = Arc::new(FakeDocs::with(&[("a", "foo")]));
|
|
let d = docs.clone();
|
|
let c = cancel.clone();
|
|
let summary = std::thread::spawn(move || {
|
|
replace_files(&*d, &query("foo"), "bar", false, &targets(&["a"]), &c, &|_| {}).unwrap()
|
|
})
|
|
.join()
|
|
.unwrap();
|
|
assert!(summary.cancelled);
|
|
assert_eq!(docs.text("a"), "foo");
|
|
}
|
|
|
|
#[test]
|
|
fn replace_files_respects_selected_matches_and_invalid_patterns() {
|
|
let docs = FakeDocs::with(&[("a", "foo\nfoo\nfoo")]);
|
|
let t = vec![ReplaceTarget { path: "a".into(), only: Some(vec![Pos::new(1, 0)]) }];
|
|
let s = replace_files(&docs, &query("foo"), "x", false, &t, &CancellationToken::new(), &|_| {}).unwrap();
|
|
assert_eq!(s.replacements, 1);
|
|
assert_eq!(docs.text("a"), "foo\nx\nfoo");
|
|
let err = replace_files(&docs, ®ex("("), "x", false, &t, &CancellationToken::new(), &|_| {}).unwrap_err();
|
|
assert_eq!(err.kind, ide_core::error::ErrorKind::InvalidInput);
|
|
}
|