File completion: handle quoted path
diff --git a/TODO.md b/TODO.md
index d4d016d..09516d1 100644
--- a/TODO.md
+++ b/TODO.md
@@ -13,7 +13,7 @@
 - [ ] clicolors spec (https://docs.rs/console/0.6.1/console/fn.colors_enabled.html)
 
 Completion
-- [ ] Quoted path
+- [X] Quoted path
 - [ ] Windows escape/unescape space in path
 - [ ] file completion & escape/unescape (#106)
 - [ ] file completion & tilde (#62)
diff --git a/src/completion.rs b/src/completion.rs
index 778bc43..5222b15 100644
--- a/src/completion.rs
+++ b/src/completion.rs
@@ -66,6 +66,7 @@
 /// A `Completer` for file and folder names.
 pub struct FilenameCompleter {
     break_chars: BTreeSet<char>,
+    double_quotes_special_chars: BTreeSet<char>,
 }
 
 // rl_basic_word_break_characters, rl_completer_word_break_characters
@@ -83,10 +84,15 @@
 #[cfg(windows)]
 static ESCAPE_CHAR: Option<char> = None;
 
+// In double quotes, not all break_chars need to be escaped
+// https://www.gnu.org/software/bash/manual/html_node/Double-Quotes.html
+static DOUBLE_QUOTES_SPECIAL_CHARS: [char; 4] = ['"', '$', '\\', '`'];
+
 impl FilenameCompleter {
     pub fn new() -> FilenameCompleter {
         FilenameCompleter {
             break_chars: DEFAULT_BREAK_CHARS.iter().cloned().collect(),
+            double_quotes_special_chars: DOUBLE_QUOTES_SPECIAL_CHARS.iter().cloned().collect(),
         }
     }
 }
@@ -99,9 +105,25 @@
 
 impl Completer for FilenameCompleter {
     fn complete(&self, line: &str, pos: usize) -> Result<(usize, Vec<String>)> {
-        let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars);
-        let path = unescape(path, ESCAPE_CHAR);
-        let matches = try!(filename_complete(&path, ESCAPE_CHAR, &self.break_chars));
+        let (start, path, esc_char, break_chars) =
+            if let Some((idx, double_quote)) = find_unclosed_quote(&line[..pos]) {
+                let start = idx + 1;
+                if double_quote {
+                    (
+                        start,
+                        unescape(&line[start..pos], ESCAPE_CHAR),
+                        ESCAPE_CHAR,
+                        &self.double_quotes_special_chars,
+                    )
+                } else {
+                    (start, Borrowed(&line[start..pos]), None, &self.break_chars)
+                }
+            } else {
+                let (start, path) = extract_word(line, pos, ESCAPE_CHAR, &self.break_chars);
+                let path = unescape(path, ESCAPE_CHAR);
+                (start, path, ESCAPE_CHAR, &self.break_chars)
+            };
+        let matches = try!(filename_complete(&path, esc_char, break_chars));
         Ok((start, matches))
     }
 }
@@ -271,6 +293,61 @@
     Some(&candidates[0][0..longest_common_prefix])
 }
 
+#[derive(PartialEq)]
+enum ScanMode {
+    DoubleQuote,
+    Escape,
+    EscapeInDoubleQuote,
+    Normal,
+    SingleQuote,
+}
+
+/// try to find an unclosed single/double quote in `s`.
+/// Return `None` if no unclosed quote is found.
+/// Return the unclosed quote position and if it is a double quote.
+fn find_unclosed_quote(s: &str) -> Option<(usize, bool)> {
+    let char_indices = s.char_indices();
+    let mut mode = ScanMode::Normal;
+    let mut quote_index = 0;
+    for (index, char) in char_indices {
+        match mode {
+            ScanMode::DoubleQuote => {
+                if char == '"' {
+                    mode = ScanMode::Normal;
+                } else if char == '\\' {
+                    mode = ScanMode::EscapeInDoubleQuote;
+                }
+            }
+            ScanMode::Escape => {
+                mode = ScanMode::Normal;
+            }
+            ScanMode::EscapeInDoubleQuote => {
+                mode = ScanMode::DoubleQuote;
+            }
+            ScanMode::Normal => {
+                if char == '"' {
+                    mode = ScanMode::DoubleQuote;
+                    quote_index = index;
+                } else if char == '\\' && cfg!(not(windows)) {
+                    mode = ScanMode::Escape;
+                } else if char == '\'' && cfg!(not(windows)) {
+                    mode = ScanMode::SingleQuote;
+                    quote_index = index;
+                }
+            }
+            ScanMode::SingleQuote => {
+                if char == '\'' {
+                    mode = ScanMode::Normal;
+                } // no escape in single quotes
+            }
+        };
+    }
+    if ScanMode::DoubleQuote == mode || ScanMode::SingleQuote == mode {
+        return Some((quote_index, ScanMode::DoubleQuote == mode));
+    }
+    None
+}
+
 #[cfg(test)]
 mod tests {
     use std::collections::BTreeSet;
@@ -347,4 +424,17 @@
         let lcp = super::longest_common_prefix(&candidates);
         assert_eq!(Some("f"), lcp);
     }
+
+    #[test]
+    pub fn find_unclosed_quote() {
+        assert_eq!(None, super::find_unclosed_quote("ls /etc"));
+        assert_eq!(
+            Some((3, true)),
+            super::find_unclosed_quote("ls \"User Information")
+        );
+        assert_eq!(
+            None,
+            super::find_unclosed_quote("ls \"/User Information\" /etc")
+        );
+    }
 }