From 249ee5f91d60c122bfb0f2e1071fb46716850085 Mon Sep 17 00:00:00 2001 From: Hevake Lee Date: Mon, 11 Aug 2025 14:19:22 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(terminal):1.12.21,=20=E5=88=9D?= =?UTF-8?q?=E6=AD=A5=E5=AE=9E=E7=8E=B0terminal=E7=9A=84TAB=E9=94=AE?= =?UTF-8?q?=E8=A1=A5=E5=85=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/terminal/impl/terminal_key_events.cpp | 108 +++++++++++++++++- version.mk | 2 +- 2 files changed, 107 insertions(+), 3 deletions(-) diff --git a/modules/terminal/impl/terminal_key_events.cpp b/modules/terminal/impl/terminal_key_events.cpp index dc49d1c..7b549a0 100644 --- a/modules/terminal/impl/terminal_key_events.cpp +++ b/modules/terminal/impl/terminal_key_events.cpp @@ -115,9 +115,113 @@ void Terminal::Impl::onDeleteKey(SessionContext *s) } } -void Terminal::Impl::onTabKey(SessionContext *) +void Terminal::Impl::onTabKey(SessionContext *s) { - //!TODO: 实现补全功能 + if (s->curr_input.empty()) + return; + + // 找到最后一个空格后的部分作为待补全的token + size_t last_space = s->curr_input.rfind(' '); + size_t token_start = (last_space == string::npos) ? 0 : last_space + 1; + string token = s->curr_input.substr(token_start); + + if (token.empty()) + return; + + // 分离路径和文件名 + size_t last_slash = token.rfind('/'); + string path_part; + string prefix; + + if (last_slash != string::npos) { + path_part = token.substr(0, last_slash); + prefix = token.substr(last_slash + 1); + } else { + path_part = "."; + prefix = token; + } + + // 查找路径对应的节点 + NodeToken node; + string full_path; + + // 构建完整路径 + if (!s->path.empty()) { + for (const auto& p : s->path) { + full_path += "/" + p.first; + } + } else { + full_path = "/"; + } + + if (!path_part.empty() && path_part != ".") { + if (path_part[0] == '/') { + full_path = path_part; + } else { + full_path += "/" + path_part; + } + } + + node = findNode(full_path); + if (node.isNull()) + return; + + // 获取所有匹配的子节点 + vector matches; + Node* base_node = nodes_.at(node); + if (base_node == nullptr) + return; + + if (auto dir_node = dynamic_cast(base_node)) { + vector children; + dir_node->children(children); + + for (const auto& child : children) { + if (child.name.find(prefix) == 0) { // 前缀匹配 + matches.push_back(child.name); + } + } + } + + if (matches.empty()) + return; + + // 如果只有一个匹配,直接补全 + if (matches.size() == 1) { + string completion = matches[0].substr(prefix.length()); + + // 检查是否为目录,如果是则添加/ + NodeToken child_token = findNode(full_path + "/" + matches[0]); + if (!child_token.isNull()) { + Node* child_node = nodes_.at(child_token); + if (child_node && dynamic_cast(child_node)) { + completion += "/"; + } + } + + // 在光标位置插入补全内容 + s->curr_input.insert(s->cursor, completion); + + if (s->options & kEnableEcho) { + s->wp_conn->send(s->token, completion); + } + + s->cursor += completion.length(); + } else { + // 多个匹配,显示所有可能 + stringstream ss; + ss << "\r\n"; + for (const auto& match : matches) { + ss << match << " "; + } + ss << "\r\n"; + + if (s->options & kEnableEcho) { + s->wp_conn->send(s->token, ss.str()); + printPrompt(s); + s->wp_conn->send(s->token, s->curr_input); + } + } } namespace { diff --git a/version.mk b/version.mk index 3396e9c..5d7c226 100644 --- a/version.mk +++ b/version.mk @@ -21,4 +21,4 @@ # TBOX版本号 TBOX_VERSION_MAJOR := 1 TBOX_VERSION_MINOR := 12 -TBOX_VERSION_REVISION := 20 +TBOX_VERSION_REVISION := 21 -- Gitee From ae7c54918ac0047d11e28ae8224bcb313d3a9dfa Mon Sep 17 00:00:00 2001 From: Hevake Lee Date: Tue, 12 Aug 2025 00:25:18 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat(terminal):1.12.22,=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E7=BB=88=E7=AB=AF=E5=91=BD=E4=BB=A4=E7=9A=84=E8=A1=A5?= =?UTF-8?q?=E5=85=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/terminal/impl/session_context.h | 2 +- modules/terminal/impl/terminal.h | 6 +- modules/terminal/impl/terminal_commands.cpp | 86 +++--- modules/terminal/impl/terminal_key_events.cpp | 250 ++++++++++-------- modules/terminal/impl/terminal_nodes.cpp | 28 +- modules/terminal/terminal_nodes.h | 2 +- modules/util/string.cpp | 20 ++ modules/util/string.h | 9 + modules/util/string_test.cpp | 15 ++ version.mk | 2 +- 10 files changed, 259 insertions(+), 161 deletions(-) diff --git a/modules/terminal/impl/session_context.h b/modules/terminal/impl/session_context.h index 186c49b..8a94ed8 100644 --- a/modules/terminal/impl/session_context.h +++ b/modules/terminal/impl/session_context.h @@ -35,7 +35,7 @@ struct SessionContext { uint32_t options = 0; std::string curr_input; - size_t cursor = 0; + size_t cursor_pos = 0; Path path; //! 当前路径 std::deque history; //! 历史命令 diff --git a/modules/terminal/impl/terminal.h b/modules/terminal/impl/terminal.h index 445b9a3..450b47a 100644 --- a/modules/terminal/impl/terminal.h +++ b/modules/terminal/impl/terminal.h @@ -54,7 +54,7 @@ class Terminal::Impl { NodeToken createDirNode(const std::string &help); bool deleteNode(NodeToken node_token); NodeToken rootNode() const { return root_token_; } - NodeToken findNode(const std::string &path) const; + NodeToken findNode(const std::string &path_str) const; bool mountNode(const NodeToken &parent, const NodeToken &child, const std::string &name); bool umountNode(const NodeToken &parent, const std::string &name); @@ -90,7 +90,9 @@ class Terminal::Impl { bool executeRunHistoryCmd(SessionContext *s, const Args &args); void executeUserCmd(SessionContext *s, const Args &args); - bool findNode(const std::string &path, Path &node_path) const; + bool findNode(const std::string &path_str, Path &node_path) const; + //! 从指定会话的当前路径寻找path_str对像的结点;如果不指定,则从根目录寻找 + NodeToken findNode(const std::string &path_str, SessionContext *s) const; private: event::Loop *wp_loop_ = nullptr; diff --git a/modules/terminal/impl/terminal_commands.cpp b/modules/terminal/impl/terminal_commands.cpp index 748fcab..56e8e0e 100644 --- a/modules/terminal/impl/terminal_commands.cpp +++ b/modules/terminal/impl/terminal_commands.cpp @@ -94,7 +94,7 @@ void Terminal::Impl::executeCdCmd(SessionContext *s, const Args &args) if (args.size() >= 2) path_str = args[1]; - stringstream ss; + ostringstream oss; auto node_path = s->path; bool is_found = findNode(path_str, node_path); @@ -102,17 +102,17 @@ void Terminal::Impl::executeCdCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << "Error: '" << path_str << "' node has been deleted." << "\r\n"; + oss << "Error: '" << path_str << "' node has been deleted." << "\r\n"; } else if (top_node->type() == NodeType::kDir) { s->path = node_path; } else { - ss << "Error: '" << path_str << "' not directory." << "\r\n"; + oss << "Error: '" << path_str << "' not directory." << "\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeHelpCmd(SessionContext *s, const Args &args) @@ -122,7 +122,7 @@ void Terminal::Impl::executeHelpCmd(SessionContext *s, const Args &args) return; } - stringstream ss; + ostringstream oss; string path_str = args[1]; auto node_path = s->path; @@ -131,15 +131,15 @@ void Terminal::Impl::executeHelpCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node != nullptr) { - ss << top_node->help() << "\r\n"; + oss << top_node->help() << "\r\n"; } else { - ss << "Error: '" << path_str << "' node has been deleted.\r\n"; + oss << "Error: '" << path_str << "' node has been deleted.\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeLsCmd(SessionContext *s, const Args &args) @@ -148,7 +148,7 @@ void Terminal::Impl::executeLsCmd(SessionContext *s, const Args &args) if (args.size() >= 2) path_str = args[1]; - stringstream ss; + ostringstream oss; auto node_path = s->path; bool is_found = findNode(path_str, node_path); @@ -156,40 +156,40 @@ void Terminal::Impl::executeLsCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << "Error: '" << path_str << "' node has been deleted.\r\n"; + oss << "Error: '" << path_str << "' node has been deleted.\r\n"; } else if (top_node->type() == NodeType::kDir) { auto top_dir_node = static_cast(top_node); vector node_info_vec; top_dir_node->children(node_info_vec); for (auto item : node_info_vec) { - ss << "- " << item.name; + oss << "- " << item.name; auto node = nodes_.at(item.token); if (node == nullptr) { - ss << "(X)"; + oss << "(X)"; } else if (node->type() == NodeType::kDir) - ss << '/'; - ss << "\r\n"; + oss << '/'; + oss << "\r\n"; } - ss << "\r\n"; + oss << "\r\n"; } else { - ss << path_str << " is function" << ".\r\n"; + oss << path_str << " is function" << ".\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeHistoryCmd(SessionContext *s, const Args &) { - stringstream ss; + ostringstream oss; for (size_t i = 0; i < s->history.size(); ++i) { const auto &cmd = s->history.at(i); - ss << setw(2) << i << " " << cmd << "\r\n"; + oss << setw(2) << i << " " << cmd << "\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeExitCmd(SessionContext *s, const Args &) @@ -212,7 +212,7 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) if (args.size() >= 2) path_str = args[1]; - stringstream ss; + ostringstream oss; auto node_path = s->path; bool is_found = findNode(path_str, node_path); @@ -221,7 +221,7 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << node_path.back().first << " node has been deleted.\r\n"; + oss << node_path.back().first << " node has been deleted.\r\n"; } else if (top_node->type() == NodeType::kDir) { vector> node_token_stack; //!< 遍历栈 string indent_str; //!< 缩进字串 @@ -257,15 +257,15 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) const char *curr_indent_str = is_last_node ? "`-- " : "|-- "; auto &curr_node_info = last_level.front(); - ss << indent_str << curr_indent_str << curr_node_info.name; + oss << indent_str << curr_indent_str << curr_node_info.name; auto curr_node = nodes_.at(curr_node_info.token); if (curr_node == nullptr) { //! 如果已被删除了的结点,显示(X) - ss << "(X)\r\n"; + oss << "(X)\r\n"; } else if (curr_node->type() == NodeType::kFunc) { //! 如果是Func,就打印一下名称就可以了 - ss << "\r\n"; + oss << "\r\n"; } else if (curr_node->type() == NodeType::kDir) { //! 如果是Dir,则需要再深入地遍历其子Node //! 首先需要查重,防止循环路径引起的死循环 @@ -276,9 +276,9 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) } if (is_repeat) - ss << "(R)"; + oss << "(R)"; - ss << "\r\n"; + oss << "\r\n"; if (!is_repeat) { //! 找出该Dir下所有的子Node。 @@ -307,26 +307,26 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) } } } else { - ss << node_path.back().first << " is a function.\r\n"; + oss << node_path.back().first << " is a function.\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executePwdCmd(SessionContext *s, const Args &) { - stringstream ss; - ss << '/'; + ostringstream oss; + oss << '/'; for (size_t i = 0; i < s->path.size(); ++i) { - ss << s->path.at(i).first; + oss << s->path.at(i).first; if ((i + 1) != s->path.size()) - ss << '/'; + oss << '/'; } - ss << "\r\n"; - s->wp_conn->send(s->token, ss.str()); + oss << "\r\n"; + s->wp_conn->send(s->token, oss.str()); } bool Terminal::Impl::executeRunHistoryCmd(SessionContext *s, const Args &args) @@ -368,7 +368,7 @@ bool Terminal::Impl::executeRunHistoryCmd(SessionContext *s, const Args &args) void Terminal::Impl::executeUserCmd(SessionContext *s, const Args &args) { - stringstream ss; + ostringstream oss; const auto &cmd = args[0]; auto node_path = s->path; @@ -378,7 +378,7 @@ void Terminal::Impl::executeUserCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << "Error: '" << cmd << "' node has been deleted.\r\n"; + oss << "Error: '" << cmd << "' node has been deleted.\r\n"; } else if (top_node->type() == NodeType::kFunc) { auto top_func_node = static_cast(top_node); Session session(s->wp_conn, s->token); @@ -387,10 +387,10 @@ void Terminal::Impl::executeUserCmd(SessionContext *s, const Args &args) s->path = node_path; } } else { - ss << "Error: '" << cmd << "' not found.\r\n"; + oss << "Error: '" << cmd << "' not found.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } } diff --git a/modules/terminal/impl/terminal_key_events.cpp b/modules/terminal/impl/terminal_key_events.cpp index 7b549a0..3d00f6e 100644 --- a/modules/terminal/impl/terminal_key_events.cpp +++ b/modules/terminal/impl/terminal_key_events.cpp @@ -22,6 +22,7 @@ #include #include +#include #include "session_context.h" #include "dir_node.h" @@ -37,24 +38,25 @@ namespace { const string MOVE_LEFT_KEY("\033[D"); const string MOVE_RIGHT_KEY("\033[C"); const size_t HISTORY_MAX_SIZE(20); + const char * SPACE_CHARS = " \t"; } void Terminal::Impl::onChar(SessionContext *s, char ch) { - if (s->cursor == s->curr_input.size()) + if (s->cursor_pos == s->curr_input.size()) s->curr_input.push_back(ch); else - s->curr_input.insert(s->cursor, 1, ch); - s->cursor++; + s->curr_input.insert(s->cursor_pos, 1, ch); + s->cursor_pos++; if (s->options & kEnableEcho) { s->wp_conn->send(s->token, ch); - stringstream ss; - ss << s->curr_input.substr(s->cursor) - << string((s->curr_input.size() - s->cursor), '\b'); + ostringstream oss; + oss << s->curr_input.substr(s->cursor_pos) + << string((s->curr_input.size() - s->cursor_pos), '\b'); - auto refresh_str = ss.str(); + auto refresh_str = oss.str(); if (!refresh_str.empty()) s->wp_conn->send(s->token, refresh_str); } @@ -74,152 +76,192 @@ void Terminal::Impl::onEnterKey(SessionContext *s) printPrompt(s); s->curr_input.clear(); - s->cursor = 0; + s->cursor_pos = 0; s->history_index = 0; } void Terminal::Impl::onBackspaceKey(SessionContext *s) { - if (s->cursor == 0) + if (s->cursor_pos == 0) return; - if (s->cursor == s->curr_input.size()) + if (s->cursor_pos == s->curr_input.size()) s->curr_input.pop_back(); else - s->curr_input.erase((s->cursor - 1), 1); + s->curr_input.erase((s->cursor_pos - 1), 1); - s->cursor--; + s->cursor_pos--; if (s->options & kEnableEcho) { - stringstream ss; - ss << '\b' << s->curr_input.substr(s->cursor) << ' ' - << string((s->curr_input.size() - s->cursor + 1), '\b'); + ostringstream oss; + oss << '\b' << s->curr_input.substr(s->cursor_pos) << ' ' + << string((s->curr_input.size() - s->cursor_pos + 1), '\b'); - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } } void Terminal::Impl::onDeleteKey(SessionContext *s) { - if (s->cursor >= s->curr_input.size()) + if (s->cursor_pos >= s->curr_input.size()) return; - s->curr_input.erase((s->cursor), 1); + s->curr_input.erase((s->cursor_pos), 1); if (s->options & kEnableEcho) { - stringstream ss; - ss << s->curr_input.substr(s->cursor) << ' ' - << string((s->curr_input.size() - s->cursor + 1), '\b'); + ostringstream oss; + oss << s->curr_input.substr(s->cursor_pos) << ' ' + << string((s->curr_input.size() - s->cursor_pos + 1), '\b'); - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } } +/** + * 仅实现对Node的补全,不实现参数的补全 + */ void Terminal::Impl::onTabKey(SessionContext *s) { - if (s->curr_input.empty()) + //! 如果当前用户没有任何输入,则不进行补全 + if (s->curr_input.empty() || s->cursor_pos == 0) return; - // 找到最后一个空格后的部分作为待补全的token - size_t last_space = s->curr_input.rfind(' '); - size_t token_start = (last_space == string::npos) ? 0 : last_space + 1; - string token = s->curr_input.substr(token_start); + //! 找出当前光标下正在输入的命令的起始位置 + size_t start_pos = 0; + { + /** + * 针对单行多指令的情况:"set_a 12; get_ " + * ^ + * 要补全的是get_,而不是set_a + */ + + //! 尝试从光标所在处往前寻找分号。如果是找到,则将start_pos修正为分号后一位 + auto semicolon_pos = s->curr_input.find_last_of(';', s->cursor_pos - 1); + if (semicolon_pos != std::string::npos) + start_pos = semicolon_pos + 1; + + //! 略过左边空格,找到有效字符起始位置 + start_pos = s->curr_input.find_first_not_of(SPACE_CHARS, start_pos); + + /** + * 如果没有找到或找到的位置在光标之后,说明无效,则退出 + * 如:" set_a 12" + * ^ + */ + if (start_pos == std::string::npos || start_pos >= s->cursor_pos) + return; + } - if (token.empty()) - return; + const auto cmd_str = s->curr_input.substr(start_pos, s->cursor_pos - start_pos); - // 分离路径和文件名 - size_t last_slash = token.rfind('/'); - string path_part; - string prefix; + if (cmd_str.empty()) + return; - if (last_slash != string::npos) { - path_part = token.substr(0, last_slash); - prefix = token.substr(last_slash + 1); - } else { - path_part = "."; - prefix = token; + /** + * 检查从start_pos到s->cursor_pos之间有没有空格 + * 如果有则说明当前用户输入的是参数,不是命令,不进行补全 + * 如:"set_value 12" + * ^ + */ + { + auto space_pos = cmd_str.find_first_of(SPACE_CHARS); + if (space_pos != std::string::npos) + return; } - // 查找路径对应的节点 - NodeToken node; - string full_path; + //! 分离路径和节点名 + size_t last_slash_pos = cmd_str.rfind('/'); + string dir_path; + string prefix; - // 构建完整路径 - if (!s->path.empty()) { - for (const auto& p : s->path) { - full_path += "/" + p.first; - } + if (last_slash_pos != string::npos) { + dir_path = cmd_str.substr(0, last_slash_pos); + prefix = cmd_str.substr(last_slash_pos + 1); } else { - full_path = "/"; - } - - if (!path_part.empty() && path_part != ".") { - if (path_part[0] == '/') { - full_path = path_part; - } else { - full_path += "/" + path_part; - } + dir_path = "."; + prefix = cmd_str; } - node = findNode(full_path); - if (node.isNull()) + //! 查找路径对应的节点 + NodeToken dir_token = findNode(dir_path, s); + if (dir_token.isNull()) return; - // 获取所有匹配的子节点 - vector matches; - Node* base_node = nodes_.at(node); + //! 获取所有匹配的子节点对象 + Node* base_node = nodes_.at(dir_token); if (base_node == nullptr) return; + vector matched_node_name_vec; if (auto dir_node = dynamic_cast(base_node)) { - vector children; - dir_node->children(children); + vector children_info_vec; + dir_node->children(children_info_vec); - for (const auto& child : children) { - if (child.name.find(prefix) == 0) { // 前缀匹配 - matches.push_back(child.name); - } + //! 遍历dir_node下有所有子结点,找出匹配得上的,加入到matched_node_name_vec中 + for (const auto& child_info : children_info_vec) { + if (util::string::IsStartWith(child_info.name, prefix)) + matched_node_name_vec.push_back(child_info.name); } } - if (matches.empty()) + //! 如果没有找到匹配项,直接结束 + if (matched_node_name_vec.empty()) return; - // 如果只有一个匹配,直接补全 - if (matches.size() == 1) { - string completion = matches[0].substr(prefix.length()); + string completion; //! 补全内容 + + //! 如果只有一个匹配,直接补全 + if (matched_node_name_vec.size() == 1) { + const auto &match_node_name = matched_node_name_vec.back(); + completion = match_node_name.substr(prefix.length()); //! 需要补全的字串 + + //! 如果不需要补全,则结束 + if (completion.empty()) + return; - // 检查是否为目录,如果是则添加/ - NodeToken child_token = findNode(full_path + "/" + matches[0]); + //! 检查是否为目录,如果是则添加/ + NodeToken child_token = findNode(dir_path + "/" + match_node_name, s); if (!child_token.isNull()) { Node* child_node = nodes_.at(child_token); - if (child_node && dynamic_cast(child_node)) { - completion += "/"; + if (child_node && child_node->type() == NodeType::kDir) { + completion.push_back('/'); } } - // 在光标位置插入补全内容 - s->curr_input.insert(s->cursor, completion); + } else { + //! 多个匹配,显示所有可能 + ostringstream oss; + oss << "\r\n"; + for (const auto& match : matched_node_name_vec) + oss << match << "\t"; + oss << "\r\n"; if (s->options & kEnableEcho) { - s->wp_conn->send(s->token, completion); + s->wp_conn->send(s->token, oss.str()); + printPrompt(s); + s->wp_conn->send(s->token, s->curr_input); } - s->cursor += completion.length(); - } else { - // 多个匹配,显示所有可能 - stringstream ss; - ss << "\r\n"; - for (const auto& match : matches) { - ss << match << " "; - } - ss << "\r\n"; + //! 找出最大共同字串,设置补全内容 + std::string common_prefix = util::string::ExtractCommonPrefix(matched_node_name_vec); + completion = common_prefix.substr(prefix.length()); //! 需要补全的字串 + } + + //! 如果需要补全,则进行补全 + if (!completion.empty()) { + s->curr_input.insert(s->cursor_pos, completion); + s->cursor_pos += completion.length(); if (s->options & kEnableEcho) { - s->wp_conn->send(s->token, ss.str()); - printPrompt(s); - s->wp_conn->send(s->token, s->curr_input); + s->wp_conn->send(s->token, completion); + + ostringstream oss; + oss << s->curr_input.substr(s->cursor_pos) + << string((s->curr_input.size() - s->cursor_pos), '\b'); + + auto refresh_str = oss.str(); + if (!refresh_str.empty()) + s->wp_conn->send(s->token, refresh_str); } } } @@ -227,12 +269,12 @@ void Terminal::Impl::onTabKey(SessionContext *s) namespace { void CleanupInput(SessionContext *s) { - while (s->cursor < s->curr_input.size()) { + while (s->cursor_pos < s->curr_input.size()) { s->wp_conn->send(s->token, MOVE_RIGHT_KEY); - s->cursor++; + s->cursor_pos++; } - while (s->cursor--) + while (s->cursor_pos--) s->wp_conn->send(s->token, "\b \b"); } } @@ -246,7 +288,7 @@ void Terminal::Impl::onMoveUpKey(SessionContext *s) s->history_index++; s->curr_input = s->history[s->history.size() - s->history_index]; - s->cursor = s->curr_input.size(); + s->cursor_pos = s->curr_input.size(); s->wp_conn->send(s->token, s->curr_input); } @@ -261,10 +303,10 @@ void Terminal::Impl::onMoveDownKey(SessionContext *s) s->history_index--; if (s->history_index > 0) { s->curr_input = s->history[s->history.size() - s->history_index]; - s->cursor = s->curr_input.size(); + s->cursor_pos = s->curr_input.size(); } else { s->curr_input.clear(); - s->cursor = 0; + s->cursor_pos = 0; } s->wp_conn->send(s->token, s->curr_input); @@ -272,35 +314,35 @@ void Terminal::Impl::onMoveDownKey(SessionContext *s) void Terminal::Impl::onMoveLeftKey(SessionContext *s) { - if (s->cursor == 0) + if (s->cursor_pos == 0) return; - s->cursor--; + s->cursor_pos--; s->wp_conn->send(s->token, MOVE_LEFT_KEY); } void Terminal::Impl::onMoveRightKey(SessionContext *s) { - if (s->cursor >= s->curr_input.size()) + if (s->cursor_pos >= s->curr_input.size()) return; - s->cursor++; + s->cursor_pos++; s->wp_conn->send(s->token, MOVE_RIGHT_KEY); } void Terminal::Impl::onHomeKey(SessionContext *s) { - while (s->cursor != 0) { + while (s->cursor_pos != 0) { s->wp_conn->send(s->token, MOVE_LEFT_KEY); - s->cursor--; + s->cursor_pos--; } } void Terminal::Impl::onEndKey(SessionContext *s) { - while (s->cursor < s->curr_input.size()) { + while (s->cursor_pos < s->curr_input.size()) { s->wp_conn->send(s->token, MOVE_RIGHT_KEY); - s->cursor++; + s->cursor_pos++; } } diff --git a/modules/terminal/impl/terminal_nodes.cpp b/modules/terminal/impl/terminal_nodes.cpp index 03fb807..67a6fcc 100644 --- a/modules/terminal/impl/terminal_nodes.cpp +++ b/modules/terminal/impl/terminal_nodes.cpp @@ -54,17 +54,10 @@ bool Terminal::Impl::deleteNode(NodeToken node_token) } } +//! 从根结点开始寻找 NodeToken Terminal::Impl::findNode(const string &path_str) const { - Path node_path; - if (findNode(path_str, node_path)) { - if (node_path.empty()) - return root_token_; - else - return node_path.back().second; - } else { - return NodeToken(); - } + return findNode(path_str, nullptr); } bool Terminal::Impl::mountNode(const NodeToken &parent, const NodeToken &child, const string &name) @@ -146,5 +139,22 @@ bool Terminal::Impl::findNode(const string &path_str, Path &node_path) const return true; } +NodeToken Terminal::Impl::findNode(const std::string &path_str, SessionContext *s) const +{ + Path node_path; + + if (s != nullptr) + node_path = s->path; + + if (findNode(path_str, node_path)) { + if (node_path.empty()) + return root_token_; + else + return node_path.back().second; + } else { + return NodeToken(); + } +} + } } diff --git a/modules/terminal/terminal_nodes.h b/modules/terminal/terminal_nodes.h index 1516d93..d2ebcd5 100644 --- a/modules/terminal/terminal_nodes.h +++ b/modules/terminal/terminal_nodes.h @@ -41,7 +41,7 @@ class TerminalNodes { //! 获取根结点 virtual NodeToken rootNode() const = 0; //! 根据路径查找结点 - virtual NodeToken findNode(const std::string &path) const = 0; + virtual NodeToken findNode(const std::string &path_str) const = 0; //! 将子指定结点挂载到指定父目录结点,并指定名称 virtual bool mountNode(const NodeToken &parent, const NodeToken &child, const std::string &name) = 0; diff --git a/modules/util/string.cpp b/modules/util/string.cpp index c662ea2..0dece2b 100644 --- a/modules/util/string.cpp +++ b/modules/util/string.cpp @@ -289,6 +289,26 @@ bool IsEndWith(const std::string &origin_str, const std::string &text) return origin_str.find(text, (origin_str.length() - text.length())) != std::string::npos; } +std::string ExtractCommonPrefix(const std::vector &str_vec) +{ + std::string common_prefix; + auto &first_str = str_vec.front(); + //! 从第一个字串中提取每一个字符,与其它字串对应位置的字符进行比对 + //! 全对应得上,则为公共字符 + for (size_t i = 0; i < first_str.size(); ++i) { + auto ch = first_str[i]; + for (size_t j = 1; j < str_vec.size(); ++j) { + auto &test_str = str_vec[j]; + //! 如果检测字串不够长或者对应不上,则终止 + if (test_str.size() == i || test_str[i] != ch) + return common_prefix; + } + common_prefix.push_back(ch); + } + + return common_prefix; +} + } } } diff --git a/modules/util/string.h b/modules/util/string.h index 6e0eae5..1b59317 100644 --- a/modules/util/string.h +++ b/modules/util/string.h @@ -172,6 +172,15 @@ bool IsStartWith(const std::string &origin_str, const std::string &text); */ bool IsEndWith(const std::string &origin_str, const std::string &text); +/** + * \brief 提取多个字串的共同前缀 + * + * \param str_vec 字串数组 + * + * \return 共同前缀字串 + */ +std::string ExtractCommonPrefix(const std::vector &str_vec); + } } } diff --git a/modules/util/string_test.cpp b/modules/util/string_test.cpp index 96f16a6..1910621 100644 --- a/modules/util/string_test.cpp +++ b/modules/util/string_test.cpp @@ -296,3 +296,18 @@ TEST(string, IsEndWith) { EXPECT_FALSE(IsEndWith("abc", "abcd")); } +TEST(string, ExtractCommonPrefix) { + { + std::vector str_vec = {"abc123", "abc113", "abd123"}; + EXPECT_EQ(ExtractCommonPrefix(str_vec), "ab"); + } + { + std::vector str_vec = {"a", "abc113"}; + EXPECT_EQ(ExtractCommonPrefix(str_vec), "a"); + } + { + std::vector str_vec = {"abc123", "abc1"}; + EXPECT_EQ(ExtractCommonPrefix(str_vec), "abc1"); + } +} + diff --git a/version.mk b/version.mk index 5d7c226..3b9045e 100644 --- a/version.mk +++ b/version.mk @@ -21,4 +21,4 @@ # TBOX版本号 TBOX_VERSION_MAJOR := 1 TBOX_VERSION_MINOR := 12 -TBOX_VERSION_REVISION := 21 +TBOX_VERSION_REVISION := 22 -- Gitee