Separate tab completion of the initial [ for lists

US: https://tree.taiga.io/project/jktjkt-netconf-cli/us/144
Change-Id: I03ffc2830c62bbf833fb1075e2dbead55b0ca838
diff --git a/src/ast_handlers.cpp b/src/ast_handlers.cpp
index 7738085..7def513 100644
--- a/src/ast_handlers.cpp
+++ b/src/ast_handlers.cpp
@@ -1,5 +1,5 @@
 #include "ast_handlers.hpp"
-std::set<std::string> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentKeys)
+std::set<Completion> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentKeys)
 {
     std::set<std::string> missingKeys;
 
@@ -9,11 +9,9 @@
         }
     }
 
-    std::set<std::string> res;
+    std::set<Completion> res;
 
-    std::transform(missingKeys.begin(), missingKeys.end(),
-                   std::inserter(res, res.end()),
-                   [] (auto it) { return it + "="; });
+    std::transform(missingKeys.begin(), missingKeys.end(), std::inserter(res, res.end()), [](auto it) { return Completion{it + "="}; });
     return res;
 }
 
@@ -26,14 +24,14 @@
     return leafDataToString(value);
 }
 
-template<>
-decltype(ParserContext::m_suggestions) createSetSuggestions_class<yang::LeafDataTypes::Enum>::getSuggestions(const ParserContext& parserContext, const Schema& schema) const
+template <>
+std::set<std::string> createSetSuggestions_class<yang::LeafDataTypes::Enum>::getSuggestions(const ParserContext& parserContext, const Schema& schema) const
 {
     return schema.enumValues(parserContext.m_tmpListKeyLeafPath.m_location, parserContext.m_tmpListKeyLeafPath.m_node);
 }
 
-template<>
-decltype(ParserContext::m_suggestions) createSetSuggestions_class<yang::LeafDataTypes::IdentityRef>::getSuggestions(const ParserContext& parserContext, const Schema& schema) const
+template <>
+std::set<std::string> createSetSuggestions_class<yang::LeafDataTypes::IdentityRef>::getSuggestions(const ParserContext& parserContext, const Schema& schema) const
 {
     return schema.validIdentities(parserContext.m_tmpListKeyLeafPath.m_location, parserContext.m_tmpListKeyLeafPath.m_node, Prefixes::WhenNeeded);
 }
diff --git a/src/ast_handlers.hpp b/src/ast_handlers.hpp
index d5f972a..c046040 100644
--- a/src/ast_handlers.hpp
+++ b/src/ast_handlers.hpp
@@ -339,7 +339,6 @@
         auto& parserContext = x3::get<parser_context_tag>(context);
         parserContext.m_suggestions.clear();
         parserContext.m_completionIterator = boost::none;
-        parserContext.m_completionSuffix.clear();
     }
 };
 
@@ -539,7 +538,7 @@
 
         parserContext.m_completionIterator = begin;
         auto suggestions = schema.childNodes(parserContext.currentSchemaPath(), Recursion::NonRecursive);
-        std::set<std::string> suffixesAdded;
+        std::set<Completion> suffixesAdded;
         std::transform(suggestions.begin(), suggestions.end(),
             std::inserter(suffixesAdded, suffixesAdded.end()),
             [&parserContext, &schema] (auto it) {
@@ -553,21 +552,21 @@
                 }
 
                 if (schema.isLeaf(parserContext.currentSchemaPath(), node)) {
-                    return it + " ";
+                    return Completion{it + " "};
                 }
                 if (schema.isContainer(parserContext.currentSchemaPath(), node)) {
-                    return it + "/";
+                    return Completion{it + "/"};
                 }
                 if (schema.isList(parserContext.currentSchemaPath(), node)) {
-                    return it + "[";
+                    return Completion{it, "[", Completion::WhenToAdd::IfFullMatch};
                 }
-                return it;
+                return Completion{it};
         });
         parserContext.m_suggestions = suffixesAdded;
     }
 };
 
-std::set<std::string> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentSet);
+std::set<Completion> generateMissingKeyCompletionSet(std::set<std::string> keysNeeded, std::map<std::string, leaf_data_> currentSet);
 
 struct createKeySuggestions_class {
     template <typename T, typename Iterator, typename Context>
@@ -609,10 +608,10 @@
         };
         std::copy_if(listInstances.begin(), listInstances.end(), std::inserter(filteredInstances, filteredInstances.end()), partialFitsComplete);
 
-        std::set<std::string> validValues;
+        std::set<Completion> validValues;
 
-        std::transform(filteredInstances.begin(), filteredInstances.end(), std::inserter(validValues, validValues.end()), [&parserContext] (const auto& instance) {
-            return leafDataToCompletion(instance.at(parserContext.m_tmpListKeyLeafPath.m_node.second));
+        std::transform(filteredInstances.begin(), filteredInstances.end(), std::inserter(validValues, validValues.end()), [&parserContext](const auto& instance) {
+            return Completion{leafDataToCompletion(instance.at(parserContext.m_tmpListKeyLeafPath.m_node.second))};
         });
 
         parserContext.m_suggestions = validValues;
@@ -629,16 +628,16 @@
         parserContext.m_completionIterator = begin;
         const auto& keysNeeded = schema.listKeys(parserContext.currentSchemaPath(), {parserContext.m_curModule, parserContext.m_tmpListName});
         if (generateMissingKeyCompletionSet(keysNeeded, parserContext.m_tmpListKeys).empty()) {
-            parserContext.m_suggestions = {"]/"};
+            parserContext.m_suggestions = {Completion{"]/"}};
         } else {
-            parserContext.m_suggestions = {"]["};
+            parserContext.m_suggestions = {Completion{"]["}};
         }
     }
 };
 
 struct commandNamesVisitor {
     template <typename T>
-    auto operator()(boost::type<T>)
+    std::string operator()(boost::type<T>)
     {
         return T::name;
     }
@@ -653,7 +652,7 @@
 
         parserContext.m_suggestions.clear();
         boost::mpl::for_each<CommandTypes, boost::type<boost::mpl::_>>([&parserContext](auto cmd) {
-            parserContext.m_suggestions.emplace(commandNamesVisitor()(cmd));
+            parserContext.m_suggestions.insert({commandNamesVisitor()(cmd)});
         });
     }
 };
@@ -671,7 +670,7 @@
 
 template<yang::LeafDataTypes TYPE>
 struct createSetSuggestions_class {
-    decltype(ParserContext::m_suggestions) getSuggestions(const ParserContext& ctx, const Schema& schema) const;
+    std::set<std::string> getSuggestions(const ParserContext& ctx, const Schema& schema) const;
 
     template <typename T, typename Iterator, typename Context>
     void on_success(Iterator const& begin, Iterator const&, T&, Context const& context)
@@ -683,7 +682,10 @@
         // overwrite some other completions.
         if (schema.leafType(parserContext.m_tmpListKeyLeafPath.m_location, parserContext.m_tmpListKeyLeafPath.m_node) == TYPE) {
             parserContext.m_completionIterator = begin;
-            parserContext.m_suggestions = getSuggestions(parserContext, schema);
+            auto suggestions = getSuggestions(parserContext, schema);
+            std::set<Completion> res;
+            std::transform(suggestions.begin(), suggestions.end(), std::inserter(res, res.end()), [](auto it) { return Completion{it}; });
+            parserContext.m_suggestions = res;
         }
     }
 };
diff --git a/src/completion.cpp b/src/completion.cpp
new file mode 100644
index 0000000..44286e9
--- /dev/null
+++ b/src/completion.cpp
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+#include <boost/algorithm/string/predicate.hpp>
+#include <tuple>
+#include "completion.hpp"
+
+bool Completion::operator<(const Completion& b) const
+{
+    return std::tie(this->m_value, this->m_suffix, this->m_whenToAdd) < std::tie(b.m_value, b.m_suffix, b.m_whenToAdd);
+}
+
+bool Completion::operator==(const Completion& b) const
+{
+    return std::tie(this->m_value, this->m_suffix, this->m_whenToAdd) == std::tie(b.m_value, b.m_suffix, b.m_whenToAdd);
+}
+
+std::set<Completion> filterByPrefix(const std::set<Completion>& set, const std::string_view prefix)
+{
+    std::set<Completion> filtered;
+    std::copy_if(set.begin(), set.end(), std::inserter(filtered, filtered.end()), [prefix](Completion it) { return boost::starts_with(it.m_value, prefix); });
+    return filtered;
+}
diff --git a/src/completion.hpp b/src/completion.hpp
new file mode 100644
index 0000000..a95dafc
--- /dev/null
+++ b/src/completion.hpp
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2020 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Václav Kubernát <kubernat@cesnet.cz>
+ *
+*/
+#include <set>
+#include <string>
+struct Completion {
+    enum class WhenToAdd {
+        Always,
+        IfFullMatch
+    };
+    bool operator<(const Completion& b) const;
+    bool operator==(const Completion& b) const;
+    std::string m_value;
+
+    /** A completion can have a suffix specified. This suffix is appended to the completion, if it's the only valid completion present in
+     * the ParserContext. For example, let's say there are two valid schema paths: "user/name", "user/nationality" and "user/city" and the
+     * user tries to complete this path: "user/n". The parser determines these completions: "name", "nationality", "city". The parser
+     * filters out "city", because it doesn't start with an "n", so valid completions are "name" and "nationality". Their common prefix is
+     * "na", so the input becomes "user/na". Next, the user changes the output to "user/natio", the completions will again be "name",
+     * "nationality", "city", but now, after filtering, only one single completion remains - "nationality". This is where m_suffix gets in
+     * play: since there is only one completion remaining, m_suffix can get added depending on the value of m_whenToAdd. It it's set to
+     * Always, the suffix gets appended always. That means, the actual completion would be "nationality" plus whatever m_suffix is set to.
+     * Otherwise (if m_whenToAdd is set to IfFullMatch), the suffix will only get added after the user input fully matches the completion.
+     * For example, if the user input is "user/natio", the completion becomes just "nationality", but if the user input is "user/nationality",
+     * the completion becomes "nationality" plus the suffix.
+     */
+    std::string m_suffix = "";
+    WhenToAdd m_whenToAdd = WhenToAdd::Always;
+};
+
+/** Returns a subset of the original set with only the strings starting with prefix */
+std::set<Completion> filterByPrefix(const std::set<Completion>& set, const std::string_view prefix);
diff --git a/src/parser.cpp b/src/parser.cpp
index ff262fc..85793a0 100644
--- a/src/parser.cpp
+++ b/src/parser.cpp
@@ -67,11 +67,18 @@
 
     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}, completionContext};
+    auto filtered = filterByPrefix(ctx.m_suggestions, std::string(completionIterator, line.end()));
+    if (filtered.size() == 1) {
+        auto suffix = filtered.begin()->m_whenToAdd == Completion::WhenToAdd::IfFullMatch
+                && filtered.begin()->m_value == std::string{completionIterator, line.end()}
+            ? filtered.begin()->m_suffix
+            : "";
+        return {{filtered.begin()->m_value + suffix}, completionContext};
     }
-    return {set, completionContext};
+
+    std::set<std::string> res;
+    std::transform(filtered.begin(), filtered.end(), std::inserter(res, res.end()), [](auto it) { return it.m_value; });
+    return {res, completionContext};
 }
 
 void Parser::changeNode(const dataPath_& name)
diff --git a/src/parser_context.hpp b/src/parser_context.hpp
index b9798fc..3ad0179 100644
--- a/src/parser_context.hpp
+++ b/src/parser_context.hpp
@@ -7,6 +7,7 @@
 */
 #pragma once
 
+#include "completion.hpp"
 #include "data_query.hpp"
 #include "schema.hpp"
 struct ParserContext {
@@ -33,13 +34,10 @@
     std::map<std::string, leaf_data_> m_tmpListKeys;
     bool m_errorHandled = false;
     bool m_completing = false;
-    std::set<std::string> m_suggestions;
+
+    std::set<Completion> m_suggestions;
     // Iterator pointing to where suggestions were created
     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)
-    std::string m_completionSuffix;
 
 private:
     boost::variant<dataPath_, schemaPath_> m_curPath;
diff --git a/src/utils.cpp b/src/utils.cpp
index 978e0a9..fcbe0d0 100644
--- a/src/utils.cpp
+++ b/src/utils.cpp
@@ -5,8 +5,8 @@
  * Written by Václav Kubernát <kubervac@fit.cvut.cz>
  *
 */
-#include <boost/algorithm/string/predicate.hpp>
 #include <sstream>
+#include "completion.hpp"
 #include "utils.hpp"
 
 std::string joinPaths(const std::string& prefix, const std::string& suffix)
@@ -104,15 +104,6 @@
     return fullNodeName(dataPathToSchemaPath(location), pair);
 }
 
-std::set<std::string> filterByPrefix(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); });
-    return filtered;
-}
-
 struct leafDataToStringVisitor : boost::static_visitor<std::string> {
     std::string operator()(const enum_& data) const
     {
diff --git a/src/utils.hpp b/src/utils.hpp
index bd959e3..f0f7ae5 100644
--- a/src/utils.hpp
+++ b/src/utils.hpp
@@ -20,6 +20,4 @@
 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 */
-std::set<std::string> filterByPrefix(const std::set<std::string>& set, const std::string_view prefix);
 std::string leafDataToString(const leaf_data_ value);