Branch data Line data Source code
1 : : // Copyright 2026 HPActor Contributors
2 : : // Licensed under the Apache License, Version 2.0
3 : :
4 : : #include <hpactor/cli/line_editor.hpp>
5 : : #include <hpactor/cli/command_node.hpp>
6 : : #include <hpactor/cli/lexer.hpp>
7 : : #include <hpactor/cli/token.hpp>
8 : :
9 : : extern "C" {
10 : : #include <linenoise.h>
11 : : }
12 : :
13 : : #include <cstdlib>
14 : : #include <cstring>
15 : :
16 : : namespace hpactor::cli {
17 : :
18 : : // Static singleton pointer — CliActor is the sole LineEditor user.
19 : : LineEditor* LineEditor::current_ = nullptr;
20 : :
21 : 6 : LineEditor::LineEditor(const LineEditorConfig& cfg, const CommandNode* root)
22 : 6 : : root_(root), config_(cfg) {
23 : 6 : current_ = this;
24 : 6 : linenoiseHistorySetMaxLen(static_cast<int>(cfg.history_max));
25 : 6 : if (!cfg.history_path.empty()) {
26 : 2 : linenoiseHistoryLoad(cfg.history_path.c_str());
27 : : }
28 : 6 : if (cfg.multiline) {
29 : 1 : linenoiseSetMultiLine(1);
30 : : }
31 : 6 : }
32 : :
33 : 6 : LineEditor::~LineEditor() {
34 : 6 : if (!config_.history_path.empty()) {
35 : 2 : linenoiseHistorySave(config_.history_path.c_str());
36 : : }
37 : 6 : current_ = nullptr;
38 : 6 : }
39 : :
40 : 0 : std::string LineEditor::readline(const std::string& prompt) {
41 : 0 : install_callbacks();
42 : 0 : char* line = linenoise(prompt.c_str());
43 : 0 : if (line == nullptr) return {};
44 : 0 : std::string result(line);
45 : 0 : linenoiseFree(line);
46 : 0 : return result;
47 : 0 : }
48 : :
49 : 2 : void LineEditor::add_history(const std::string& line) const {
50 : 2 : linenoiseHistoryAdd(line.c_str());
51 : 2 : if (!config_.history_path.empty()) {
52 : 1 : linenoiseHistorySave(config_.history_path.c_str());
53 : : }
54 : 2 : }
55 : :
56 : 1 : void LineEditor::load_history() const {
57 : 1 : if (!config_.history_path.empty()) {
58 : 0 : linenoiseHistoryLoad(config_.history_path.c_str());
59 : : }
60 : 1 : }
61 : :
62 : 1 : void LineEditor::save_history() const {
63 : 1 : if (!config_.history_path.empty()) {
64 : 0 : linenoiseHistorySave(config_.history_path.c_str());
65 : : }
66 : 1 : }
67 : :
68 : 0 : void LineEditor::install_callbacks() {
69 : 0 : if (callbacks_installed_) return;
70 : 0 : linenoiseSetCompletionCallback(on_completion);
71 : 0 : linenoiseSetHintsCallback(on_hints);
72 : 0 : linenoiseSetFreeHintsCallback(on_free_hints);
73 : 0 : callbacks_installed_ = true;
74 : : }
75 : :
76 : 0 : std::vector<std::string> LineEditor::tokenize_partial(const std::string& buf) {
77 : 0 : auto tokens = Lexer::tokenize(buf);
78 : 0 : std::vector<std::string> words;
79 : 0 : for (auto& t : tokens) {
80 : 0 : if (t.type == TokenType::Eof) continue;
81 : 0 : words.push_back(std::move(t.value));
82 : : }
83 : 0 : return words;
84 : 0 : }
85 : :
86 : 0 : void LineEditor::on_completion(const char* buf,
87 : : linenoiseCompletions* lc) {
88 : 0 : auto* self = current_;
89 : 0 : if (!self || !self->root_) return;
90 : :
91 : 0 : auto words = tokenize_partial(buf);
92 : 0 : size_t len = strlen(buf);
93 : 0 : bool ends_with_space = (len > 0 && buf[len - 1] == ' ');
94 : :
95 : : // consumed tracks tokens already matched (exact or prefix-expanded).
96 : : // linenoise replaces the ENTIRE buffer with the completion string,
97 : : // so every completion must include the full prefix.
98 : 0 : const CommandNode* node = self->root_;
99 : 0 : std::vector<std::string> consumed;
100 : 0 : size_t i = 0;
101 : 0 : if (i < words.size() && words[i] == "/") ++i;
102 : :
103 : 0 : size_t words_to_consume = ends_with_space ? words.size() : (words.size() > 0 ? words.size() - 1 : 0);
104 : 0 : for (; i < words_to_consume; ++i) {
105 : 0 : std::string param;
106 : 0 : auto* child = node->find_child(words[i], param);
107 : 0 : if (!child) child = node->find_child_prefix(words[i]);
108 : 0 : if (!child) {
109 : 0 : std::vector<std::string> matches;
110 : 0 : node->collect_completions(words[i], matches);
111 : 0 : std::string prefix = "/";
112 : 0 : for (auto& w : consumed) prefix += w + " ";
113 : 0 : for (auto& m : matches) linenoiseAddCompletion(lc, (prefix + m).c_str());
114 : 0 : return;
115 : 0 : }
116 : : // Use the matched keyword for prefix matches (e.g. "act"→"actor"),
117 : : // user input for parameter tokens (e.g. "0x123"→"<id>").
118 : 0 : consumed.push_back(child->is_parameter ? words[i] : child->keyword);
119 : 0 : node = child;
120 : 0 : }
121 : :
122 : 0 : std::string partial;
123 : 0 : if (!ends_with_space && !words.empty()) partial = words.back();
124 : :
125 : : // Exact keyword match: advance into the node so sub-commands appear.
126 : : // Include the matched keyword in the prefix.
127 : 0 : if (!partial.empty()) {
128 : 0 : for (auto& child : node->children) {
129 : 0 : if (!child->is_parameter && child->keyword == partial) {
130 : 0 : consumed.push_back(partial);
131 : 0 : node = child.get();
132 : 0 : partial.clear();
133 : 0 : break;
134 : : }
135 : : }
136 : : }
137 : :
138 : : // Build full prefix: leading "/" + consumed tokens with trailing space.
139 : 0 : std::string prefix = "/";
140 : 0 : for (auto& w : consumed) prefix += w + " ";
141 : :
142 : 0 : std::vector<std::string> matches;
143 : 0 : node->collect_completions(partial, matches);
144 : 0 : for (auto& m : matches) linenoiseAddCompletion(lc, (prefix + m).c_str());
145 : 0 : }
146 : :
147 : 0 : char* LineEditor::on_hints(const char* buf,
148 : : int* color,
149 : : int* bold) {
150 : 0 : auto* self = current_;
151 : 0 : if (!self || !self->root_) return nullptr;
152 : :
153 : 0 : auto words = tokenize_partial(buf);
154 : 0 : size_t len = strlen(buf);
155 : 0 : if (len == 0) return nullptr;
156 : 0 : bool ends_with_space = (buf[len - 1] == ' ');
157 : :
158 : 0 : const CommandNode* node = self->root_;
159 : 0 : size_t i = 0;
160 : : // Skip leading "/"
161 : 0 : if (i < words.size() && words[i] == "/") ++i;
162 : :
163 : : // Consume fully-typed tokens, using prefix match as fallback so
164 : : // partial commands like "/act" show hints for "/actor".
165 : 0 : size_t words_to_consume = ends_with_space ? words.size() : (words.size() > 0 ? words.size() - 1 : 0);
166 : 0 : for (; i < words_to_consume; ++i) {
167 : 0 : std::string param;
168 : 0 : auto* child = node->find_child(words[i], param);
169 : 0 : if (!child) {
170 : 0 : child = node->find_child_prefix(words[i]);
171 : 0 : if (!child) return nullptr; // no match, no hint possible
172 : : }
173 : 0 : node = child;
174 : 0 : }
175 : :
176 : : // Determine partial token
177 : 0 : std::string partial;
178 : 0 : if (!ends_with_space && !words.empty()) {
179 : 0 : partial = words.back();
180 : : }
181 : :
182 : : // If the partial exactly matches a child keyword, the user typed a
183 : : // complete token — advance to show hints from the next level.
184 : 0 : if (!partial.empty()) {
185 : 0 : for (auto& child : node->children) {
186 : 0 : if (!child->is_parameter && child->keyword == partial) {
187 : 0 : node = child.get();
188 : 0 : partial.clear();
189 : 0 : break;
190 : : }
191 : : }
192 : : }
193 : :
194 : : // Find first non-parameter child whose keyword starts with partial
195 : 0 : for (auto& child : node->children) {
196 : 0 : if (child->is_parameter) continue;
197 : 0 : if (partial.empty() || child->keyword.starts_with(partial)) {
198 : 0 : std::string hint_text = child->keyword.substr(partial.size());
199 : 0 : if (color) *color = 90; // bright black = gray
200 : 0 : if (bold) *bold = 0;
201 : 0 : return strdup(hint_text.c_str());
202 : 0 : }
203 : : }
204 : 0 : return nullptr;
205 : 0 : }
206 : :
207 : 0 : void LineEditor::on_free_hints(void* hint) {
208 : : // NOLINTNEXTLINE(cppcoreguidelines-no-malloc) — C API callback, must free strdup result
209 : 0 : free(hint);
210 : 0 : }
211 : :
212 : : } // namespace hpactor::cli
|