Tab completion for commands

Change-Id: Ia38120da7b45cb75effcb2c93eee148419c2fa09
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c03cb47..5656ee8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -175,6 +175,7 @@
     target_include_directories(test_sysrepo PRIVATE ${PROJECT_SOURCE_DIR}/tests/mock)
     cli_test(utils)
     cli_test(path_completion)
+    cli_test(command_completion)
 endif()
 
 if(WITH_DOCS)
diff --git a/src/ast_commands.hpp b/src/ast_commands.hpp
index 787fb94..3a59e26 100644
--- a/src/ast_commands.hpp
+++ b/src/ast_commands.hpp
@@ -7,6 +7,7 @@
 */
 #pragma once
 
+#include <boost/mpl/vector.hpp>
 #include "ast_path.hpp"
 #include "ast_values.hpp"
 
@@ -34,46 +35,57 @@
 };
 
 struct discard_ : x3::position_tagged {
+    static constexpr auto name = "discard";
     bool operator==(const discard_& b) const;
 };
 
 struct ls_ : x3::position_tagged {
+    static constexpr auto name = "ls";
     bool operator==(const ls_& b) const;
     std::vector<LsOption> m_options;
     boost::optional<boost::variant<dataPath_, schemaPath_>> m_path;
 };
 
 struct cd_ : x3::position_tagged {
+    static constexpr auto name = "cd";
     bool operator==(const cd_& b) const;
     dataPath_ m_path;
 };
 
 struct create_ : x3::position_tagged {
+    static constexpr auto name = "create";
     bool operator==(const create_& b) const;
     dataPath_ m_path;
 };
 
 struct delete_ : x3::position_tagged {
+    static constexpr auto name = "delete";
     bool operator==(const delete_& b) const;
     dataPath_ m_path;
 };
 
 struct set_ : x3::position_tagged {
+    static constexpr auto name = "set";
     bool operator==(const set_& b) const;
     dataPath_ m_path;
     leaf_data_ m_data;
 };
 
 struct commit_ : x3::position_tagged {
+    static constexpr auto name = "commit";
     bool operator==(const set_& b) const;
 };
 
 struct get_ : x3::position_tagged {
+    static constexpr auto name = "get";
     bool operator==(const get_& b) const;
     boost::optional<boost::variant<dataPath_, schemaPath_>> m_path;
 };
 
-using command_ = boost::variant<discard_, ls_, cd_, create_, delete_, set_, commit_, get_>;
+// TODO: The usage of MPL won't be necessary after std::variant support is added to Spirit
+// https://github.com/boostorg/spirit/issues/270
+using CommandTypes = boost::mpl::vector<discard_, ls_, cd_, create_, delete_, set_, commit_, get_>;
+using command_ = boost::make_variant_over<CommandTypes>::type;
 
 BOOST_FUSION_ADAPT_STRUCT(ls_, m_options, m_path)
 BOOST_FUSION_ADAPT_STRUCT(cd_, m_path)
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index 5a55f36..f0bbf4c 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -8,6 +8,8 @@
 
 #pragma once
 
+#include <boost/mpl/for_each.hpp>
+#include "ast_commands.hpp"
 #include "parser_context.hpp"
 #include "schema.hpp"
 #include "utils.hpp"
@@ -534,3 +536,25 @@
         }
     }
 };
+
+struct commandNamesVisitor {
+    template <typename T>
+    auto operator()(boost::type<T>)
+    {
+        return T::name;
+    }
+};
+
+struct createCommandSuggestions_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);
+        parserContext.m_completionIterator = begin;
+
+        parserContext.m_suggestions.clear();
+        boost::mpl::for_each<CommandTypes, boost::type<boost::mpl::_>>([&parserContext](auto cmd) {
+            parserContext.m_suggestions.emplace(commandNamesVisitor()(cmd));
+        });
+    }
+};
diff --git a/src/grammars.hpp b/src/grammars.hpp
index 2bf7be4..e532174 100644
--- a/src/grammars.hpp
+++ b/src/grammars.hpp
@@ -57,6 +57,7 @@
 x3::rule<createPathSuggestions_class, x3::unused_type> const createPathSuggestions = "createPathSuggestions";
 x3::rule<createKeySuggestions_class, x3::unused_type> const createKeySuggestions = "createKeySuggestions";
 x3::rule<suggestKeysEnd_class, x3::unused_type> const suggestKeysEnd = "suggestKeysEnd";
+x3::rule<createCommandSuggestions_class, x3::unused_type> const createCommandSuggestions = "createCommandSuggestions";
 
 #if __clang__
 #pragma GCC diagnostic push
@@ -236,8 +237,11 @@
 auto const discard_def =
         lit("discard") >> x3::attr(discard_());
 
+auto const createCommandSuggestions_def =
+        x3::eps;
+
 auto const command_def =
-        x3::expect[cd | create | delete_rule | set | commit | get | ls | discard];
+        createCommandSuggestions >> x3::expect[cd | create | delete_rule | set | commit | get | ls | discard];
 
 #if __clang__
 #pragma GCC diagnostic pop
@@ -285,3 +289,4 @@
 BOOST_SPIRIT_DEFINE(createPathSuggestions)
 BOOST_SPIRIT_DEFINE(createKeySuggestions)
 BOOST_SPIRIT_DEFINE(suggestKeysEnd)
+BOOST_SPIRIT_DEFINE(createCommandSuggestions)
diff --git a/tests/command_completion.cpp b/tests/command_completion.cpp
new file mode 100644
index 0000000..f3cc868
--- /dev/null
+++ b/tests/command_completion.cpp
@@ -0,0 +1,64 @@
+/*
+ * 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 "parser.hpp"
+#include "static_schema.hpp"
+
+TEST_CASE("command completion")
+{
+    auto schema = std::make_shared<StaticSchema>();
+    Parser parser(schema);
+    std::string input;
+    std::ostringstream errorStream;
+    std::set<std::string> expected;
+    SECTION("")
+    {
+        input = "";
+        expected = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard"};
+    }
+
+    SECTION(" ")
+    {
+        input = " ";
+        expected = {"cd", "create", "delete", "set", "commit", "get", "ls", "discard"};
+    }
+
+    SECTION("c")
+    {
+        input = "c";
+        expected = {"d", "ommit", "reate"};
+    }
+
+    SECTION("d")
+    {
+        input = "d";
+        expected = {"elete", "iscard"};
+    }
+
+    SECTION("x")
+    {
+        input = "x";
+        expected = {};
+    }
+
+    SECTION("cd")
+    {
+        input = "cd";
+        // TODO: depending on how Readline works, this will have to be changed to include a space
+        expected = {""};
+    }
+
+    SECTION("create")
+    {
+        input = "create";
+        expected = {""};
+    }
+
+    REQUIRE(parser.completeCommand(input, errorStream) == expected);
+}