Change how word splitting works when completing

Previously, I relied on replxx to correctly split words based on
word-splitting characters. However, as completion gets more complex and
completions possibly insert word-splitting characters, it starts to do
weird stuff like deleting some of your input. Fortunately, replxx allows
you to set the context length for completion - that is, how many
character it should consider as part of the word you're completing.

Change-Id: I035ac5059c8ab125efedb90cbeb2910f20da04a7
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 0ce611d..d99e020 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -337,6 +337,7 @@
     {
         auto& parserContext = x3::get<parser_context_tag>(context);
         parserContext.m_suggestions.clear();
+        parserContext.m_completionIterator = boost::none;
         parserContext.m_completionSuffix.clear();
     }
 };
diff --git a/src/main.cpp b/src/main.cpp
index f1a2603..c6e3fce 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -38,16 +38,15 @@
     SysrepoAccess datastore("netconf-cli");
     Parser parser(datastore.schema());
     replxx::Replxx lineEditor;
-    lineEditor.set_completion_callback([&parser](const std::string& input, int&) {
+    lineEditor.set_completion_callback([&parser](const std::string& input, int& context) {
         std::stringstream stream;
-        auto completionsSet = parser.completeCommand(input, stream);
+        auto completions = parser.completeCommand(input, stream);
 
         std::vector<replxx::Replxx::Completion> res;
-        std::transform(completionsSet.begin(), completionsSet.end(), std::back_inserter(res),
-                [input](auto it) { return it; });
+        std::copy(completions.m_completions.begin(), completions.m_completions.end(), std::back_inserter(res));
+        context = completions.m_contextLength;
         return res;
     });
-    lineEditor.set_word_break_characters(" '/[");
 
     std::optional<std::string> historyFile;
     if (auto xdgHome = getenv("XDG_DATA_HOME")) {
diff --git a/src/parser.cpp b/src/parser.cpp
index de375f8..c3dc088 100644
--- a/src/parser.cpp
+++ b/src/parser.cpp
@@ -21,6 +21,11 @@
     m_curDir.m_scope = Scope::Absolute;
 }
 
+bool Completions::operator==(const Completions& b) const
+{
+    return this->m_completions == b.m_completions && this->m_contextLength == b.m_contextLength;
+}
+
 command_ Parser::parseCommand(const std::string& line, std::ostream& errorStream)
 {
     command_ parsedCommand;
@@ -42,7 +47,7 @@
     return parsedCommand;
 }
 
-std::set<std::string> Parser::completeCommand(const std::string& line, std::ostream& errorStream) const
+Completions Parser::completeCommand(const std::string& line, std::ostream& errorStream) const
 {
     std::set<std::string> completions;
     command_ parsedCommand;
@@ -57,11 +62,15 @@
     ];
     x3::phrase_parse(it, line.end(), grammar, space, parsedCommand);
 
-    auto set = filterByPrefix(ctx.m_suggestions, std::string(ctx.m_completionIterator, line.end()));
+    auto completionIterator = ctx.m_completionIterator ? *ctx.m_completionIterator : line.end();
+
+    int completionContext = line.end() - completionIterator;
+
+    auto set = filterByPrefix(ctx.m_suggestions, std::string(completionIterator, line.end()));
     if (set.size() == 1) {
-        return {(*set.begin()) + ctx.m_completionSuffix};
+        return {{(*set.begin()) + ctx.m_completionSuffix}, completionContext};
     }
-    return set;
+    return {set, completionContext};
 }
 
 void Parser::changeNode(const dataPath_& name)
diff --git a/src/parser.hpp b/src/parser.hpp
index 3c009e6..c2cd8d6 100644
--- a/src/parser.hpp
+++ b/src/parser.hpp
@@ -24,6 +24,12 @@
     ~TooManyArgumentsException() override;
 };
 
+struct Completions {
+    bool operator==(const Completions& b) const;
+    std::set<std::string> m_completions;
+    int m_contextLength;
+};
+
 class Parser {
 public:
     Parser(const std::shared_ptr<const Schema> schema);
@@ -31,7 +37,7 @@
     void changeNode(const dataPath_& name);
     std::string currentNode() const;
     std::set<std::string> availableNodes(const boost::optional<boost::variant<boost::variant<dataPath_, schemaPath_>, module_>>& path, const Recursion& option) const;
-    std::set<std::string> completeCommand(const std::string& line, std::ostream& errorStream) const;
+    Completions completeCommand(const std::string& line, std::ostream& errorStream) const;
 
 private:
     const std::shared_ptr<const Schema> m_schema;
diff --git a/src/parser_context.hpp b/src/parser_context.hpp
index 10a6634..77357e7 100644
--- a/src/parser_context.hpp
+++ b/src/parser_context.hpp
@@ -32,7 +32,7 @@
     bool m_completing = false;
     std::set<std::string> m_suggestions;
     // Iterator pointing to where suggestions were created
-    std::string::const_iterator m_completionIterator;
+    boost::optional<std::string::const_iterator> m_completionIterator;
     // If the parser determines that suggestions are unambiguous (after
     // filtering by prefix), this suffix gets added to the completion (for
     // example a left bracket after a list)
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
index 015b4db..d439394 100644
--- a/tests/command_completion.cpp
+++ b/tests/command_completion.cpp
@@ -8,6 +8,7 @@
 
 #include "trompeloeil_doctest.h"
 #include "parser.hpp"
+#include "pretty_printers.hpp"
 #include "static_schema.hpp"
 
 TEST_CASE("command completion")
@@ -16,49 +17,57 @@
     Parser parser(schema);
     std::string input;
     std::ostringstream errorStream;
-    std::set<std::string> expected;
+    std::set<std::string> expectedCompletions;
+    int expectedContextLength;
     SECTION("")
     {
         input = "";
-        expected = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help"};
+        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help"};
+        expectedContextLength = 0;
     }
 
     SECTION(" ")
     {
         input = " ";
-        expected = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help"};
+        expectedCompletions = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard", "help"};
+        expectedContextLength = 0;
     }
 
     SECTION("c")
     {
         input = "c";
-        expected = {"cd", "commit", "create"};
+        expectedCompletions = {"cd", "commit", "create"};
+        expectedContextLength = 1;
     }
 
     SECTION("d")
     {
         input = "d";
-        expected = {"delete", "discard"};
+        expectedCompletions = {"delete", "discard"};
+        expectedContextLength = 1;
     }
 
     SECTION("x")
     {
         input = "x";
-        expected = {};
+        expectedCompletions = {};
+        expectedContextLength = 1;
     }
 
     SECTION("cd")
     {
         input = "cd";
         // TODO: depending on how Readline works, this will have to be changed to include a space
-        expected = {"cd"};
+        expectedCompletions = {"cd"};
+        expectedContextLength = 2;
     }
 
     SECTION("create")
     {
         input = "create";
-        expected = {"create"};
+        expectedCompletions = {"create"};
+        expectedContextLength = 6;
     }
 
-    REQUIRE(parser.completeCommand(input, errorStream) == expected);
+    REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
 }
diff --git a/tests/enum_completion.cpp b/tests/enum_completion.cpp
index d7dfd58..29f23a7 100644
--- a/tests/enum_completion.cpp
+++ b/tests/enum_completion.cpp
@@ -9,6 +9,7 @@
 
 #include "trompeloeil_doctest.h"
 #include "parser.hpp"
+#include "pretty_printers.hpp"
 #include "static_schema.hpp"
 
 TEST_CASE("enum completion")
@@ -25,38 +26,44 @@
     std::string input;
     std::ostringstream errorStream;
 
-    std::set<std::string> expected;
+    std::set<std::string> expectedCompletions;
+    int expectedContextLength;
 
     SECTION("set mod:leafEnum ")
     {
         input = "set mod:leafEnum ";
-        expected = {"lala", "lol", "data", "coze"};
+        expectedCompletions = {"lala", "lol", "data", "coze"};
+        expectedContextLength = 0;
     }
 
     SECTION("set mod:leafEnum c")
     {
         input = "set mod:leafEnum c";
-        expected = {"coze"};
+        expectedCompletions = {"coze"};
+        expectedContextLength = 1;
     }
 
     SECTION("set mod:leafEnum l")
     {
         input = "set mod:leafEnum l";
-        expected = {"lala", "lol"};
+        expectedCompletions = {"lala", "lol"};
+        expectedContextLength = 1;
     }
 
 
     SECTION("set mod:contA/leafInCont ")
     {
         input = "set mod:contA/leafInCont ";
-        expected = {"abc", "def"};
+        expectedCompletions = {"abc", "def"};
+        expectedContextLength = 0;
     }
 
     SECTION("set mod:list[number=42]/leafInList ")
     {
         input = "set mod:list[number=42]/leafInList ";
-        expected = {"ano", "anoda", "ne", "katoda"};
+        expectedCompletions = {"ano", "anoda", "ne", "katoda"};
+        expectedContextLength = 0;
     }
 
-    REQUIRE(parser.completeCommand(input, errorStream) == expected);
+    REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
 }
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
index 77dae51..f84ae4f 100644
--- a/tests/path_completion.cpp
+++ b/tests/path_completion.cpp
@@ -6,21 +6,11 @@
  *
 */
 
-#include <experimental/iterator>
 #include "trompeloeil_doctest.h"
 #include "parser.hpp"
+#include "pretty_printers.hpp"
 #include "static_schema.hpp"
 
-namespace std {
-std::ostream& operator<<(std::ostream& s, const std::set<std::string> set)
-{
-    s << std::endl << "{";
-    std::copy(set.begin(), set.end(), std::experimental::make_ostream_joiner(s, ", "));
-    s << "}" << std::endl;
-    return s;
-}
-}
-
 TEST_CASE("path_completion")
 {
     auto schema = std::make_shared<StaticSchema>();
@@ -49,86 +39,105 @@
     Parser parser(schema);
     std::string input;
     std::ostringstream errorStream;
-    std::set<std::string> expected;
+
+    std::set<std::string> expectedCompletions;
+    // GCC complains here with -Wmaybe-uninitialized if I don't assign
+    // something here. I suspect it's because of nested SECTIONs. -1 is an
+    // invalid value (as in, I'll never expect expectedContextLength to be -1),
+    // so let's go with that.
+    int expectedContextLength = -1;
 
     SECTION("node name completion")
     {
         SECTION("ls ")
         {
             input = "ls ";
-            expected = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList[", "second:amelie/"};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList[", "second:amelie/"};
+            expectedContextLength = 0;
         }
 
         SECTION("ls e")
         {
             input = "ls e";
-            expected = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList["};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList["};
+            expectedContextLength = 1;
         }
 
         SECTION("ls example:ano")
         {
             input = "ls example:ano";
-            expected = {"example:ano/", "example:anoda/"};
+            expectedCompletions = {"example:ano/", "example:anoda/"};
+            expectedContextLength = 11;
         }
 
         SECTION("ls example:ano/example:a")
         {
             input = "ls example:ano/example:a";
-            expected = {"example:a2/"};
+            expectedCompletions = {"example:a2/"};
+            expectedContextLength = 9;
         }
 
         SECTION("ls x")
         {
             input = "ls x";
-            expected = {};
+            expectedCompletions = {};
+            expectedContextLength = 1;
         }
 
         SECTION("ls /")
         {
             input = "ls /";
-            expected = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList[", "second:amelie/"};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList[", "second:amelie/"};
+            expectedContextLength = 0;
         }
 
         SECTION("ls /e")
         {
             input = "ls /e";
-            expected = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList["};
+            expectedCompletions = {"example:ano/", "example:anoda/", "example:bota/", "example:leafInt ", "example:list[", "example:ovoce[", "example:ovocezelenina[", "example:twoKeyList["};
+            expectedContextLength = 1;
         }
 
         SECTION("ls example:bota")
         {
             input = "ls example:bota";
-            expected = {"example:bota/"};
+            expectedCompletions = {"example:bota/"};
+            expectedContextLength = 12;
         }
 
         SECTION("ls /example:bota")
         {
             input = "ls /example:bota";
-            expected = {"example:bota/"};
+            expectedCompletions = {"example:bota/"};
+            expectedContextLength = 12;
         }
 
         SECTION("ls /s")
         {
             input = "ls /s";
-            expected = {"second:amelie/"};
+            expectedCompletions = {"second:amelie/"};
+            expectedContextLength = 1;
         }
 
         SECTION("ls /example:list[number=3]/")
         {
             input = "ls /example:list[number=3]/";
-            expected = {"example:contInList/", "example:number "};
+            expectedCompletions = {"example:contInList/", "example:number "};
+            expectedContextLength = 0;
         }
 
         SECTION("ls /example:list[number=3]/c")
         {
             input = "ls /example:list[number=3]/e";
-            expected = {"example:contInList/", "example:number "};
+            expectedCompletions = {"example:contInList/", "example:number "};
+            expectedContextLength = 1;
         }
 
         SECTION("ls /example:list[number=3]/a")
         {
             input = "ls /example:list[number=3]/a";
-            expected = {};
+            expectedCompletions = {};
+            expectedContextLength = 1;
         }
     }
 
@@ -137,105 +146,122 @@
         SECTION("cd example:lis")
         {
             input = "cd example:lis";
-            expected = {"example:list["};
+            expectedCompletions = {"example:list["};
+            expectedContextLength = 11;
         }
 
         SECTION("set example:list")
         {
             input = "set example:list";
-            expected = {"example:list["};
+            expectedCompletions = {"example:list["};
+            expectedContextLength = 12;
         }
 
         SECTION("cd example:list")
         {
             input = "cd example:list";
-            expected = {"example:list["};
+            expectedCompletions = {"example:list["};
+            expectedContextLength = 12;
         }
 
         SECTION("cd example:list[")
         {
             input = "cd example:list[";
-            expected = {"number="};
+            expectedCompletions = {"number="};
+            expectedContextLength = 0;
         }
 
         SECTION("cd example:list[numb")
         {
             input = "cd example:list[numb";
-            expected = {"number="};
+            expectedCompletions = {"number="};
+            expectedContextLength = 4;
         }
 
         SECTION("cd example:list[number")
         {
             input = "cd example:list[number";
-            expected = {"number="};
+            expectedCompletions = {"number="};
+            expectedContextLength = 6;
         }
 
         SECTION("cd example:list[number=12")
         {
             input = "cd example:list[number=12";
-            expected = {"]/"};
+            expectedCompletions = {"]/"};
+            expectedContextLength = 0;
         }
 
         SECTION("cd example:list[number=12]")
         {
             input = "cd example:list[number=12]";
-            expected = {"]/"};
+            expectedCompletions = {"]/"};
+            expectedContextLength = 1;
         }
 
         SECTION("cd example:twoKeyList[")
         {
             input = "cd example:twoKeyList[";
-            expected = {"name=", "number="};
+            expectedCompletions = {"name=", "number="};
+            expectedContextLength = 0;
         }
 
         SECTION("cd example:twoKeyList[name=\"AHOJ\"")
         {
             input = "cd example:twoKeyList[name=\"AHOJ\"";
-            expected = {"]["};
+            expectedCompletions = {"]["};
+            expectedContextLength = 0;
         }
 
         SECTION("cd example:twoKeyList[name=\"AHOJ\"]")
         {
             input = "cd example:twoKeyList[name=\"AHOJ\"]";
-            expected = {"]["};
+            expectedCompletions = {"]["};
+            expectedContextLength = 1;
         }
 
         SECTION("cd example:twoKeyList[name=\"AHOJ\"][")
         {
             input = "cd example:twoKeyList[name=\"AHOJ\"][";
-            expected = {"number="};
+            expectedCompletions = {"number="};
+            expectedContextLength = 0;
         }
 
         SECTION("cd example:twoKeyList[number=42][")
         {
             input = "cd example:twoKeyList[number=42][";
-            expected = {"name="};
+            expectedCompletions = {"name="};
+            expectedContextLength = 0;
         }
 
         SECTION("cd example:twoKeyList[name=\"AHOJ\"][number=123")
         {
             input = "cd example:twoKeyList[name=\"AHOJ\"][number=123";
-            expected = {"]/"};
+            expectedCompletions = {"]/"};
+            expectedContextLength = 0;
         }
 
         SECTION("cd example:twoKeyList[name=\"AHOJ\"][number=123]")
         {
             input = "cd example:twoKeyList[name=\"AHOJ\"][number=123]";
-            expected = {"]/"};
+            expectedCompletions = {"]/"};
+            expectedContextLength = 1;
         }
 
         SECTION("cd example:ovoce")
         {
             input = "cd example:ovoce";
-            expected = {"example:ovoce[", "example:ovocezelenina["};
+            expectedCompletions = {"example:ovoce[", "example:ovocezelenina["};
+            expectedContextLength = 13;
         }
     }
 
     SECTION("clear completions when no longer inputting path")
     {
         input = "set example:leafInt ";
-        expected = {};
+        expectedCompletions = {};
+        expectedContextLength = 0;
     }
 
-    REQUIRE(parser.completeCommand(input, errorStream) == expected);
+    REQUIRE(parser.completeCommand(input, errorStream) == (Completions{expectedCompletions, expectedContextLength}));
 }
diff --git a/tests/pretty_printers.hpp b/tests/pretty_printers.hpp
new file mode 100644
index 0000000..42be33e
--- /dev/null
+++ b/tests/pretty_printers.hpp
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+
+#include <experimental/iterator>
+#include "parser.hpp"
+namespace std {
+std::ostream& operator<<(std::ostream& s, const Completions& completion)
+{
+    s << std::endl << "Completions {" << std::endl << "    m_completions: ";
+    std::transform(completion.m_completions.begin(), completion.m_completions.end(),
+            std::experimental::make_ostream_joiner(s, ", "),
+            [] (auto it) { return '"' + it + '"'; });
+    s << std::endl << "    m_contextLength: " << completion.m_contextLength << std::endl;
+    s << "}" << std::endl;
+    return s;
+}
+}