diff --git a/crates/engine/src/spell.rs b/crates/engine/src/spell.rs index 44d1c6b7..addb8048 100644 --- a/crates/engine/src/spell.rs +++ b/crates/engine/src/spell.rs @@ -356,7 +356,14 @@ impl Dictionary { } /// Is `word` spelt correctly? Capitalised and upper-case forms of a lower-case word pass. + /// Words longer than 100 characters are not checked (they pass). pub fn check(&self, word: &str) -> bool { + // Compound splitting tries every split point, so checking costs O(n²) in the word's + // length: a 100,000-character "word" took seconds per check under a compounding + // dictionary, on every edit (#1175). + if word.chars().nth(MAX_CHECK_CHARS).is_some() { + return true; + } if word.contains('\u{2019}') { return self.check(&word.replace('\u{2019}', "'")); } @@ -553,6 +560,9 @@ pub fn find_in(dirs: &[PathBuf], lang: &str) -> Option<(PathBuf, PathBuf)> { /// Longest word `Dictionary::suggest` generates corrections for. const MAX_SUGGEST_CHARS: usize = 64; +/// Longest word `Dictionary::check` checks, as Hunspell's word length limit. +const MAX_CHECK_CHARS: usize = 100; + type Cache = Mutex>>>; fn cache() -> &'static Cache { @@ -843,6 +853,20 @@ SFX D Y 1\nSFX D 0 ed/S .\n"; assert!(d.suggest("workingslides", 5).contains(&"working slides".to_string())); } + /// #1175: compound splitting is quadratic in the word's length, so a word longer than + /// `MAX_CHECK_CHARS` is not checked; shorter ones still are. + #[test] + fn a_word_longer_than_the_check_limit_is_not_checked() { + let d = Dictionary::parse(b"COMPOUNDFLAG X\nCOMPOUNDMIN 3\n", b"2\nfoot/X\nball/X\n".as_slice()); + assert!(d.check("footballfoot"), "a compound is checked"); + let under: String = "footb".repeat(MAX_CHECK_CHARS / 5); + assert_eq!(under.chars().count(), MAX_CHECK_CHARS); + assert!(!d.check(&under), "a word at the limit is still checked"); + // Unchecked, not seconds of compound splitting. + assert!(d.check(&"footb".repeat(20_000))); + assert!(d.check(&format!("{under}x"))); + } + #[test] fn compounds_join_flagged_parts() { let aff = "COMPOUNDFLAG X\nCOMPOUNDMIN 3\nONLYINCOMPOUND O\nSFX S Y 1\nSFX S 0 en .\n";