Tab completion for paths

There were several problems with the implementation of the completion.
Firstly, a way to propagate completions back from the parser was needed.
That was rather easy to solve with the ParserContext class. Next problem
was prefix matching, or specifically, where will it happen. There are
three possible situations, that can happen, after the input goes through
the parser:

1) The parsing was successful and there is no more input. Then there is
no need for prefix matching and completions are returned as they are:
if there was a valid path "first/second" and the user entered "cd
first/second/". Then the whole input would be parsed and completions
would be the nodes contained within "first/second".

2) The parsing was successful and there is more unparsed input. Then the
completions need to be matched with the input and the common prefix
deleted: if there were valid paths "first/second", "first/service" and
"first/something", and the user inputs "cd first/se", then "se" would be
left unparsed and it would be used as a prefix to change the
completions: they would be "cond" and "rvice".

3) The parsing was successful, there is no more input, but the parsed
input could still be completed. This means I need to know, where exactly
(in the input string) were the completions generated. Example: if there
were valid paths "first/min" and "first/minister", and the user inputs
"cd first/min". Then the parsing is completed successfully, the user
could press enter and execute the command, or generate completions and
"minister" would pop up.

Because there is no way to see the whole input in the AST handlers, the
matching had to be done outside the grammar (that is, in the
completeCommand method). The problem is mainly with the third situation.
Fortunately, I can save the position, where the completions were
generated, and use this information in the completeCommand method. This
solution also has the advantage of less duplicate code since we do all
matching in the same place.

The last problem was when do we exactly create the suggestions. One
option was to create them right after the "node" parser succeeds (in its
AST handler). Unfortunately, that created some problems if the parsing
succeeded but no additional user input was present. To solve this, a
pseudo-parser (a nothing parser) called createSuggestions was created.
This parser generates suggestions in-between nodes, so it's not
dependent on the "node" parser.

Change-Id: I02d9b5d23d7e695bfe7a5e168d9fefe931bc04ef
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9864ce4..05283b8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -171,7 +171,8 @@
     cli_test(sysrepo)
     target_link_libraries(test_sysrepo sysreposubscription sysrepoaccess yangschema parser)
     target_include_directories(test_sysrepo PRIVATE ${PROJECT_SOURCE_DIR}/tests/mock)
-
+    cli_test(utils)
+    cli_test(path_completion)
 endif()
 
 if(WITH_DOCS)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 717f077..b9b9101 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -469,7 +469,7 @@
     }
 };
 
-struct initializeContext_class {
+struct initializePath_class {
     template <typename T, typename Iterator, typename Context>
     void on_success(Iterator const&, Iterator const&, T&, Context const& context)
     {
@@ -477,6 +477,7 @@
         parserContext.m_curPath = parserContext.m_curPathOrig;
         parserContext.m_tmpListKeys.clear();
         parserContext.m_tmpListName.clear();
+        parserContext.m_suggestions.clear();
         if (!parserContext.m_curPath.m_nodes.empty() && parserContext.m_curPath.m_nodes.at(0).m_prefix)
             parserContext.m_topLevelModulePresent = true;
         else
@@ -485,3 +486,15 @@
 };
 
 struct trailingSlash_class;
+
+struct createPathSuggestions_class {
+    template <typename T, typename Iterator, typename Context>
+    void on_success(Iterator const& begin, Iterator const&, T&, Context const& context)
+    {
+        auto& parserContext = x3::get<parser_context_tag>(context);
+        const auto& schema = parserContext.m_schema;
+
+        parserContext.m_completionIterator = begin;
+        parserContext.m_suggestions = schema.childNodes(parserContext.m_curPath, Recursion::NonRecursive);
+    }
+};
diff --git a/src/grammars.hpp b/src/grammars.hpp
index d0101fb..79cd17a 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -53,7 +53,8 @@
 x3::rule<commit_class, commit_> const commit = "commit";
 x3::rule<command_class, command_> const command = "command";
 
-x3::rule<initializeContext_class, x3::unused_type> const initializeContext = "initializeContext";
+x3::rule<initializePath_class, x3::unused_type> const initializePath = "initializePath";
+x3::rule<createPathSuggestions_class, x3::unused_type> const createPathSuggestions = "createPathSuggestions";
 
 #if __clang__
 #pragma GCC diagnostic push
@@ -110,12 +111,15 @@
 auto const leaf_def =
         node_identifier;
 
+auto const createPathSuggestions_def =
+        x3::eps;
+
 // leaf cannot be in the middle of a path, however, I need the grammar's attribute to be a vector of variants
 auto const schemaNode_def =
-        -(module) >> x3::expect[container | list | nodeup | leaf];
+        createPathSuggestions >> -(module) >> (container | list | nodeup | leaf);
 
 auto const dataNode_def =
-        -(module) >> (container | listElement | nodeup | leaf);
+        createPathSuggestions >> -(module) >> (container | listElement | nodeup | leaf);
 
 auto const absoluteStart_def =
         x3::omit['/'] >> x3::attr(Scope::Absolute);
@@ -123,29 +127,32 @@
 auto const trailingSlash_def =
         x3::omit['/'] >> x3::attr(TrailingSlash::Present);
 
+auto const space_separator =
+        x3::omit[x3::no_skip[space]];
+
 // I have to insert an empty vector to the first alternative, otherwise they won't have the same attribute
 auto const dataPath_def =
-        absoluteStart >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi |
-        -(absoluteStart) >> dataNode % '/' >> -trailingSlash;
+        initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi |
+        initializePath >> -(absoluteStart >> createPathSuggestions) >> dataNode % '/' >> (trailingSlash >> createPathSuggestions | (&space_separator | x3::eoi));
 
 auto const dataNodeList_def =
-        -(module) >> list;
+        -(module) >> createPathSuggestions >> list;
 
 // This intermediate rule is mandatory, because we need the first alternative
 // to be collapsed to a vector. If we didn't use the intermediate rule,
 // Spirit wouldn't know we want it to collapse.
 // https://github.com/boostorg/spirit/issues/408
 auto const dataNodesListEnd_def =
-        dataNode % '/' >> '/' >> dataNodeList |
-        initializeContext >> x3::attr(decltype(dataPath_::m_nodes)()) >> dataNodeList;
+        initializePath >> dataNode % '/' >> '/' >> dataNodeList >> -(&char_('/') >> createPathSuggestions) |
+        initializePath >> x3::attr(decltype(dataPath_::m_nodes)()) >> dataNodeList;
 
 auto const dataPathListEnd_def =
-        absoluteStart >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi |
-        -(absoluteStart) >> dataNodesListEnd >> -trailingSlash;
+        initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(dataPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi |
+        initializePath >> -(absoluteStart >> createPathSuggestions) >> dataNodesListEnd >> (trailingSlash >> createPathSuggestions | (&space_separator | x3::eoi));
 
 auto const schemaPath_def =
-        absoluteStart >> x3::attr(decltype(schemaPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi |
-        -(absoluteStart) >> schemaNode % '/' >> -trailingSlash;
+        initializePath >> absoluteStart >> createPathSuggestions >> x3::attr(decltype(schemaPath_::m_nodes)()) >> x3::attr(TrailingSlash::NonPresent) >> x3::eoi |
+        initializePath >> -(absoluteStart >> createPathSuggestions) >> schemaNode % '/' >> (trailingSlash >> createPathSuggestions | (&space_separator | x3::eoi));
 
 auto const leafPath_def =
         dataPath;
@@ -182,9 +189,6 @@
         leaf_data_uint |
         leaf_data_string];
 
-auto const space_separator =
-        x3::omit[x3::no_skip[space]];
-
 struct ls_options_table : x3::symbols<LsOption> {
     ls_options_table()
     {
@@ -193,12 +197,12 @@
     }
 } const ls_options;
 
-// A "nothing" parser, which is used to reset the context (when trying to parse different types of paths)
-auto const initializeContext_def =
+// A "nothing" parser, which is used to indicate we tried to parse a path
+auto const initializePath_def =
         x3::eps;
 
 auto const ls_def =
-        lit("ls") >> *(space_separator >> ls_options) >> -(space_separator >> (dataPathListEnd | initializeContext >> dataPath));
+        lit("ls") >> *(space_separator >> ls_options) >> -(space_separator >> (dataPathListEnd | dataPath | schemaPath));
 
 auto const cd_def =
         lit("cd") >> space_separator > dataPath;
@@ -210,7 +214,7 @@
         lit("delete") >> space_separator > dataPath;
 
 auto const get_def =
-        lit("get") >> -(space_separator >> (dataPathListEnd | initializeContext >> dataPath));
+        lit("get") >> -(space_separator >> (dataPathListEnd | dataPath));
 
 auto const set_def =
         lit("set") >> space_separator > leafPath > leaf_data;
@@ -222,7 +226,7 @@
         lit("discard") >> x3::attr(discard_());
 
 auto const command_def =
-        x3::expect[cd | create | delete_rule | set | commit | get | ls | discard] >> x3::eoi;
+        x3::expect[cd | create | delete_rule | set | commit | get | ls | discard];
 
 #if __clang__
 #pragma GCC diagnostic pop
@@ -257,7 +261,7 @@
 BOOST_SPIRIT_DEFINE(leaf_data_int)
 BOOST_SPIRIT_DEFINE(leaf_data_uint)
 BOOST_SPIRIT_DEFINE(leaf_data_string)
-BOOST_SPIRIT_DEFINE(initializeContext)
+BOOST_SPIRIT_DEFINE(initializePath)
 BOOST_SPIRIT_DEFINE(set)
 BOOST_SPIRIT_DEFINE(commit)
 BOOST_SPIRIT_DEFINE(get)
@@ -267,3 +271,4 @@
 BOOST_SPIRIT_DEFINE(create)
 BOOST_SPIRIT_DEFINE(delete_rule)
 BOOST_SPIRIT_DEFINE(command)
+BOOST_SPIRIT_DEFINE(createPathSuggestions)
diff --git a/src/main.cpp b/src/main.cpp
index eaba76a..3c973dd 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -39,6 +39,12 @@
     Parser parser(datastore.schema());
 
     while (true) {
+        linenoise::SetCompletionCallback([&parser](const char* editBuffer, std::vector<std::string>& completions) {
+            std::stringstream stream;
+            auto completionsSet = parser.completeCommand(editBuffer, stream);
+            std::transform(completionsSet.begin(), completionsSet.end(), std::back_inserter(completions),
+                           [editBuffer](auto it) { return std::string(editBuffer) + it; });
+        });
         linenoise::SetHistoryMaxLen(4);
         linenoise::SetMultiLine(true);
         std::string line;
diff --git a/src/parser.cpp b/src/parser.cpp
index ee0233e..2dbea6b 100644
--- a/src/parser.cpp
+++ b/src/parser.cpp
@@ -39,6 +39,23 @@
     return parsedCommand;
 }
 
+std::set<std::string> Parser::completeCommand(const std::string& line, std::ostream& errorStream) const
+{
+    std::set<std::string> completions;
+    command_ parsedCommand;
+    ParserContext ctx(*m_schema, dataPathToSchemaPath(m_curDir));
+    auto it = line.begin();
+    boost::spirit::x3::error_handler<std::string::const_iterator> errorHandler(it, line.end(), errorStream);
+
+    auto grammar =
+            x3::with<parser_context_tag>(ctx)[
+            x3::with<x3::error_handler_tag>(std::ref(errorHandler))[command]
+    ];
+    x3::phrase_parse(it, line.end(), grammar, space, parsedCommand);
+
+    return filterAndErasePrefix(ctx.m_suggestions, std::string(ctx.m_completionIterator, line.end()));
+}
+
 void Parser::changeNode(const dataPath_& name)
 {
     if (name.m_scope == Scope::Absolute) {
diff --git a/src/parser.hpp b/src/parser.hpp
index 343a1e5..8c9290f 100644
--- a/src/parser.hpp
+++ b/src/parser.hpp
@@ -30,6 +30,7 @@
     void changeNode(const dataPath_& name);
     std::string currentNode() const;
     std::set<std::string> availableNodes(const boost::optional<boost::variant<dataPath_, schemaPath_>>& path, const Recursion& option) const;
+    std::set<std::string> 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 a0b8e30..5d0fe38 100644
--- a/src/parser_context.hpp
+++ b/src/parser_context.hpp
@@ -18,4 +18,7 @@
     bool m_topLevelModulePresent = false;
     std::set<std::string> m_tmpListKeys;
     bool m_errorHandled = false;
+    std::set<std::string> m_suggestions;
+    // Iterator pointing to where suggestions were created
+    std::string::const_iterator m_completionIterator;
 };
diff --git a/src/utils.cpp b/src/utils.cpp
index f9eae06..358bc05 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -5,6 +5,8 @@
  * Written by Václav Kubernát <kubervac@fit.cvut.cz>
  *
 */
+#include <boost/algorithm/string/erase.hpp>
+#include <boost/algorithm/string/predicate.hpp>
 #include "utils.hpp"
 
 std::string joinPaths(const std::string& prefix, const std::string& suffix)
@@ -64,3 +66,18 @@
 {
     return fullNodeName(dataPathToSchemaPath(location), pair);
 }
+
+std::set<std::string> filterAndErasePrefix(const std::set<std::string>& set, const std::string_view prefix)
+{
+    std::set<std::string> filtered;
+    std::copy_if(set.begin(), set.end(),
+            std::inserter(filtered, filtered.end()),
+            [prefix] (auto it) { return boost::starts_with(it, prefix); });
+
+    std::set<std::string> withoutPrefix;
+    std::transform(filtered.begin(), filtered.end(),
+            std::inserter(withoutPrefix, withoutPrefix.end()),
+            [prefix] (auto it) { boost::erase_first(it, prefix); return it; });
+    return withoutPrefix;
+}
+
diff --git a/src/utils.hpp b/src/utils.hpp
index f009ccd..38db0af 100644
--- a/src/utils.hpp
+++ b/src/utils.hpp
@@ -5,6 +5,9 @@
  * Written by Václav Kubernát <kubervac@fit.cvut.cz>
  *
 */
+/*! \file utils.hpp
+    \brief A header containing utility functions
+*/
 #include <string>
 #include "ast_path.hpp"
 #include "schema.hpp"
@@ -16,3 +19,7 @@
 std::string leafDataTypeToString(yang::LeafDataTypes type);
 std::string fullNodeName(const schemaPath_& location, const ModuleNodePair& pair);
 std::string fullNodeName(const dataPath_& location, const ModuleNodePair& pair);
+/** Returns a subset of the original set with only the strings starting with prefix
+ * and with the actual prefix deleted from the string
+ */
+std::set<std::string> filterAndErasePrefix(const std::set<std::string>& set, const std::string_view prefix);
diff --git a/tests/path_completion.cpp b/tests/path_completion.cpp
new file mode 100644
index 0000000..cd1a2c6
--- /dev/null
+++ b/tests/path_completion.cpp
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
+ * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#include "trompeloeil_catch.h"
+#include "ast_commands.hpp"
+#include "parser.hpp"
+#include "static_schema.hpp"
+
+TEST_CASE("path_completion")
+{
+    auto schema = std::make_shared<StaticSchema>();
+    schema->addModule("example");
+    schema->addModule("second");
+    schema->addContainer("", "example:ano");
+    schema->addContainer("", "example:anoda");
+    schema->addList("example:ano", "example:listInCont", {"number"});
+    schema->addContainer("", "second:amelie");
+    schema->addContainer("", "example:bota");
+    schema->addContainer("example:ano", "example:a2");
+    schema->addContainer("example:bota", "example:b2");
+    schema->addContainer("example:ano/example:a2", "example:a3");
+    schema->addContainer("example:bota/example:b2", "example:b3");
+    schema->addList("", "example:list", {"number"});
+    schema->addContainer("example:list", "example:contInList");
+    schema->addList("", "example:twoKeyList", {"number", "name"});
+    Parser parser(schema);
+    std::string input;
+    std::ostringstream errorStream;
+    std::set<std::string> expected;
+
+    SECTION("ls ")
+    {
+        input = "ls ";
+        expected = {"example:ano", "example:anoda", "example:bota", "second:amelie", "example:list", "example:twoKeyList"};
+    }
+
+    SECTION("ls e")
+    {
+        input = "ls e";
+        expected = {"xample:ano", "xample:anoda", "xample:bota", "xample:list", "xample:twoKeyList"};
+    }
+
+    SECTION("ls example:ano")
+    {
+        input = "ls example:ano";
+        expected = {"", "da"};
+    }
+
+    SECTION("ls example:ano/example:a")
+    {
+        input = "ls example:ano/example:a";
+        expected = {"2"};
+    }
+
+    SECTION("ls x")
+    {
+        input = "ls x";
+        expected = {};
+    }
+
+    SECTION("ls /")
+    {
+        input = "ls /";
+        expected = {"example:ano", "example:anoda", "example:bota", "second:amelie", "example:list", "example:twoKeyList"};
+    }
+
+    SECTION("ls /e")
+    {
+        input = "ls /e";
+        expected = {"xample:ano", "xample:anoda", "xample:bota", "xample:list", "xample:twoKeyList"};
+    }
+
+    SECTION("ls /s")
+    {
+        input = "ls /s";
+        expected = {"econd:amelie"};
+    }
+
+    SECTION("ls /example:list[number=3]/")
+    {
+        input = "ls /example:list[number=3]/";
+        expected = {"example:contInList"};
+    }
+
+    SECTION("ls /example:list[number=3]/c")
+    {
+        input = "ls /example:list[number=3]/e";
+        expected = {"xample:contInList"};
+    }
+
+    SECTION("ls /example:list[number=3]/a")
+    {
+        input = "ls /example:list[number=3]/a";
+        expected = {};
+    }
+
+    REQUIRE(parser.completeCommand(input, errorStream) == expected);
+}
diff --git a/tests/utils.cpp b/tests/utils.cpp
new file mode 100644
index 0000000..62e4c23
--- /dev/null
+++ b/tests/utils.cpp
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2018 CESNET, https://photonics.cesnet.cz/
+ * Copyright (C) 2018 FIT CVUT, https://fit.cvut.cz/
+ *
+ * Written by Václav Kubernát <kubervac@fit.cvut.cz>
+ *
+*/
+
+#include "trompeloeil_catch.h"
+#include "utils.hpp"
+
+TEST_CASE("utils")
+{
+    SECTION("filterAndErasePrefix")
+    {
+        std::set<std::string> set{"ahoj", "coze", "copak", "aha", "polivka"};
+
+        REQUIRE((filterAndErasePrefix(set, "a") == std::set<std::string>{"hoj", "ha"}));
+        REQUIRE((filterAndErasePrefix(set, "ah") == std::set<std::string>{"oj", "a"}));
+        REQUIRE((filterAndErasePrefix(set, "aho") == std::set<std::string>{"j"}));
+        REQUIRE((filterAndErasePrefix(set, "polivka") == std::set<std::string>{""}));
+        REQUIRE((filterAndErasePrefix(set, "polivka") == std::set<std::string>{""}));
+        REQUIRE((filterAndErasePrefix(set, "polivkax") == std::set<std::string>{}));
+        REQUIRE((filterAndErasePrefix(set, "co") == std::set<std::string>{"pak", "ze"}));
+    }
+
+}