use std::process::{Child, ChildStdin};
use std::io::Write;
use std::time::Duration;
use std::thread;
use std::sync::mpsc::{channel, Receiver, TryRecvError};
use error::{Result, Error};
use ispell_result::{IspellResult, IspellError};
use async_reader::AsyncReader;
pub struct SpellChecker {
ispell: Child,
stdin: ChildStdin,
receiver: Receiver<Result<String>>,
timeout: Duration,
_child: thread::JoinHandle<()>,
}
impl SpellChecker {
#[doc(hidden)]
pub fn new(mut process: Child, timeout: u64) -> Result<SpellChecker> {
let stdin = if let Some(stdin) = process.stdin.take() {
stdin
} else {
return Err(Error::process("could not access stdin of spawned process"));
};
let stdout = if let Some(stdout) = process.stdout.take() {
stdout
} else {
return Err(Error::process("could not access stdin of spawned process"));
};
let (sender, receiver) = channel();
let mut reader = AsyncReader::new(stdout, sender);
let child = thread::spawn(move || {
reader.read_loop();
});
let mut checker = SpellChecker {
ispell: process,
stdin: stdin,
timeout: Duration::from_millis(timeout),
receiver: receiver,
_child: child,
};
let s = checker.read_str()?;
match s.chars().next() {
Some('@') => Ok(checker),
_ => Err(Error::protocol(format!("First line of ispell output doesn't start with '@', aborting")))
}
}
fn read_str(&mut self) -> Result<String> {
match self.receiver.recv_timeout(self.timeout) {
Ok(result) => result,
Err(_) => return Err(Error::process("timeout error: spawned process didn't respond in time, aborting")),
}
}
fn flush_stdout(&mut self) -> Result<()> {
loop {
match self.receiver.try_recv() {
Ok(_) => continue,
Err(TryRecvError::Empty) => break,
Err(TryRecvError::Disconnected) => return Err(Error::process("spawned process closed its stdout early, aborting")),
}
}
Ok(())
}
fn write_str(&mut self, text: &str) -> Result<()> {
self.flush_stdout()?;
self.stdin.write_all(b"^")?;
self.stdin.write_all(text.as_bytes())?;
self.stdin.write_all(b"\n")?;
self.stdin.flush()?;
Ok(())
}
pub fn add_word_to_dictionary(&mut self, word: &str) -> Result<()> {
if word.contains(|c:char| c.is_whitespace()) {
return Err(Error::invalid_word(format!("word '{}' contains space(s)",
word)));
}
self.stdin.write_all(b"*")?;
self.stdin.write_all(word.as_bytes())?;
self.stdin.write_all(b"\n")?;
self.stdin.flush()?;
self.stdin.write_all(b"#\n")?;
self.stdin.flush()?;
Ok(())
}
pub fn add_word(&mut self, word: &str) -> Result<()> {
if word.contains(|c:char| c.is_whitespace()) {
return Err(Error::invalid_word(format!("word '{}' contains space(s)",
word)));
}
self.stdin.write_all(b"@")?;
self.stdin.write_all(word.as_bytes())?;
self.stdin.write_all(b"\n")?;
self.stdin.flush()?;
Ok(())
}
pub fn check(&mut self, text: &str) -> Result<Vec<IspellError>> {
let results = self.check_raw(text)?;
let mut errors = vec!();
for elem in results.into_iter() {
match elem {
IspellResult::Miss(error)
| IspellResult::Guess(error)
| IspellResult::None(error)
=> errors.push(error),
_ => (),
}
}
Ok(errors)
}
pub fn check_raw(&mut self, text: &str) -> Result<Vec<IspellResult>> {
self.write_str(text)?;
let mut output = Vec::new();
if let Ok(s) = self.read_str() {
for line in s.lines() {
if line.is_empty() {
break;
}
let first = line.chars().next().unwrap();
match first {
'*' => output.push(IspellResult::Ok),
'-' => output.push(IspellResult::Compound),
'+' => {
let words:Vec<_> = line.split_whitespace().collect();
if words.len() != 2 {
return Err(Error::protocol(format!("'root' line ill-formatted: {}", line)));
}
output.push(IspellResult::Root(words[1].to_owned()));
},
'#' => {
let error = get_ispell_error(line, 3)?;
output.push(IspellResult::None(error));
},
'&' | '?' => {
let parts: Vec<_> = line.split(':').collect();
if parts.len() != 2 {
return Err(Error::protocol(format!("unexpected output from ispell: {}", line)));
}
let mut error = get_ispell_error(parts[0], 4)?;
let suggestions: Vec<_> = parts[1].split(",")
.map(|s| s.trim().to_owned())
.collect();
error.suggestions = suggestions;
if first == '&' {
output.push(IspellResult::Miss(error));
} else {
output.push(IspellResult::Guess(error));
}
},
_ => return Err(Error::protocol(format!("unexpected output: {}", line))),
}
}
}
Ok(output)
}
}
impl Drop for SpellChecker {
fn drop(&mut self) {
self.ispell.kill().unwrap();
}
}
fn get_ispell_error(input: &str, n: usize) -> Result<IspellError> {
let words: Vec<_> = input.split_whitespace().collect();
if words.len() != n {
return Err(Error::protocol(format!("unexpected result: {}", input)));
}
let misspelled = words[1].to_owned();
let position:usize = words[n - 1].parse()
.map_err(|_| Error::protocol(format!("could not parse '{}' as an int", words[2])))?;
Ok(IspellError {
misspelled: misspelled,
position: position - 1, suggestions: vec!(),
})
}
#[test]
fn add_word() {
use spell_launcher::SpellLauncher;
let mut checker = SpellLauncher::new()
.launch()
.unwrap();
checker.add_word("notaword").unwrap();
assert!(checker.check("notaword").unwrap().is_empty());
checker.add_word("stillnotaword2").unwrap();
assert_eq!(checker.check("stillnotaword2").unwrap().len(), 1);
}