1 #include "commands.hh"
2
3 #include "buffer.hh"
4 #include "buffer_manager.hh"
5 #include "buffer_utils.hh"
6 #include "client.hh"
7 #include "client_manager.hh"
8 #include "command_manager.hh"
9 #include "completion.hh"
10 #include "context.hh"
11 #include "event_manager.hh"
12 #include "face_registry.hh"
13 #include "file.hh"
14 #include "hash_map.hh"
15 #include "highlighter.hh"
16 #include "highlighters.hh"
17 #include "insert_completer.hh"
18 #include "normal.hh"
19 #include "option_manager.hh"
20 #include "option_types.hh"
21 #include "parameters_parser.hh"
22 #include "ranges.hh"
23 #include "ranked_match.hh"
24 #include "regex.hh"
25 #include "register_manager.hh"
26 #include "remote.hh"
27 #include "shell_manager.hh"
28 #include "string.hh"
29 #include "user_interface.hh"
30 #include "window.hh"
31
32 #include <functional>
33 #include <utility>
34
35 #include <sys/types.h>
36 #include <sys/stat.h>
37 #include <fcntl.h>
38 #include <unistd.h>
39
40 #if defined(__GLIBC__) || defined(__CYGWIN__)
41 #include <malloc.h>
42 #endif
43
44 namespace Kakoune
45 {
46
47 extern const char* version;
48
49 namespace
50 {
51
open_fifo(StringView name,StringView filename,Buffer::Flags flags,bool scroll)52 Buffer* open_fifo(StringView name, StringView filename, Buffer::Flags flags, bool scroll)
53 {
54 int fd = open(parse_filename(filename).c_str(), O_RDONLY | O_NONBLOCK);
55 fcntl(fd, F_SETFD, FD_CLOEXEC);
56 if (fd < 0)
57 throw runtime_error(format("unable to open '{}'", filename));
58
59 return create_fifo_buffer(name.str(), fd, flags, scroll);
60 }
61
62 template<typename... Completers> struct PerArgumentCommandCompleter;
63
64 template<> struct PerArgumentCommandCompleter<>
65 {
operator ()Kakoune::__anon866708e20111::PerArgumentCommandCompleter66 Completions operator()(const Context&, CompletionFlags, CommandParameters,
67 size_t, ByteCount) const { return {}; }
68 };
69
70 template<typename Completer, typename... Rest>
71 struct PerArgumentCommandCompleter<Completer, Rest...> : PerArgumentCommandCompleter<Rest...>
72 {
73 template<typename C, typename... R,
74 typename = std::enable_if_t<not std::is_base_of<PerArgumentCommandCompleter<>,
75 std::remove_reference_t<C>>::value>>
76 PerArgumentCommandCompleter(C&& completer, R&&... rest)
77 : PerArgumentCommandCompleter<Rest...>(std::forward<R>(rest)...),
78 m_completer(std::forward<C>(completer)) {}
79
operator ()Kakoune::__anon866708e20111::PerArgumentCommandCompleter80 Completions operator()(const Context& context, CompletionFlags flags,
81 CommandParameters params, size_t token_to_complete,
82 ByteCount pos_in_token) const
83 {
84 if (token_to_complete == 0)
85 {
86 const String& arg = token_to_complete < params.size() ?
87 params[token_to_complete] : String();
88 return m_completer(context, flags, arg, pos_in_token);
89 }
90 return PerArgumentCommandCompleter<Rest...>::operator()(
91 context, flags, params.subrange(1),
92 token_to_complete-1, pos_in_token);
93 }
94
95 Completer m_completer;
96 };
97
98 template<typename... Completers>
99 PerArgumentCommandCompleter<std::decay_t<Completers>...>
make_completer(Completers &&...completers)100 make_completer(Completers&&... completers)
101 {
102 return {std::forward<Completers>(completers)...};
103 }
104
105 template<typename Completer>
add_flags(Completer completer,Completions::Flags completions_flags)106 auto add_flags(Completer completer, Completions::Flags completions_flags)
107 {
108 return [completer=std::move(completer), completions_flags]
109 (const Context& context, CompletionFlags flags, const String& prefix, ByteCount cursor_pos) {
110 Completions res = completer(context, flags, prefix, cursor_pos);
111 res.flags |= completions_flags;
112 return res;
113 };
114 }
115
116 template<typename Completer>
menu(Completer completer)117 auto menu(Completer completer)
118 {
119 return add_flags(std::move(completer), Completions::Flags::Menu);
120 }
121
122 template<bool menu>
123 auto filename_completer = make_completer(
124 [](const Context& context, CompletionFlags flags, const String& prefix, ByteCount cursor_pos)
__anon866708e20302(const Context& context, CompletionFlags flags, const String& prefix, ByteCount cursor_pos) 125 { return Completions{ 0_byte, cursor_pos,
126 complete_filename(prefix,
127 context.options()["ignored_files"].get<Regex>(),
128 cursor_pos, FilenameFlags::Expand),
129 menu ? Completions::Flags::Menu : Completions::Flags::None}; });
130
131 template<bool ignore_current = false>
complete_buffer_name(const Context & context,CompletionFlags flags,StringView prefix,ByteCount cursor_pos)132 static Completions complete_buffer_name(const Context& context, CompletionFlags flags,
133 StringView prefix, ByteCount cursor_pos)
134 {
135 struct RankedMatchAndBuffer : RankedMatch
136 {
137 RankedMatchAndBuffer(RankedMatch m, const Buffer* b)
138 : RankedMatch{std::move(m)}, buffer{b} {}
139
140 using RankedMatch::operator==;
141 using RankedMatch::operator<;
142
143 const Buffer* buffer;
144 };
145
146 StringView query = prefix.substr(0, cursor_pos);
147 Vector<RankedMatchAndBuffer> filename_matches;
148 Vector<RankedMatchAndBuffer> matches;
149 for (const auto& buffer : BufferManager::instance())
150 {
151 if (ignore_current and buffer.get() == &context.buffer())
152 continue;
153
154 StringView bufname = buffer->display_name();
155 if (buffer->flags() & Buffer::Flags::File)
156 {
157 if (RankedMatch match{split_path(bufname).second, query})
158 {
159 filename_matches.emplace_back(match, buffer.get());
160 continue;
161 }
162 }
163 if (RankedMatch match{bufname, query})
164 matches.emplace_back(match, buffer.get());
165 }
166 std::sort(filename_matches.begin(), filename_matches.end());
167 std::sort(matches.begin(), matches.end());
168
169 CandidateList res;
170 for (auto& match : filename_matches)
171 res.push_back(match.buffer->display_name());
172 for (auto& match : matches)
173 res.push_back(match.buffer->display_name());
174
175 return { 0, cursor_pos, res };
176 }
177
178 template<typename Func>
make_single_word_completer(Func && func)179 auto make_single_word_completer(Func&& func)
180 {
181 return make_completer(
182 [func = std::move(func)](const Context& context, CompletionFlags flags,
183 const String& prefix, ByteCount cursor_pos) -> Completions {
184 auto candidate = { func(context) };
185 return { 0_byte, cursor_pos, complete(prefix, cursor_pos, candidate) }; });
186 }
187
188 const ParameterDesc no_params{ {}, ParameterDesc::Flags::None, 0, 0 };
189 const ParameterDesc single_param{ {}, ParameterDesc::Flags::None, 1, 1 };
190 const ParameterDesc single_optional_param{ {}, ParameterDesc::Flags::None, 0, 1 };
191 const ParameterDesc double_params{ {}, ParameterDesc::Flags::None, 2, 2 };
192
193 static constexpr auto scopes = { "global", "buffer", "window" };
194
complete_scope(const Context &,CompletionFlags,const String & prefix,ByteCount cursor_pos)195 static Completions complete_scope(const Context&, CompletionFlags,
196 const String& prefix, ByteCount cursor_pos)
197 {
198 return { 0_byte, cursor_pos, complete(prefix, cursor_pos, scopes) };
199 }
200
201
complete_command_name(const Context & context,CompletionFlags,const String & prefix,ByteCount cursor_pos)202 static Completions complete_command_name(const Context& context, CompletionFlags,
203 const String& prefix, ByteCount cursor_pos)
204 {
205 return CommandManager::instance().complete_command_name(
206 context, prefix.substr(0, cursor_pos));
207 }
208
complete_alias_name(const Context & context,CompletionFlags,const String & prefix,ByteCount cursor_pos)209 static Completions complete_alias_name(const Context& context, CompletionFlags,
210 const String& prefix, ByteCount cursor_pos)
211 {
212 return { 0_byte, cursor_pos, complete(prefix, cursor_pos,
213 context.aliases().flatten_aliases()
214 | transform(&HashItem<String, String>::key)) };
215 }
216
217 struct ShellScriptCompleter
218 {
ShellScriptCompleterKakoune::__anon866708e20111::ShellScriptCompleter219 ShellScriptCompleter(String shell_script,
220 Completions::Flags flags = Completions::Flags::None)
221 : m_shell_script{std::move(shell_script)}, m_flags(flags) {}
222
operator ()Kakoune::__anon866708e20111::ShellScriptCompleter223 Completions operator()(const Context& context, CompletionFlags flags,
224 CommandParameters params, size_t token_to_complete,
225 ByteCount pos_in_token)
226 {
227 if (flags & CompletionFlags::Fast) // no shell on fast completion
228 return Completions{};
229
230 ShellContext shell_context{
231 params,
232 { { "token_to_complete", to_string(token_to_complete) },
233 { "pos_in_token", to_string(pos_in_token) } }
234 };
235 String output = ShellManager::instance().eval(m_shell_script, context, {},
236 ShellManager::Flags::WaitForStdout,
237 shell_context).first;
238 CandidateList candidates;
239 for (auto&& candidate : output | split<StringView>('\n'))
240 candidates.push_back(candidate.str());
241
242 return {0_byte, pos_in_token, std::move(candidates), m_flags};
243 }
244 private:
245 String m_shell_script;
246 Completions::Flags m_flags;
247 };
248
249 struct ShellCandidatesCompleter
250 {
ShellCandidatesCompleterKakoune::__anon866708e20111::ShellCandidatesCompleter251 ShellCandidatesCompleter(String shell_script,
252 Completions::Flags flags = Completions::Flags::None)
253 : m_shell_script{std::move(shell_script)}, m_flags(flags) {}
254
operator ()Kakoune::__anon866708e20111::ShellCandidatesCompleter255 Completions operator()(const Context& context, CompletionFlags flags,
256 CommandParameters params, size_t token_to_complete,
257 ByteCount pos_in_token)
258 {
259 if (flags & CompletionFlags::Start)
260 m_token = -1;
261
262 if (m_token != token_to_complete)
263 {
264 ShellContext shell_context{
265 params,
266 { { "token_to_complete", to_string(token_to_complete) } }
267 };
268 String output = ShellManager::instance().eval(m_shell_script, context, {},
269 ShellManager::Flags::WaitForStdout,
270 shell_context).first;
271 m_candidates.clear();
272 for (auto c : output | split<StringView>('\n'))
273 m_candidates.emplace_back(c.str(), used_letters(c));
274 m_token = token_to_complete;
275 }
276
277 StringView query = params[token_to_complete].substr(0, pos_in_token);
278 UsedLetters query_letters = used_letters(query);
279 Vector<RankedMatch> matches;
280 for (const auto& candidate : m_candidates)
281 {
282 if (RankedMatch match{candidate.first, candidate.second, query, query_letters})
283 matches.push_back(match);
284 }
285
286 constexpr size_t max_count = 100;
287 CandidateList res;
288 // Gather best max_count matches
289 for_n_best(matches, max_count, [](auto& lhs, auto& rhs) { return rhs < lhs; },
290 [&] (const RankedMatch& m) {
291 if (not res.empty() and res.back() == m.candidate())
292 return false;
293 res.push_back(m.candidate().str());
294 return true;
295 });
296
297 return Completions{0_byte, pos_in_token, std::move(res), m_flags};
298 }
299
300 private:
301 String m_shell_script;
302 Vector<std::pair<String, UsedLetters>, MemoryDomain::Completion> m_candidates;
303 int m_token = -1;
304 Completions::Flags m_flags;
305 };
306
307 template<typename Completer>
308 struct PromptCompleterAdapter
309 {
PromptCompleterAdapterKakoune::__anon866708e20111::PromptCompleterAdapter310 PromptCompleterAdapter(Completer completer) : m_completer{completer} {}
311
operator ()Kakoune::__anon866708e20111::PromptCompleterAdapter312 Completions operator()(const Context& context, CompletionFlags flags,
313 StringView prefix, ByteCount cursor_pos)
314 {
315 return m_completer(context, flags, {String{String::NoCopy{}, prefix}}, 0, cursor_pos);
316 }
317
318 private:
319 Completer m_completer;
320 };
321
get_scope_ifp(StringView scope,const Context & context)322 Scope* get_scope_ifp(StringView scope, const Context& context)
323 {
324 if (prefix_match("global", scope))
325 return &GlobalScope::instance();
326 else if (prefix_match("buffer", scope))
327 return &context.buffer();
328 else if (prefix_match("window", scope))
329 return &context.window();
330 else if (prefix_match(scope, "buffer="))
331 return &BufferManager::instance().get_buffer(scope.substr(7_byte));
332 return nullptr;
333 }
334
get_scope(StringView scope,const Context & context)335 Scope& get_scope(StringView scope, const Context& context)
336 {
337 if (auto s = get_scope_ifp(scope, context))
338 return *s;
339 throw runtime_error(format("no such scope: '{}'", scope));
340 }
341
342 struct CommandDesc
343 {
344 const char* name;
345 const char* alias;
346 const char* docstring;
347 ParameterDesc params;
348 CommandFlags flags;
349 CommandHelper helper;
350 CommandCompleter completer;
351 void (*func)(const ParametersParser&, Context&, const ShellContext&);
352 };
353
354 template<bool force_reload>
edit(const ParametersParser & parser,Context & context,const ShellContext &)355 void edit(const ParametersParser& parser, Context& context, const ShellContext&)
356 {
357 const bool scratch = (bool)parser.get_switch("scratch");
358
359 if (parser.positional_count() == 0 and not force_reload and not scratch)
360 throw wrong_argument_count();
361
362 const bool no_hooks = context.hooks_disabled();
363 const auto flags = (no_hooks ? Buffer::Flags::NoHooks : Buffer::Flags::None) |
364 (parser.get_switch("debug") ? Buffer::Flags::Debug : Buffer::Flags::None);
365
366 auto& buffer_manager = BufferManager::instance();
367 const auto& name = parser.positional_count() > 0 ?
368 parser[0] : (scratch ? generate_buffer_name("*scratch-{}*") : context.buffer().name());
369
370 Buffer* buffer = buffer_manager.get_buffer_ifp(name);
371 if (scratch)
372 {
373 if (parser.get_switch("readonly") or parser.get_switch("fifo") or parser.get_switch("scroll"))
374 throw runtime_error("scratch is not compatible with readonly, fifo or scroll");
375
376 if (buffer == nullptr or force_reload)
377 {
378 if (buffer != nullptr and force_reload)
379 buffer_manager.delete_buffer(*buffer);
380 buffer = create_buffer_from_string(std::move(name), flags, {});
381 }
382 else if (buffer->flags() & Buffer::Flags::File)
383 throw runtime_error(format("buffer '{}' exists but is not a scratch buffer", name));
384 }
385 else if (force_reload and buffer and buffer->flags() & Buffer::Flags::File)
386 {
387 reload_file_buffer(*buffer);
388 }
389 else
390 {
391 if (auto fifo = parser.get_switch("fifo"))
392 buffer = open_fifo(name, *fifo, flags, (bool)parser.get_switch("scroll"));
393 else if (not buffer)
394 {
395 buffer = parser.get_switch("existing") ? open_file_buffer(name, flags)
396 : open_or_create_file_buffer(name, flags);
397 if (buffer->flags() & Buffer::Flags::New)
398 context.print_status({ format("new file '{}'", name),
399 context.faces()["StatusLine"] });
400 }
401
402 buffer->flags() &= ~Buffer::Flags::NoHooks;
403 if (parser.get_switch("readonly"))
404 {
405 buffer->flags() |= Buffer::Flags::ReadOnly;
406 buffer->options().get_local_option("readonly").set(true);
407 }
408 }
409
410 Buffer* current_buffer = context.has_buffer() ? &context.buffer() : nullptr;
411
412 const size_t param_count = parser.positional_count();
413 if (current_buffer and (buffer != current_buffer or param_count > 1))
414 context.push_jump();
415
416 if (buffer != current_buffer)
417 context.change_buffer(*buffer);
418 buffer = &context.buffer(); // change_buffer hooks might change the buffer again
419
420 if (parser.get_switch("fifo") and not parser.get_switch("scroll"))
421 context.selections_write_only() = { *buffer, Selection{} };
422 else if (param_count > 1 and not parser[1].empty())
423 {
424 int line = std::max(0, str_to_int(parser[1]) - 1);
425 int column = param_count > 2 and not parser[2].empty() ?
426 std::max(0, str_to_int(parser[2]) - 1) : 0;
427
428 auto& buffer = context.buffer();
429 context.selections_write_only() = { buffer, buffer.clamp({ line, column }) };
430 if (context.has_window())
431 context.window().center_line(context.selections().main().cursor().line);
432 }
433 }
434
435 ParameterDesc edit_params{
436 { { "existing", { false, "fail if the file does not exist, do not open a new file" } },
437 { "scratch", { false, "create a scratch buffer, not linked to a file" } },
438 { "debug", { false, "create buffer as debug output" } },
439 { "fifo", { true, "create a buffer reading its content from a named fifo" } },
440 { "readonly", { false, "create a buffer in readonly mode" } },
441 { "scroll", { false, "place the initial cursor so that the fifo will scroll to show new data" } } },
442 ParameterDesc::Flags::None, 0, 3
443 };
444 const CommandDesc edit_cmd = {
445 "edit",
446 "e",
447 "edit [<switches>] <filename> [<line> [<column>]]: open the given filename in a buffer",
448 edit_params,
449 CommandFlags::None,
450 CommandHelper{},
451 filename_completer<false>,
452 edit<false>
453 };
454
455 const CommandDesc force_edit_cmd = {
456 "edit!",
457 "e!",
458 "edit! [<switches>] <filename> [<line> [<column>]]: open the given filename in a buffer, "
459 "force reload if needed",
460 edit_params,
461 CommandFlags::None,
462 CommandHelper{},
463 filename_completer<false>,
464 edit<true>
465 };
466
467 const ParameterDesc write_params{
468 {
469 { "sync", { false, "force the synchronization of the file onto the filesystem" } },
470 { "method", { true, "explicit writemethod (replace|overwrite)" } },
471 { "force", { false, "Allow overwriting existing file with explicit filename" } },
472 },
473 ParameterDesc::Flags::SwitchesOnlyAtStart, 0, 1
474 };
475
parse_write_method(StringView str)476 auto parse_write_method(StringView str)
477 {
478 constexpr auto desc = enum_desc(Meta::Type<WriteMethod>{});
479 auto it = find_if(desc, [str](const EnumDesc<WriteMethod>& d) { return d.name == str; });
480 if (it == desc.end())
481 throw runtime_error(format("invalid writemethod '{}'", str));
482 return it->value;
483 }
484
do_write_buffer(Context & context,Optional<String> filename,WriteFlags flags,Optional<WriteMethod> write_method={})485 void do_write_buffer(Context& context, Optional<String> filename, WriteFlags flags, Optional<WriteMethod> write_method = {})
486 {
487 Buffer& buffer = context.buffer();
488 const bool is_file = (bool)(buffer.flags() & Buffer::Flags::File);
489
490 if (not filename and !is_file)
491 throw runtime_error("cannot write a non file buffer without a filename");
492
493 const bool is_readonly = (bool)(context.buffer().flags() & Buffer::Flags::ReadOnly);
494 // if the buffer is in read-only mode and we try to save it directly
495 // or we try to write to it indirectly using e.g. a symlink, throw an error
496 if (is_file and is_readonly and
497 (not filename or real_path(*filename) == buffer.name()))
498 throw runtime_error("cannot overwrite the buffer when in readonly mode");
499
500 auto effective_filename = filename ? parse_filename(*filename) : buffer.name();
501 if (filename and not (flags & WriteFlags::Force) and
502 real_path(effective_filename) != buffer.name() and
503 regular_file_exists(effective_filename))
504 throw runtime_error("cannot overwrite existing file without -force");
505
__anon866708e20802null506 auto method = write_method.value_or_compute([&] { return context.options()["writemethod"].get<WriteMethod>(); });
507
508 context.hooks().run_hook(Hook::BufWritePre, effective_filename, context);
509 write_buffer_to_file(buffer, effective_filename, method, flags);
510 context.hooks().run_hook(Hook::BufWritePost, effective_filename, context);
511 }
512
513 template<bool force = false>
write_buffer(const ParametersParser & parser,Context & context,const ShellContext &)514 void write_buffer(const ParametersParser& parser, Context& context, const ShellContext&)
515 {
516 return do_write_buffer(context,
517 parser.positional_count() > 0 ? parser[0] : Optional<String>{},
518 (parser.get_switch("sync") ? WriteFlags::Sync : WriteFlags::None) |
519 (parser.get_switch("force") or force ? WriteFlags::Force : WriteFlags::None),
520 parser.get_switch("method").map(parse_write_method));
521 }
522
523 const CommandDesc write_cmd = {
524 "write",
525 "w",
526 "write [<switches>] [<filename>]: write the current buffer to its file "
527 "or to <filename> if specified",
528 write_params,
529 CommandFlags::None,
530 CommandHelper{},
531 filename_completer<false>,
532 write_buffer,
533 };
534
535 const CommandDesc force_write_cmd = {
536 "write!",
537 "w!",
538 "write! [<switches>] [<filename>]: write the current buffer to its file "
539 "or to <filename> if specified, even when the file is write protected",
540 write_params,
541 CommandFlags::None,
542 CommandHelper{},
543 filename_completer<false>,
544 write_buffer<true>,
545 };
546
write_all_buffers(const Context & context,bool sync=false,Optional<WriteMethod> write_method={})547 void write_all_buffers(const Context& context, bool sync = false, Optional<WriteMethod> write_method = {})
548 {
549 // Copy buffer list because hooks might be creating/deleting buffers
550 Vector<SafePtr<Buffer>> buffers;
551 for (auto& buffer : BufferManager::instance())
552 buffers.emplace_back(buffer.get());
553
554 for (auto& buffer : buffers)
555 {
556 if ((buffer->flags() & Buffer::Flags::File) and
557 ((buffer->flags() & Buffer::Flags::New) or
558 buffer->is_modified())
559 and !(buffer->flags() & Buffer::Flags::ReadOnly))
560 {
__anon866708e20902null561 auto method = write_method.value_or_compute([&] { return context.options()["writemethod"].get<WriteMethod>(); });
562 auto flags = sync ? WriteFlags::Sync : WriteFlags::None;
563 buffer->run_hook_in_own_context(Hook::BufWritePre, buffer->name(), context.name());
564 write_buffer_to_file(*buffer, buffer->name(), method, flags);
565 buffer->run_hook_in_own_context(Hook::BufWritePost, buffer->name(), context.name());
566 }
567 }
568 }
569
570 const CommandDesc write_all_cmd = {
571 "write-all",
572 "wa",
573 "write-all [<switches>]: write all changed buffers that are associated to a file",
574 ParameterDesc{
575 write_params.switches,
576 ParameterDesc::Flags::None, 0, 0
577 },
578 CommandFlags::None,
579 CommandHelper{},
580 CommandCompleter{},
__anon866708e20a02()581 [](const ParametersParser& parser, Context& context, const ShellContext&){
582 write_all_buffers(context,
583 (bool)parser.get_switch("sync"),
584 parser.get_switch("method").map(parse_write_method));
585 }
586 };
587
ensure_all_buffers_are_saved()588 static void ensure_all_buffers_are_saved()
589 {
590 auto is_modified = [](const std::unique_ptr<Buffer>& buf) {
591 return (buf->flags() & Buffer::Flags::File) and buf->is_modified();
592 };
593
594 auto it = find_if(BufferManager::instance(), is_modified);
595 const auto end = BufferManager::instance().end();
596 if (it == end)
597 return;
598
599 String message = format("{} modified buffers remaining: [",
600 std::count_if(it, end, is_modified));
601 while (it != end)
602 {
603 message += (*it)->name();
604 it = std::find_if(it+1, end, is_modified);
605 message += (it != end) ? ", " : "]";
606 }
607 throw runtime_error(message);
608 }
609
610 template<bool force>
kill(const ParametersParser & parser,Context & context,const ShellContext &)611 void kill(const ParametersParser& parser, Context& context, const ShellContext&)
612 {
613 auto& client_manager = ClientManager::instance();
614
615 if (not force)
616 ensure_all_buffers_are_saved();
617
618 const int status = parser.positional_count() > 0 ? str_to_int(parser[0]) : 0;
619 while (not client_manager.empty())
620 client_manager.remove_client(**client_manager.begin(), true, status);
621
622 throw kill_session{status};
623 }
624
625 const CommandDesc kill_cmd = {
626 "kill",
627 nullptr,
628 "kill [<exit status>]: terminate the current session, the server and all clients connected. "
629 "An optional integer parameter can set the server and client processes exit status",
630 { {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
631 CommandFlags::None,
632 CommandHelper{},
633 CommandCompleter{},
634 kill<false>
635 };
636
637
638 const CommandDesc force_kill_cmd = {
639 "kill!",
640 nullptr,
641 "kill! [<exit status>]: force the termination of the current session, the server and all clients connected. "
642 "An optional integer parameter can set the server and client processes exit status",
643 { {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
644 CommandFlags::None,
645 CommandHelper{},
646 CommandCompleter{},
647 kill<true>
648 };
649
650 template<bool force>
quit(const ParametersParser & parser,Context & context,const ShellContext &)651 void quit(const ParametersParser& parser, Context& context, const ShellContext&)
652 {
653 if (not force and ClientManager::instance().count() == 1 and not Server::instance().is_daemon())
654 ensure_all_buffers_are_saved();
655
656 const int status = parser.positional_count() > 0 ? str_to_int(parser[0]) : 0;
657 ClientManager::instance().remove_client(context.client(), true, status);
658 }
659
660 const CommandDesc quit_cmd = {
661 "quit",
662 "q",
663 "quit [<exit status>]: quit current client, and the kakoune session if the client is the last "
664 "(if not running in daemon mode). "
665 "An optional integer parameter can set the client exit status",
666 { {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
667 CommandFlags::None,
668 CommandHelper{},
669 CommandCompleter{},
670 quit<false>
671 };
672
673 const CommandDesc force_quit_cmd = {
674 "quit!",
675 "q!",
676 "quit! [<exit status>]: quit current client, and the kakoune session if the client is the last "
677 "(if not running in daemon mode). Force quit even if the client is the "
678 "last and some buffers are not saved. "
679 "An optional integer parameter can set the client exit status",
680 { {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 1 },
681 CommandFlags::None,
682 CommandHelper{},
683 CommandCompleter{},
684 quit<true>
685 };
686
687 template<bool force>
write_quit(const ParametersParser & parser,Context & context,const ShellContext & shell_context)688 void write_quit(const ParametersParser& parser, Context& context,
689 const ShellContext& shell_context)
690 {
691 do_write_buffer(context, {},
692 parser.get_switch("sync") ? WriteFlags::Sync : WriteFlags::None,
693 parser.get_switch("method").map(parse_write_method));
694 quit<force>(parser, context, shell_context);
695 }
696
697 const CommandDesc write_quit_cmd = {
698 "write-quit",
699 "wq",
700 "write-quit [-sync] [<exit status>]: write current buffer and quit current client. "
701 "An optional integer parameter can set the client exit status",
702 write_params,
703 CommandFlags::None,
704 CommandHelper{},
705 CommandCompleter{},
706 write_quit<false>
707 };
708
709 const CommandDesc force_write_quit_cmd = {
710 "write-quit!",
711 "wq!",
712 "write-quit! [-sync] [<exit status>] write: current buffer and quit current client, even if other buffers are not saved. "
713 "An optional integer parameter can set the client exit status",
714 write_params,
715 CommandFlags::None,
716 CommandHelper{},
717 CommandCompleter{},
718 write_quit<true>
719 };
720
721 const CommandDesc write_all_quit_cmd = {
722 "write-all-quit",
723 "waq",
724 "write-all-quit [-sync] [<exit status>]: write all buffers associated to a file and quit current client. "
725 "An optional integer parameter can set the client exit status.",
726 write_params,
727 CommandFlags::None,
728 CommandHelper{},
729 CommandCompleter{},
730 [](const ParametersParser& parser, Context& context, const ShellContext& shell_context)
__anon866708e20c02() 731 {
732 write_all_buffers(context,
733 (bool)parser.get_switch("sync"),
734 parser.get_switch("method").map(parse_write_method));
735 quit<false>(parser, context, shell_context);
736 }
737 };
738
739 const CommandDesc buffer_cmd = {
740 "buffer",
741 "b",
742 "buffer <name>: set buffer to edit in current client",
743 single_param,
744 CommandFlags::None,
745 CommandHelper{},
746 make_completer(menu(complete_buffer_name<true>)),
747 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e20d02() 748 {
749 Buffer& buffer = BufferManager::instance().get_buffer(parser[0]);
750 if (&buffer != &context.buffer())
751 {
752 context.push_jump();
753 context.change_buffer(buffer);
754 }
755 }
756 };
757
758 template<bool next>
cycle_buffer(const ParametersParser & parser,Context & context,const ShellContext &)759 void cycle_buffer(const ParametersParser& parser, Context& context, const ShellContext&)
760 {
761 Buffer* oldbuf = &context.buffer();
762 auto it = find_if(BufferManager::instance(),
763 [oldbuf](const std::unique_ptr<Buffer>& lhs)
764 { return lhs.get() == oldbuf; });
765 kak_assert(it != BufferManager::instance().end());
766
767 Buffer* newbuf = nullptr;
768 auto cycle = [&] {
769 if (not next)
770 {
771 if (it == BufferManager::instance().begin())
772 it = BufferManager::instance().end();
773 --it;
774 }
775 else
776 {
777 if (++it == BufferManager::instance().end())
778 it = BufferManager::instance().begin();
779 }
780 newbuf = it->get();
781 };
782 cycle();
783 while (newbuf != oldbuf and newbuf->flags() & Buffer::Flags::Debug)
784 cycle();
785
786 if (newbuf != oldbuf)
787 {
788 context.push_jump();
789 context.change_buffer(*newbuf);
790 }
791 }
792
793 const CommandDesc buffer_next_cmd = {
794 "buffer-next",
795 "bn",
796 "buffer-next: move to the next buffer in the list",
797 no_params,
798 CommandFlags::None,
799 CommandHelper{},
800 CommandCompleter{},
801 cycle_buffer<true>
802 };
803
804 const CommandDesc buffer_previous_cmd = {
805 "buffer-previous",
806 "bp",
807 "buffer-previous: move to the previous buffer in the list",
808 no_params,
809 CommandFlags::None,
810 CommandHelper{},
811 CommandCompleter{},
812 cycle_buffer<false>
813 };
814
815 template<bool force>
delete_buffer(const ParametersParser & parser,Context & context,const ShellContext &)816 void delete_buffer(const ParametersParser& parser, Context& context, const ShellContext&)
817 {
818 BufferManager& manager = BufferManager::instance();
819 Buffer& buffer = parser.positional_count() == 0 ? context.buffer() : manager.get_buffer(parser[0]);
820 if (not force and (buffer.flags() & Buffer::Flags::File) and buffer.is_modified())
821 throw runtime_error(format("buffer '{}' is modified", buffer.name()));
822
823 manager.delete_buffer(buffer);
824 context.forget_buffer(buffer);
825 }
826
827 const CommandDesc delete_buffer_cmd = {
828 "delete-buffer",
829 "db",
830 "delete-buffer [name]: delete current buffer or the buffer named <name> if given",
831 single_optional_param,
832 CommandFlags::None,
833 CommandHelper{},
834 make_completer(menu(complete_buffer_name<false>)),
835 delete_buffer<false>
836 };
837
838 const CommandDesc force_delete_buffer_cmd = {
839 "delete-buffer!",
840 "db!",
841 "delete-buffer! [name]: delete current buffer or the buffer named <name> if "
842 "given, even if the buffer is unsaved",
843 single_optional_param,
844 CommandFlags::None,
845 CommandHelper{},
846 make_completer(menu(complete_buffer_name<false>)),
847 delete_buffer<true>
848 };
849
850 const CommandDesc rename_buffer_cmd = {
851 "rename-buffer",
852 nullptr,
853 "rename-buffer <name>: change current buffer name",
854 ParameterDesc{
855 {
856 { "scratch", { false, "convert a file buffer to a scratch buffer" } },
857 { "file", { false, "convert a scratch buffer to a file buffer" } }
858 },
859 ParameterDesc::Flags::None, 1, 1
860 },
861 CommandFlags::None,
862 CommandHelper{},
863 filename_completer<false>,
864 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e21002() 865 {
866 if (parser.get_switch("scratch") and parser.get_switch("file"))
867 throw runtime_error("scratch and file are incompatible switches");
868
869 auto& buffer = context.buffer();
870 if (parser.get_switch("scratch"))
871 buffer.flags() &= ~(Buffer::Flags::File | Buffer::Flags::New);
872 if (parser.get_switch("file"))
873 buffer.flags() |= Buffer::Flags::File;
874
875 const bool is_file = (buffer.flags() & Buffer::Flags::File);
876
877 if (not buffer.set_name(is_file ? parse_filename(parser[0]) : parser[0]))
878 throw runtime_error(format("unable to change buffer name to '{}': a buffer with this name already exists", parser[0]));
879 }
880 };
881
882 static constexpr auto highlighter_scopes = { "global/", "buffer/", "window/", "shared/" };
883
884 template<bool add>
highlighter_cmd_completer(const Context & context,CompletionFlags flags,CommandParameters params,size_t token_to_complete,ByteCount pos_in_token)885 Completions highlighter_cmd_completer(
886 const Context& context, CompletionFlags flags, CommandParameters params,
887 size_t token_to_complete, ByteCount pos_in_token)
888 {
889 if (token_to_complete == 0)
890 {
891
892 StringView path = params[0];
893 auto sep_it = find(path, '/');
894 if (sep_it == path.end())
895 return { 0_byte, pos_in_token, complete(path, pos_in_token, highlighter_scopes) };
896
897 StringView scope{path.begin(), sep_it};
898 HighlighterGroup* root = nullptr;
899 if (scope == "shared")
900 root = &SharedHighlighters::instance();
901 else if (auto* s = get_scope_ifp(scope, context))
902 root = &s->highlighters().group();
903 else
904 return {};
905
906 auto offset = scope.length() + 1;
907 return offset_pos(root->complete_child(StringView{sep_it+1, path.end()}, pos_in_token - offset, add), offset);
908 }
909 else if (add and token_to_complete == 1)
910 {
911 StringView name = params[1];
912 return { 0_byte, name.length(), complete(name, pos_in_token, HighlighterRegistry::instance() | transform(&HighlighterRegistry::Item::key)) };
913 }
914 else
915 return {};
916 }
917
get_highlighter(const Context & context,StringView path)918 Highlighter& get_highlighter(const Context& context, StringView path)
919 {
920 if (not path.empty() and path.back() == '/')
921 path = path.substr(0_byte, path.length() - 1);
922
923 auto sep_it = find(path, '/');
924 StringView scope{path.begin(), sep_it};
925 auto* root = (scope == "shared") ? static_cast<HighlighterGroup*>(&SharedHighlighters::instance())
926 : static_cast<HighlighterGroup*>(&get_scope(scope, context).highlighters().group());
927 if (sep_it != path.end())
928 return root->get_child(StringView{sep_it+1, path.end()});
929 return *root;
930 }
931
redraw_relevant_clients(Context & context,StringView highlighter_path)932 static void redraw_relevant_clients(Context& context, StringView highlighter_path)
933 {
934 StringView scope{highlighter_path.begin(), find(highlighter_path, '/')};
935 if (scope == "window")
936 context.window().force_redraw();
937 else if (scope == "buffer" or prefix_match(scope, "buffer="))
938 {
939 auto& buffer = scope == "buffer" ? context.buffer() : BufferManager::instance().get_buffer(scope.substr(7_byte));
940 for (auto&& client : ClientManager::instance())
941 {
942 if (&client->context().buffer() == &buffer)
943 client->context().window().force_redraw();
944 }
945 }
946 else
947 {
948 for (auto&& client : ClientManager::instance())
949 client->context().window().force_redraw();
950 }
951 }
952
953 const CommandDesc arrange_buffers_cmd = {
954 "arrange-buffers",
955 nullptr,
956 "arrange-buffers <buffer>...: reorder the buffers in the buffers list\n"
957 " the named buffers will be moved to the front of the buffer list, in the order given\n"
958 " buffers that do not appear in the parameters will remain at the end of the list, keeping their current order",
959 ParameterDesc{{}, ParameterDesc::Flags::None, 1},
960 CommandFlags::None,
961 CommandHelper{},
962 [](const Context& context, CompletionFlags flags, CommandParameters params, size_t, ByteCount cursor_pos)
__anon866708e21102() 963 {
964 return complete_buffer_name<false>(context, flags, params.back(), cursor_pos);
965 },
966 [](const ParametersParser& parser, Context&, const ShellContext&)
__anon866708e21202() 967 {
968 BufferManager::instance().arrange_buffers(parser.positionals_from(0));
969 }
970 };
971
972 const CommandDesc add_highlighter_cmd = {
973 "add-highlighter",
974 "addhl",
975 "add-highlighter <path>/<name> <type> <type params>...: add a highlighter to the group identified by <path>\n"
976 " <path> is a '/' delimited path or the parent highlighter, starting with either\n"
977 " 'global', 'buffer', 'window' or 'shared', if <name> is empty, it will be autogenerated",
978 ParameterDesc{
979 { { "override", { false, "replace existing highlighter with same path if it exists" } }, },
980 ParameterDesc::Flags::SwitchesOnlyAtStart, 2
981 },
982 CommandFlags::None,
983 [](const Context& context, CommandParameters params) -> String
__anon866708e21302() 984 {
985 if (params.size() > 1)
986 {
987 HighlighterRegistry& registry = HighlighterRegistry::instance();
988 auto it = registry.find(params[1]);
989 if (it != registry.end())
990 {
991 auto docstring = it->value.description->docstring;
992 auto desc_params = generate_switches_doc(it->value.description->params.switches);
993
994 if (desc_params.empty())
995 return format("{}:\n{}", params[1], indent(docstring));
996 else
997 {
998 auto desc_indent = Vector<String>{docstring, "Switches:", indent(desc_params)}
999 | transform([](auto& s) { return indent(s); });
1000 return format("{}:\n{}", params[1], join(desc_indent, "\n"));
1001 }
1002 }
1003 }
1004 return "";
1005 },
1006 highlighter_cmd_completer<true>,
1007 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e21502() 1008 {
1009 HighlighterRegistry& registry = HighlighterRegistry::instance();
1010
1011 auto begin = parser.begin();
1012 StringView path = *begin++;
1013 StringView type = *begin++;
1014 Vector<String> highlighter_params;
1015 for (; begin != parser.end(); ++begin)
1016 highlighter_params.push_back(*begin);
1017
1018 auto it = registry.find(type);
1019 if (it == registry.end())
1020 throw runtime_error(format("no such highlighter type: '{}'", type));
1021
1022 auto slash = find(path | reverse(), '/');
1023 if (slash == path.rend())
1024 throw runtime_error("no parent in path");
1025
1026 auto auto_name = [](ConstArrayView<String> params) {
1027 return join(params | transform([](StringView s) { return replace(s, "/", "<slash>"); }), "_");
1028 };
1029
1030 String name{slash.base(), path.end()};
1031 Highlighter& parent = get_highlighter(context, {path.begin(), slash.base() - 1});
1032 parent.add_child(name.empty() ? auto_name(parser.positionals_from(1)) : std::move(name),
1033 it->value.factory(highlighter_params, &parent), (bool)parser.get_switch("override"));
1034
1035 redraw_relevant_clients(context, path);
1036 }
1037 };
1038
1039 const CommandDesc remove_highlighter_cmd = {
1040 "remove-highlighter",
1041 "rmhl",
1042 "remove-highlighter <path>: remove highlighter identified by <path>",
1043 single_param,
1044 CommandFlags::None,
1045 CommandHelper{},
1046 highlighter_cmd_completer<false>,
1047 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e21802() 1048 {
1049 StringView path = parser[0];
1050 if (not path.empty() and path.back() == '/') // ignore trailing /
1051 path = path.substr(0_byte, path.length() - 1_byte);
1052
1053 auto rev_path = path | reverse();
1054 auto sep_it = find(rev_path, '/');
1055 if (sep_it == rev_path.end())
1056 return;
1057 get_highlighter(context, {path.begin(), sep_it.base()}).remove_child({sep_it.base(), path.end()});
1058 redraw_relevant_clients(context, path);
1059 }
1060 };
1061
complete_hooks(const Context &,CompletionFlags,const String & prefix,ByteCount cursor_pos)1062 static Completions complete_hooks(const Context&, CompletionFlags,
1063 const String& prefix, ByteCount cursor_pos)
1064 {
1065 return { 0_byte, cursor_pos, complete(prefix, cursor_pos, enum_desc(Meta::Type<Hook>{}) | transform(&EnumDesc<Hook>::name)) };
1066 }
1067
1068 const CommandDesc add_hook_cmd = {
1069 "hook",
1070 nullptr,
1071 "hook [<switches>] <scope> <hook_name> <filter> <command>: add <command> in <scope> "
1072 "to be executed on hook <hook_name> when its parameter matches the <filter> regex\n"
1073 "<scope> can be:\n"
1074 " * global: hook is executed for any buffer or window\n"
1075 " * buffer: hook is executed only for the current buffer\n"
1076 " (and any window for that buffer)\n"
1077 " * window: hook is executed only for the current window\n",
1078 ParameterDesc{
1079 { { "group", { true, "set hook group, see remove-hooks" } },
1080 { "always", { false, "run hook even if hooks are disabled" } },
1081 { "once", { false, "run the hook only once" } } },
1082 ParameterDesc::Flags::None, 4, 4
1083 },
1084 CommandFlags::None,
1085 CommandHelper{},
1086 make_completer(complete_scope, complete_hooks, complete_nothing,
1087 [](const Context& context, CompletionFlags flags,
1088 const String& prefix, ByteCount cursor_pos)
__anon866708e21902() 1089 { return CommandManager::instance().complete(
1090 context, flags, prefix, cursor_pos); }),
1091 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e21a02() 1092 {
1093 auto descs = enum_desc(Meta::Type<Hook>{});
1094 auto it = find_if(descs, [&](const EnumDesc<Hook>& desc) { return desc.name == parser[1]; });
1095 if (it == descs.end())
1096 throw runtime_error{format("no such hook: '{}'", parser[1])};
1097
1098 Regex regex{parser[2], RegexCompileFlags::Optimize};
1099 const String& command = parser[3];
1100 auto group = parser.get_switch("group").value_or(StringView{});
1101
1102 if (any_of(group, [](char c) { return not is_word(c, { '-' }); }) or
1103 (not group.empty() and not is_word(group[0])))
1104 throw runtime_error{format("invalid group name '{}'", group)};
1105
1106 const auto flags = (parser.get_switch("always") ? HookFlags::Always : HookFlags::None) |
1107 (parser.get_switch("once") ? HookFlags::Once : HookFlags::None);
1108 get_scope(parser[0], context).hooks().add_hook(it->value, group.str(), flags,
1109 std::move(regex), command);
1110 }
1111 };
1112
1113 const CommandDesc remove_hook_cmd = {
1114 "remove-hooks",
1115 "rmhooks",
1116 "remove-hooks <scope> <group>: remove all hooks whose group matches the regex <group>",
1117 double_params,
1118 CommandFlags::None,
1119 CommandHelper{},
1120 [](const Context& context, CompletionFlags flags,
1121 CommandParameters params, size_t token_to_complete,
1122 ByteCount pos_in_token) -> Completions
__anon866708e21d02() 1123 {
1124 if (token_to_complete == 0)
1125 return { 0_byte, params[0].length(),
1126 complete(params[0], pos_in_token, scopes) };
1127 else if (token_to_complete == 1)
1128 {
1129 if (auto scope = get_scope_ifp(params[0], context))
1130 return { 0_byte, params[0].length(),
1131 scope->hooks().complete_hook_group(params[1], pos_in_token) };
1132 }
1133 return {};
1134 },
1135 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e21e02() 1136 {
1137 get_scope(parser[0], context).hooks().remove_hooks(Regex{parser[1]});
1138 }
1139 };
1140
1141 const CommandDesc trigger_user_hook_cmd = {
1142 "trigger-user-hook",
1143 nullptr,
1144 "trigger-user-hook <param>: run 'User' hook with <param> as filter string",
1145 single_param,
1146 CommandFlags::None,
1147 CommandHelper{},
1148 CommandCompleter{},
1149 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e21f02() 1150 {
1151 context.hooks().run_hook(Hook::User, parser[0], context);
1152 }
1153 };
1154
params_to_shell(const ParametersParser & parser)1155 Vector<String> params_to_shell(const ParametersParser& parser)
1156 {
1157 Vector<String> vars;
1158 for (size_t i = 0; i < parser.positional_count(); ++i)
1159 vars.push_back(parser[i]);
1160 return vars;
1161 }
1162
define_command(const ParametersParser & parser,Context & context,const ShellContext &)1163 void define_command(const ParametersParser& parser, Context& context, const ShellContext&)
1164 {
1165 const String& cmd_name = parser[0];
1166 auto& cm = CommandManager::instance();
1167
1168 if (not all_of(cmd_name, is_identifier))
1169 throw runtime_error(format("invalid command name: '{}'", cmd_name));
1170
1171 if (cm.command_defined(cmd_name) and not parser.get_switch("override"))
1172 throw runtime_error(format("command '{}' already defined", cmd_name));
1173
1174 CommandFlags flags = CommandFlags::None;
1175 if (parser.get_switch("hidden"))
1176 flags = CommandFlags::Hidden;
1177
1178 const Completions::Flags completions_flags = parser.get_switch("menu") ?
1179 Completions::Flags::Menu : Completions::Flags::None;
1180
1181 const String& commands = parser[1];
1182 CommandFunc cmd;
1183 ParameterDesc desc;
1184 if (auto params = parser.get_switch("params"))
1185 {
1186 size_t min = 0, max = -1;
1187 StringView counts = *params;
1188 static const Regex re{R"((\d+)?..(\d+)?)"};
1189 MatchResults<const char*> res;
1190 if (regex_match(counts.begin(), counts.end(), res, re))
1191 {
1192 if (res[1].matched)
1193 min = (size_t)str_to_int({res[1].first, res[1].second});
1194 if (res[2].matched)
1195 max = (size_t)str_to_int({res[2].first, res[2].second});
1196 }
1197 else
1198 min = max = (size_t)str_to_int(counts);
1199
1200 desc = ParameterDesc{ {}, ParameterDesc::Flags::SwitchesAsPositional, min, max };
1201 cmd = [=](const ParametersParser& parser, Context& context, const ShellContext& sc) {
1202 CommandManager::instance().execute(commands, context,
1203 { params_to_shell(parser), sc.env_vars });
1204 };
1205 }
1206 else
1207 {
1208 desc = ParameterDesc{ {}, ParameterDesc::Flags::SwitchesAsPositional, 0, 0 };
1209 cmd = [=](const ParametersParser& parser, Context& context, const ShellContext& sc) {
1210 CommandManager::instance().execute(commands, context, { {}, sc.env_vars });
1211 };
1212 }
1213
1214 CommandCompleter completer;
1215 if (parser.get_switch("file-completion"))
1216 {
1217 completer = [=](const Context& context, CompletionFlags flags,
1218 CommandParameters params,
1219 size_t token_to_complete, ByteCount pos_in_token)
1220 {
1221 const String& prefix = params[token_to_complete];
1222 const auto& ignored_files = context.options()["ignored_files"].get<Regex>();
1223 return Completions{0_byte, pos_in_token,
1224 complete_filename(prefix, ignored_files,
1225 pos_in_token, FilenameFlags::Expand),
1226 completions_flags};
1227 };
1228 }
1229 else if (parser.get_switch("client-completion"))
1230 {
1231 completer = [=](const Context& context, CompletionFlags flags,
1232 CommandParameters params,
1233 size_t token_to_complete, ByteCount pos_in_token)
1234 {
1235 const String& prefix = params[token_to_complete];
1236 auto& cm = ClientManager::instance();
1237 return Completions{0_byte, pos_in_token,
1238 cm.complete_client_name(prefix, pos_in_token),
1239 completions_flags};
1240 };
1241 }
1242 else if (parser.get_switch("buffer-completion"))
1243 {
1244 completer = [=](const Context& context, CompletionFlags flags,
1245 CommandParameters params,
1246 size_t token_to_complete, ByteCount pos_in_token)
1247 {
1248 return add_flags(complete_buffer_name<false>, completions_flags)(
1249 context, flags, params[token_to_complete], pos_in_token);
1250 };
1251 }
1252 else if (auto shell_script = parser.get_switch("shell-script-completion"))
1253 {
1254 completer = ShellScriptCompleter{shell_script->str(), completions_flags};
1255 }
1256 else if (auto shell_script = parser.get_switch("shell-script-candidates"))
1257 {
1258 completer = ShellCandidatesCompleter{shell_script->str(), completions_flags};
1259 }
1260 else if (parser.get_switch("command-completion"))
1261 {
1262 completer = [](const Context& context, CompletionFlags flags,
1263 CommandParameters params,
1264 size_t token_to_complete, ByteCount pos_in_token)
1265 {
1266 return CommandManager::instance().complete(
1267 context, flags, params, token_to_complete, pos_in_token);
1268 };
1269 }
1270 else if (parser.get_switch("shell-completion"))
1271 {
1272 completer = [=](const Context& context, CompletionFlags flags,
1273 CommandParameters params,
1274 size_t token_to_complete, ByteCount pos_in_token)
1275 {
1276 return add_flags(shell_complete, completions_flags)(
1277 context, flags, params[token_to_complete], pos_in_token);
1278 };
1279 }
1280
1281 auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{}));
1282
1283 cm.register_command(cmd_name, cmd, docstring, desc, flags, CommandHelper{}, completer);
1284 }
1285
1286 const CommandDesc define_command_cmd = {
1287 "define-command",
1288 "def",
1289 "define-command [<switches>] <name> <cmds>: define a command <name> executing <cmds>",
1290 ParameterDesc{
1291 { { "params", { true, "take parameters, accessible to each shell escape as $0..$N\n"
1292 "parameter should take the form <count> or <min>..<max> (both omittable)" } },
1293 { "override", { false, "allow overriding an existing command" } },
1294 { "hidden", { false, "do not display the command in completion candidates" } },
1295 { "docstring", { true, "define the documentation string for command" } },
1296 { "menu", { false, "treat completions as the only valid inputs" } },
1297 { "file-completion", { false, "complete parameters using filename completion" } },
1298 { "client-completion", { false, "complete parameters using client name completion" } },
1299 { "buffer-completion", { false, "complete parameters using buffer name completion" } },
1300 { "command-completion", { false, "complete parameters using kakoune command completion" } },
1301 { "shell-completion", { false, "complete parameters using shell command completion" } },
1302 { "shell-script-completion", { true, "complete parameters using the given shell-script" } },
1303 { "shell-script-candidates", { true, "get the parameter candidates using the given shell-script" } } },
1304 ParameterDesc::Flags::None,
1305 2, 2
1306 },
1307 CommandFlags::None,
1308 CommandHelper{},
1309 CommandCompleter{},
1310 define_command
1311 };
1312
1313 const CommandDesc alias_cmd = {
1314 "alias",
1315 nullptr,
1316 "alias <scope> <alias> <command>: alias <alias> to <command> in <scope>",
1317 ParameterDesc{{}, ParameterDesc::Flags::None, 3, 3},
1318 CommandFlags::None,
1319 CommandHelper{},
1320 make_completer(complete_scope, complete_alias_name, complete_command_name),
1321 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e22702() 1322 {
1323 if (not CommandManager::instance().command_defined(parser[2]))
1324 throw runtime_error(format("no such command: '{}'", parser[2]));
1325
1326 AliasRegistry& aliases = get_scope(parser[0], context).aliases();
1327 aliases.add_alias(parser[1], parser[2]);
1328 }
1329 };
1330
complete_alias(const Context & context,CompletionFlags flags,const String & prefix,ByteCount cursor_pos)1331 static Completions complete_alias(const Context& context, CompletionFlags flags,
1332 const String& prefix, ByteCount cursor_pos)
1333 {
1334 return {0_byte, cursor_pos,
1335 complete(prefix, cursor_pos, context.aliases().flatten_aliases() |
1336 transform([](auto& entry) -> const String& { return entry.key; }))};
1337 }
1338
1339 const CommandDesc unalias_cmd = {
1340 "unalias",
1341 nullptr,
1342 "unalias <scope> <alias> [<expected>]: remove <alias> from <scope>\n"
1343 "If <expected> is specified, remove <alias> only if its value is <expected>",
1344 ParameterDesc{{}, ParameterDesc::Flags::None, 2, 3},
1345 CommandFlags::None,
1346 CommandHelper{},
1347 make_completer(complete_scope, complete_alias, complete_command_name),
1348 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e22902() 1349 {
1350 AliasRegistry& aliases = get_scope(parser[0], context).aliases();
1351 if (parser.positional_count() == 3 and
1352 aliases[parser[1]] != parser[2])
1353 return;
1354 aliases.remove_alias(parser[1]);
1355 }
1356 };
1357
1358 const CommandDesc echo_cmd = {
1359 "echo",
1360 nullptr,
1361 "echo <params>...: display given parameters in the status line",
1362 ParameterDesc{
1363 { { "markup", { false, "parse markup" } },
1364 { "quoting", { true, "quote each argument separately using the given style (raw|kakoune|shell)" } },
1365 { "to-file", { true, "echo contents to given filename" } },
1366 { "debug", { false, "write to debug buffer instead of status line" } } },
1367 ParameterDesc::Flags::SwitchesOnlyAtStart
1368 },
1369 CommandFlags::None,
1370 CommandHelper{},
1371 CommandCompleter{},
1372 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e22a02() 1373 {
1374 String message;
1375 if (auto quoting = parser.get_switch("quoting"))
1376 message = join(parser | transform(quoter(option_from_string(Meta::Type<Quoting>{}, *quoting))),
1377 ' ', false);
1378 else
1379 message = join(parser, ' ', false);
1380
1381 if (auto filename = parser.get_switch("to-file"))
1382 return write_to_file(*filename, message);
1383
1384 if (parser.get_switch("debug"))
1385 write_to_debug_buffer(message);
1386 else if (parser.get_switch("markup"))
1387 context.print_status(parse_display_line(message, context.faces()));
1388 else
1389 context.print_status({message, context.faces()["StatusLine"]});
1390 }
1391 };
1392
parse_keymap_mode(StringView str,const KeymapManager::UserModeList & user_modes)1393 KeymapMode parse_keymap_mode(StringView str, const KeymapManager::UserModeList& user_modes)
1394 {
1395 if (prefix_match("normal", str)) return KeymapMode::Normal;
1396 if (prefix_match("insert", str)) return KeymapMode::Insert;
1397 if (prefix_match("menu", str)) return KeymapMode::Menu;
1398 if (prefix_match("prompt", str)) return KeymapMode::Prompt;
1399 if (prefix_match("goto", str)) return KeymapMode::Goto;
1400 if (prefix_match("view", str)) return KeymapMode::View;
1401 if (prefix_match("user", str)) return KeymapMode::User;
1402 if (prefix_match("object", str)) return KeymapMode::Object;
1403
1404 auto it = find(user_modes, str);
1405 if (it == user_modes.end())
1406 throw runtime_error(format("no such keymap mode: '{}'", str));
1407
1408 char offset = static_cast<char>(KeymapMode::FirstUserMode);
1409 return (KeymapMode)(std::distance(user_modes.begin(), it) + offset);
1410 }
1411
1412 static constexpr auto modes = { "normal", "insert", "menu", "prompt", "goto", "view", "user", "object" };
1413
1414 const CommandDesc debug_cmd = {
1415 "debug",
1416 nullptr,
1417 "debug <command>: write some debug information to the *debug* buffer",
1418 ParameterDesc{{}, ParameterDesc::Flags::SwitchesOnlyAtStart, 1},
1419 CommandFlags::None,
1420 CommandHelper{},
1421 make_completer(
1422 [](const Context& context, CompletionFlags flags,
__anon866708e22b02() 1423 const String& prefix, ByteCount cursor_pos) -> Completions {
1424 auto c = {"info", "buffers", "options", "memory", "shared-strings",
1425 "profile-hash-maps", "faces", "mappings", "regex", "registers"};
1426 return { 0_byte, cursor_pos, complete(prefix, cursor_pos, c) };
1427 }),
1428 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e22c02() 1429 {
1430 if (parser[0] == "info")
1431 {
1432 write_to_debug_buffer(format("version: {}", version));
1433 write_to_debug_buffer(format("pid: {}", getpid()));
1434 write_to_debug_buffer(format("session: {}", Server::instance().session()));
1435 #ifdef KAK_DEBUG
1436 write_to_debug_buffer("build: debug");
1437 #else
1438 write_to_debug_buffer("build: release");
1439 #endif
1440 }
1441 else if (parser[0] == "buffers")
1442 {
1443 write_to_debug_buffer("Buffers:");
1444 for (auto& buffer : BufferManager::instance())
1445 write_to_debug_buffer(buffer->debug_description());
1446 }
1447 else if (parser[0] == "options")
1448 {
1449 write_to_debug_buffer("Options:");
1450 for (auto& option : context.options().flatten_options())
1451 write_to_debug_buffer(format(" * {}: {}", option->name(),
1452 option->get_as_string(Quoting::Kakoune)));
1453 }
1454 else if (parser[0] == "memory")
1455 {
1456 auto total = 0;
1457 write_to_debug_buffer("Memory usage:");
1458 const ColumnCount column_size = 13;
1459 write_to_debug_buffer(format("{} │{} │{} │{} ",
1460 left_pad("domain", column_size),
1461 left_pad("bytes", column_size),
1462 left_pad("active allocs", column_size),
1463 left_pad("total allocs", column_size)));
1464 write_to_debug_buffer(format("{0}┼{0}┼{0}┼{0}", String(Codepoint{0x2500}, column_size + 1)));
1465
1466 for (int domain = 0; domain < (int)MemoryDomain::Count; ++domain)
1467 {
1468 auto& stats = memory_stats[domain];
1469 total += stats.allocated_bytes;
1470 write_to_debug_buffer(format("{} │{} │{} │{} ",
1471 left_pad(domain_name((MemoryDomain)domain), column_size),
1472 left_pad(to_string(stats.allocated_bytes), column_size),
1473 left_pad(to_string(stats.allocation_count), column_size),
1474 left_pad(to_string(stats.total_allocation_count), column_size)));
1475 }
1476 write_to_debug_buffer({});
1477 write_to_debug_buffer(format(" Total: {}", total));
1478 #if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 33))
1479 write_to_debug_buffer(format(" Malloced: {}", mallinfo2().uordblks));
1480 #elif defined(__GLIBC__) || defined(__CYGWIN__)
1481 write_to_debug_buffer(format(" Malloced: {}", mallinfo().uordblks));
1482 #endif
1483 }
1484 else if (parser[0] == "shared-strings")
1485 {
1486 StringRegistry::instance().debug_stats();
1487 }
1488 else if (parser[0] == "profile-hash-maps")
1489 {
1490 profile_hash_maps();
1491 }
1492 else if (parser[0] == "faces")
1493 {
1494 write_to_debug_buffer("Faces:");
1495 for (auto& face : context.faces().flatten_faces())
1496 write_to_debug_buffer(format(" * {}: {}", face.key, face.value.face));
1497 }
1498 else if (parser[0] == "mappings")
1499 {
1500 auto& keymaps = context.keymaps();
1501 auto user_modes = keymaps.user_modes();
1502 write_to_debug_buffer("Mappings:");
1503 for (auto& mode : concatenated(modes, user_modes) | gather<Vector<String>>())
1504 {
1505 KeymapMode m = parse_keymap_mode(mode, user_modes);
1506 for (auto& key : keymaps.get_mapped_keys(m))
1507 write_to_debug_buffer(format(" * {} {}: {}",
1508 mode, key_to_str(key),
1509 keymaps.get_mapping(key, m).docstring));
1510 }
1511 }
1512 else if (parser[0] == "regex")
1513 {
1514 if (parser.positional_count() != 2)
1515 throw runtime_error("expected a regex");
1516
1517 write_to_debug_buffer(format(" * {}:\n{}",
1518 parser[1], dump_regex(compile_regex(parser[1], RegexCompileFlags::Optimize))));
1519 }
1520 else if (parser[0] == "registers")
1521 {
1522 write_to_debug_buffer("Register info:");
1523 for (auto&& [name, reg] : RegisterManager::instance())
1524 {
1525 auto content = reg->get(context);
1526
1527 if (content.size() == 1 and content[0] == "")
1528 continue;
1529
1530 write_to_debug_buffer(format(" * {} = {}\n", name,
1531 join(content | transform(quote), "\n = ")));
1532 }
1533 }
1534 else
1535 throw runtime_error(format("no such debug command: '{}'", parser[0]));
1536 }
1537 };
1538
1539 const CommandDesc source_cmd = {
1540 "source",
1541 nullptr,
1542 "source <filename> <params>...: execute commands contained in <filename>\n"
1543 "parameters are available in the sourced script as %arg{0}, %arg{1}, …",
1544 ParameterDesc{ {}, ParameterDesc::Flags::None, 1, (size_t)-1 },
1545 CommandFlags::None,
1546 CommandHelper{},
1547 filename_completer<true>,
1548 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e22d02() 1549 {
1550 const DebugFlags debug_flags = context.options()["debug"].get<DebugFlags>();
1551 const bool profile = debug_flags & DebugFlags::Profile;
1552 auto start_time = profile ? Clock::now() : Clock::time_point{};
1553
1554 String path = real_path(parse_filename(parser[0]));
1555 MappedFile file_content{path};
1556 try
1557 {
1558 auto params = parser | skip(1) | gather<Vector<String>>();
1559 CommandManager::instance().execute(file_content, context,
1560 {params, {{"source", path}}});
1561 }
1562 catch (Kakoune::runtime_error& err)
1563 {
1564 write_to_debug_buffer(format("{}:{}", parser[0], err.what()));
1565 throw;
1566 }
1567
1568 using namespace std::chrono;
1569 if (profile)
1570 write_to_debug_buffer(format("sourcing '{}' took {} us", parser[0],
1571 (size_t)duration_cast<microseconds>(Clock::now() - start_time).count()));
1572 }
1573 };
1574
option_doc_helper(const Context & context,CommandParameters params)1575 static String option_doc_helper(const Context& context, CommandParameters params)
1576 {
1577 const bool add = params.size() > 1 and params[0] == "-add";
1578 if (params.size() < 2 + (add ? 1 : 0))
1579 return "";
1580
1581 auto desc = GlobalScope::instance().option_registry().option_desc(params[1 + (add ? 1 : 0)]);
1582 if (not desc or desc->docstring().empty())
1583 return "";
1584
1585 return format("{}:\n{}", desc->name(), indent(desc->docstring()));
1586 }
1587
get_options(StringView scope,const Context & context,StringView option_name)1588 static OptionManager& get_options(StringView scope, const Context& context, StringView option_name)
1589 {
1590 if (scope == "current")
1591 return context.options()[option_name].manager();
1592 return get_scope(scope, context).options();
1593 }
1594
1595 const CommandDesc set_option_cmd = {
1596 "set-option",
1597 "set",
1598 "set-option [<switches>] <scope> <name> <value>: set option <name> in <scope> to <value>\n"
1599 "<scope> can be global, buffer, window, or current which refers to the narrowest "
1600 "scope the option is set in",
1601 ParameterDesc{
1602 { { "add", { false, "add to option rather than replacing it" } },
1603 { "remove", { false, "remove from option rather than replacing it" } } },
1604 ParameterDesc::Flags::SwitchesOnlyAtStart, 2, (size_t)-1
1605 },
1606 CommandFlags::None,
1607 option_doc_helper,
1608 [](const Context& context, CompletionFlags,
1609 CommandParameters params, size_t token_to_complete,
1610 ByteCount pos_in_token) -> Completions
__anon866708e22e02() 1611 {
1612 const bool add = params.size() > 1 and params[0] == "-add";
1613 const int start = add ? 1 : 0;
1614
1615 static constexpr auto scopes = { "global", "buffer", "window", "current" };
1616
1617 if (token_to_complete == start)
1618 return { 0_byte, params[start].length(),
1619 complete(params[start], pos_in_token, scopes) };
1620 else if (token_to_complete == start + 1)
1621 return { 0_byte, params[start + 1].length(),
1622 GlobalScope::instance().option_registry().complete_option_name(params[start + 1], pos_in_token) };
1623 else if (not add and token_to_complete == start + 2 and params[start + 2].empty() and
1624 GlobalScope::instance().option_registry().option_exists(params[start + 1]))
1625 {
1626 OptionManager& options = get_scope(params[start], context).options();
1627 return {0_byte, params[start + 2].length(),
1628 {options[params[start + 1]].get_as_string(Quoting::Kakoune)},
1629 Completions::Flags::Quoted};
1630 }
1631 return Completions{};
1632 },
1633 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e22f02() 1634 {
1635 bool add = (bool)parser.get_switch("add");
1636 bool remove = (bool)parser.get_switch("remove");
1637 if (add and remove)
1638 throw runtime_error("cannot add and remove at the same time");
1639
1640 Option& opt = get_options(parser[0], context, parser[1]).get_local_option(parser[1]);
1641 if (add)
1642 opt.add_from_strings(parser.positionals_from(2));
1643 else if (remove)
1644 opt.remove_from_strings(parser.positionals_from(2));
1645 else
1646 opt.set_from_strings(parser.positionals_from(2));
1647 }
1648 };
1649
complete_option(const Context & context,CompletionFlags,CommandParameters params,size_t token_to_complete,ByteCount pos_in_token)1650 Completions complete_option(const Context& context, CompletionFlags,
1651 CommandParameters params, size_t token_to_complete,
1652 ByteCount pos_in_token)
1653 {
1654 if (token_to_complete == 0)
1655 {
1656 static constexpr auto scopes = { "buffer", "window", "current" };
1657 return { 0_byte, params[0].length(), complete(params[0], pos_in_token, scopes) };
1658 }
1659 else if (token_to_complete == 1)
1660 return { 0_byte, params[1].length(),
1661 GlobalScope::instance().option_registry().complete_option_name(params[1], pos_in_token) };
1662 return Completions{};
1663 }
1664
1665 const CommandDesc unset_option_cmd = {
1666 "unset-option",
1667 "unset",
1668 "unset-option <scope> <name>: remove <name> option from scope, falling back on parent scope value\n"
1669 "<scope> can be buffer, window, or current which refers to the narrowest "
1670 "scope the option is set in",
1671 double_params,
1672 CommandFlags::None,
1673 option_doc_helper,
1674 complete_option,
1675 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e23002() 1676 {
1677 auto& options = get_options(parser[0], context, parser[1]);
1678 if (&options == &GlobalScope::instance().options())
1679 throw runtime_error("cannot unset options in global scope");
1680 options.unset_option(parser[1]);
1681 }
1682 };
1683
1684 const CommandDesc update_option_cmd = {
1685 "update-option",
1686 nullptr,
1687 "update-option <scope> <name>: update <name> option from scope\n"
1688 "some option types, such as line-specs or range-specs can be updated to latest buffer timestamp\n"
1689 "<scope> can be buffer, window, or current which refers to the narrowest "
1690 "scope the option is set in",
1691 double_params,
1692 CommandFlags::None,
1693 option_doc_helper,
1694 complete_option,
1695 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e23102() 1696 {
1697 Option& opt = get_options(parser[0], context, parser[1]).get_local_option(parser[1]);
1698 opt.update(context);
1699 }
1700 };
1701
1702 const CommandDesc declare_option_cmd = {
1703 "declare-option",
1704 "decl",
1705 "declare-option [<switches>] <type> <name> [value]: declare option <name> of type <type>.\n"
1706 "set its initial value to <value> if given and the option did not exist\n"
1707 "Available types:\n"
1708 " int: integer\n"
1709 " bool: boolean (true/false or yes/no)\n"
1710 " str: character string\n"
1711 " regex: regular expression\n"
1712 " int-list: list of integers\n"
1713 " str-list: list of character strings\n"
1714 " completions: list of completion candidates\n"
1715 " line-specs: list of line specs\n"
1716 " range-specs: list of range specs\n"
1717 " str-to-str-map: map from strings to strings\n",
1718 ParameterDesc{
1719 { { "hidden", { false, "do not display option name when completing" } },
1720 { "docstring", { true, "specify option description" } } },
1721 ParameterDesc::Flags::SwitchesOnlyAtStart, 2, (size_t)-1
1722 },
1723 CommandFlags::None,
1724 CommandHelper{},
1725 make_completer(
1726 [](const Context& context, CompletionFlags flags,
__anon866708e23202() 1727 const String& prefix, ByteCount cursor_pos) -> Completions {
1728 auto c = {"int", "bool", "str", "regex", "int-list", "str-list", "completions", "line-specs", "range-specs", "str-to-str-map"};
1729 return { 0_byte, cursor_pos, complete(prefix, cursor_pos, c) };
1730 }),
1731 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e23302() 1732 {
1733 Option* opt = nullptr;
1734
1735 OptionFlags flags = OptionFlags::None;
1736 if (parser.get_switch("hidden"))
1737 flags = OptionFlags::Hidden;
1738
1739 auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{}));
1740 OptionsRegistry& reg = GlobalScope::instance().option_registry();
1741
1742
1743 if (parser[0] == "int")
1744 opt = ®.declare_option<int>(parser[1], docstring, 0, flags);
1745 else if (parser[0] == "bool")
1746 opt = ®.declare_option<bool>(parser[1], docstring, false, flags);
1747 else if (parser[0] == "str")
1748 opt = ®.declare_option<String>(parser[1], docstring, "", flags);
1749 else if (parser[0] == "regex")
1750 opt = ®.declare_option<Regex>(parser[1], docstring, Regex{}, flags);
1751 else if (parser[0] == "int-list")
1752 opt = ®.declare_option<Vector<int, MemoryDomain::Options>>(parser[1], docstring, {}, flags);
1753 else if (parser[0] == "str-list")
1754 opt = ®.declare_option<Vector<String, MemoryDomain::Options>>(parser[1], docstring, {}, flags);
1755 else if (parser[0] == "completions")
1756 opt = ®.declare_option<CompletionList>(parser[1], docstring, {}, flags);
1757 else if (parser[0] == "line-specs")
1758 opt = ®.declare_option<TimestampedList<LineAndSpec>>(parser[1], docstring, {}, flags);
1759 else if (parser[0] == "range-specs")
1760 opt = ®.declare_option<TimestampedList<RangeAndString>>(parser[1], docstring, {}, flags);
1761 else if (parser[0] == "str-to-str-map")
1762 opt = ®.declare_option<HashMap<String, String, MemoryDomain::Options>>(parser[1], docstring, {}, flags);
1763 else
1764 throw runtime_error(format("no such option type: '{}'", parser[0]));
1765
1766 if (parser.positional_count() > 2)
1767 opt->set_from_strings(parser.positionals_from(2));
1768 }
1769 };
1770
1771 template<bool unmap>
map_key_completer(const Context & context,CompletionFlags flags,CommandParameters params,size_t token_to_complete,ByteCount pos_in_token)1772 static Completions map_key_completer(const Context& context, CompletionFlags flags,
1773 CommandParameters params, size_t token_to_complete,
1774 ByteCount pos_in_token)
1775 {
1776 if (token_to_complete == 0)
1777 return { 0_byte, params[0].length(),
1778 complete(params[0], pos_in_token, scopes) };
1779 if (token_to_complete == 1)
1780 {
1781 auto& user_modes = get_scope(params[0], context).keymaps().user_modes();
1782 return { 0_byte, params[1].length(),
1783 complete(params[1], pos_in_token, concatenated(modes, user_modes) | gather<Vector<String>>()) };
1784 }
1785 if (unmap and token_to_complete == 2)
1786 {
1787 KeymapManager& keymaps = get_scope(params[0], context).keymaps();
1788 KeymapMode keymap_mode = parse_keymap_mode(params[1], keymaps.user_modes());
1789 KeyList keys = keymaps.get_mapped_keys(keymap_mode);
1790
1791 return { 0_byte, params[2].length(),
1792 complete(params[2], pos_in_token,
1793 keys | transform([](Key k) { return key_to_str(k); })
1794 | gather<Vector<String>>()) };
1795 }
1796 return {};
1797 }
1798
1799 const CommandDesc map_key_cmd = {
1800 "map",
1801 nullptr,
1802 "map [<switches>] <scope> <mode> <key> <keys>: map <key> to <keys> in given <mode> in <scope>",
1803 ParameterDesc{
1804 { { "docstring", { true, "specify mapping description" } } },
1805 ParameterDesc::Flags::None, 4, 4
1806 },
1807 CommandFlags::None,
1808 CommandHelper{},
1809 map_key_completer<false>,
1810 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e23502() 1811 {
1812 KeymapManager& keymaps = get_scope(parser[0], context).keymaps();
1813 KeymapMode keymap_mode = parse_keymap_mode(parser[1], keymaps.user_modes());
1814
1815 KeyList key = parse_keys(parser[2]);
1816 if (key.size() != 1)
1817 throw runtime_error("only a single key can be mapped");
1818
1819 KeyList mapping = parse_keys(parser[3]);
1820 keymaps.map_key(key[0], keymap_mode, std::move(mapping),
1821 trim_indent(parser.get_switch("docstring").value_or("")));
1822 }
1823 };
1824
1825 const CommandDesc unmap_key_cmd = {
1826 "unmap",
1827 nullptr,
1828 "unmap <scope> <mode> [<key> [<expected-keys>]]: unmap <key> from given <mode> in <scope>.\n"
1829 "If <expected-keys> is specified, remove the mapping only if its value is <expected-keys>.\n"
1830 "If only <scope> and <mode> are specified remove all mappings",
1831 ParameterDesc{{}, ParameterDesc::Flags::None, 2, 4},
1832 CommandFlags::None,
1833 CommandHelper{},
1834 map_key_completer<true>,
1835 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e23602() 1836 {
1837 KeymapManager& keymaps = get_scope(parser[0], context).keymaps();
1838 KeymapMode keymap_mode = parse_keymap_mode(parser[1], keymaps.user_modes());
1839
1840 if (parser.positional_count() == 2)
1841 {
1842 keymaps.unmap_keys(keymap_mode);
1843 return;
1844 }
1845
1846 KeyList key = parse_keys(parser[2]);
1847 if (key.size() != 1)
1848 throw runtime_error("only a single key can be unmapped");
1849
1850 if (keymaps.is_mapped(key[0], keymap_mode) and
1851 (parser.positional_count() < 4 or
1852 (keymaps.get_mapping(key[0], keymap_mode).keys ==
1853 parse_keys(parser[3]))))
1854 keymaps.unmap_key(key[0], keymap_mode);
1855 }
1856 };
1857
1858 template<size_t... P>
make_context_wrap_params_impl(Array<HashItem<String,SwitchDesc>,sizeof...(P)> && additional_params,std::index_sequence<P...>)1859 ParameterDesc make_context_wrap_params_impl(Array<HashItem<String, SwitchDesc>, sizeof...(P)>&& additional_params,
1860 std::index_sequence<P...>)
1861 {
1862 return { { { "client", { true, "run in given client context" } },
1863 { "try-client", { true, "run in given client context if it exists, or else in the current one" } },
1864 { "buffer", { true, "run in a disposable context for each given buffer in the comma separated list argument" } },
1865 { "draft", { false, "run in a disposable context" } },
1866 { "itersel", { false, "run once for each selection with that selection as the only one" } },
1867 { "save-regs", { true, "restore all given registers after execution" } },
1868 std::move(additional_params[P])...},
1869 ParameterDesc::Flags::SwitchesOnlyAtStart, 1
1870 };
1871 }
1872
1873 template<size_t N>
make_context_wrap_params(Array<HashItem<String,SwitchDesc>,N> && additional_params)1874 ParameterDesc make_context_wrap_params(Array<HashItem<String, SwitchDesc>, N>&& additional_params)
1875 {
1876 return make_context_wrap_params_impl(std::move(additional_params), std::make_index_sequence<N>());
1877 }
1878
1879 template<typename Func>
context_wrap(const ParametersParser & parser,Context & context,StringView default_saved_regs,Func func)1880 void context_wrap(const ParametersParser& parser, Context& context, StringView default_saved_regs, Func func)
1881 {
1882 if ((int)(bool)parser.get_switch("buffer") +
1883 (int)(bool)parser.get_switch("client") +
1884 (int)(bool)parser.get_switch("try-client") > 1)
1885 throw runtime_error{"only one of -buffer, -client or -try-client can be specified"};
1886
1887 const auto& register_manager = RegisterManager::instance();
1888 auto make_register_restorer = [&](char c) {
1889 auto& reg = register_manager[c];
1890 return on_scope_end([&, c, save=reg.save(context), d=ScopedSetBool{reg.modified_hook_disabled()}] {
1891 try
1892 {
1893 reg.restore(context, save);
1894 }
1895 catch (runtime_error& err)
1896 {
1897 write_to_debug_buffer(format("failed to restore register '{}': {}", c, err.what()));
1898 }
1899 });
1900 };
1901 Vector<decltype(make_register_restorer(0))> saved_registers;
1902 for (auto c : parser.get_switch("save-regs").value_or(default_saved_regs))
1903 saved_registers.push_back(make_register_restorer(c));
1904
1905 if (auto bufnames = parser.get_switch("buffer"))
1906 {
1907 auto context_wrap_for_buffer = [&](Buffer& buffer) {
1908 InputHandler input_handler{{ buffer, Selection{} },
1909 Context::Flags::Draft};
1910 Context& c = input_handler.context();
1911
1912 ScopedSetBool disable_history(c.history_disabled());
1913
1914 func(parser, c);
1915 };
1916 if (*bufnames == "*")
1917 {
1918 for (auto&& buffer : BufferManager::instance()
1919 | transform(&std::unique_ptr<Buffer>::get)
1920 | filter([](Buffer* buf) { return not (buf->flags() & Buffer::Flags::Debug); })
1921 | gather<Vector<SafePtr<Buffer>>>()) // gather as we might be mutating the buffer list in the loop.
1922 context_wrap_for_buffer(*buffer);
1923 }
1924 else
1925 for (auto&& name : *bufnames | split<StringView>(','))
1926 context_wrap_for_buffer(BufferManager::instance().get_buffer(name));
1927 return;
1928 }
1929
1930 ClientManager& cm = ClientManager::instance();
1931 Context* base_context = &context;
1932 if (auto client_name = parser.get_switch("client"))
1933 base_context = &cm.get_client(*client_name).context();
1934 else if (auto client_name = parser.get_switch("try-client"))
1935 {
1936 if (Client* client = cm.get_client_ifp(*client_name))
1937 base_context = &client->context();
1938 }
1939
1940 Optional<InputHandler> input_handler;
1941 Context* effective_context = base_context;
1942
1943 const bool draft = (bool)parser.get_switch("draft");
1944 if (draft)
1945 {
1946 input_handler.emplace(base_context->selections(),
1947 Context::Flags::Draft,
1948 base_context->name());
1949 effective_context = &input_handler->context();
1950
1951 // Preserve window so that window scope is available
1952 if (base_context->has_window())
1953 effective_context->set_window(base_context->window());
1954
1955 // We do not want this draft context to commit undo groups if the real one is
1956 // going to commit the whole thing later
1957 if (base_context->is_editing())
1958 effective_context->disable_undo_handling();
1959 }
1960
1961 Context& c = *effective_context;
1962
1963 ScopedSetBool disable_history(c.history_disabled());
1964 ScopedEdition edition{c};
1965
1966 if (parser.get_switch("itersel"))
1967 {
1968 SelectionList sels{base_context->selections()};
1969 Vector<Selection> new_sels;
1970 size_t main = 0;
1971 size_t timestamp = c.buffer().timestamp();
1972 bool one_selection_succeeded = false;
1973 for (auto& sel : sels)
1974 {
1975 c.selections_write_only() = SelectionList{sels.buffer(), sel, sels.timestamp()};
1976 c.selections().update();
1977
1978 try
1979 {
1980 func(parser, c);
1981 one_selection_succeeded = true;
1982
1983 if (&sels.buffer() != &c.buffer())
1984 throw runtime_error("buffer has changed while iterating on selections");
1985
1986 if (not draft)
1987 {
1988 update_selections(new_sels, main, c.buffer(), timestamp);
1989 timestamp = c.buffer().timestamp();
1990 if (&sel == &sels.main())
1991 main = new_sels.size() + c.selections().main_index();
1992
1993 const auto middle = new_sels.insert(new_sels.end(), c.selections().begin(), c.selections().end());
1994 std::inplace_merge(new_sels.begin(), middle, new_sels.end(), compare_selections);
1995 }
1996 }
1997 catch (no_selections_remaining&) {}
1998 }
1999
2000 if (not one_selection_succeeded)
2001 {
2002 c.selections_write_only() = std::move(sels);
2003 throw no_selections_remaining{};
2004 }
2005
2006 if (not draft)
2007 c.selections_write_only().set(std::move(new_sels), main);
2008 }
2009 else
2010 {
2011 const bool collapse_jumps = not (c.flags() & Context::Flags::Draft) and context.has_buffer();
2012 auto& jump_list = c.jump_list();
2013 const size_t prev_index = jump_list.current_index();
2014 auto jump = collapse_jumps ? c.selections() : Optional<SelectionList>{};
2015
2016 func(parser, c);
2017
2018 // If the jump list got mutated, collapse all jumps into a single one from original selections
2019 if (auto index = jump_list.current_index();
2020 collapse_jumps and index > prev_index and
2021 contains(BufferManager::instance(), &jump->buffer()))
2022 jump_list.push(std::move(*jump), prev_index);
2023 }
2024 }
2025
2026 const CommandDesc execute_keys_cmd = {
2027 "execute-keys",
2028 "exec",
2029 "execute-keys [<switches>] <keys>: execute given keys as if entered by user",
2030 make_context_wrap_params<2>({{
2031 {"with-maps", {false, "use user defined key mapping when executing keys"}},
2032 {"with-hooks", {false, "trigger hooks while executing keys"}}
2033 }}),
2034 CommandFlags::None,
2035 CommandHelper{},
2036 CommandCompleter{},
2037 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e23b02() 2038 {
2039 context_wrap(parser, context, "/\"|^@:", [](const ParametersParser& parser, Context& context) {
2040 ScopedSetBool disable_keymaps(context.keymaps_disabled(), not parser.get_switch("with-maps"));
2041 ScopedSetBool disable_hoooks(context.hooks_disabled(), not parser.get_switch("with-hooks"));
2042
2043 KeyList keys;
2044 for (auto& param : parser)
2045 {
2046 KeyList param_keys = parse_keys(param);
2047 keys.insert(keys.end(), param_keys.begin(), param_keys.end());
2048 }
2049
2050 for (auto& key : keys)
2051 context.input_handler().handle_key(key);
2052 });
2053 }
2054 };
2055
2056 const CommandDesc evaluate_commands_cmd = {
2057 "evaluate-commands",
2058 "eval",
2059 "evaluate-commands [<switches>] <commands>...: execute commands as if entered by user",
2060 make_context_wrap_params<2>({{
2061 {"no-hooks", { false, "disable hooks while executing commands" }},
2062 {"verbatim", { false, "do not reparse argument" }}
2063 }}),
2064 CommandFlags::None,
2065 CommandHelper{},
2066 CommandCompleter{},
2067 [](const ParametersParser& parser, Context& context, const ShellContext& shell_context)
__anon866708e23d02() 2068 {
2069 context_wrap(parser, context, {}, [&](const ParametersParser& parser, Context& context) {
2070 const bool no_hooks = context.hooks_disabled() or parser.get_switch("no-hooks");
2071 ScopedSetBool disable_hoooks(context.hooks_disabled(), no_hooks);
2072
2073 if (parser.get_switch("verbatim"))
2074 CommandManager::instance().execute_single_command(parser | gather<Vector>(), context, shell_context);
2075 else
2076 CommandManager::instance().execute(join(parser, ' ', false), context, shell_context);
2077 });
2078 }
2079 };
2080
2081 struct CapturedShellContext
2082 {
CapturedShellContextKakoune::__anon866708e20111::CapturedShellContext2083 explicit CapturedShellContext(const ShellContext& sc)
2084 : params{sc.params.begin(), sc.params.end()}, env_vars{sc.env_vars} {}
2085
2086 Vector<String> params;
2087 EnvVarMap env_vars;
2088
operator ShellContextKakoune::__anon866708e20111::CapturedShellContext2089 operator ShellContext() const { return { params, env_vars }; }
2090 };
2091
2092 const CommandDesc prompt_cmd = {
2093 "prompt",
2094 nullptr,
2095 "prompt [<switches>] <prompt> <command>: prompt the user to enter a text string "
2096 "and then executes <command>, entered text is available in the 'text' value",
2097 ParameterDesc{
2098 { { "init", { true, "set initial prompt content" } },
2099 { "password", { false, "Do not display entered text and clear reg after command" } },
2100 { "file-completion", { false, "use file completion for prompt" } },
2101 { "client-completion", { false, "use client completion for prompt" } },
2102 { "buffer-completion", { false, "use buffer completion for prompt" } },
2103 { "command-completion", { false, "use command completion for prompt" } },
2104 { "shell-completion", { false, "use shell command completion for prompt" } },
2105 { "shell-script-completion", { true, "use shell command completion for prompt" } },
2106 { "shell-script-candidates", { true, "use shell command completion for prompt" } },
2107 { "on-change", { true, "command to execute whenever the prompt changes" } },
2108 { "on-abort", { true, "command to execute whenever the prompt is canceled" } } },
2109 ParameterDesc::Flags::None, 2, 2
2110 },
2111 CommandFlags::None,
2112 CommandHelper{},
2113 CommandCompleter{},
2114 [](const ParametersParser& parser, Context& context, const ShellContext& shell_context)
__anon866708e23f02() 2115 {
2116 const String& command = parser[1];
2117 auto initstr = parser.get_switch("init").value_or(StringView{});
2118
2119 PromptCompleter completer;
2120 if (parser.get_switch("file-completion"))
2121 completer = [](const Context& context, CompletionFlags,
2122 StringView prefix, ByteCount cursor_pos) -> Completions {
2123 auto& ignored_files = context.options()["ignored_files"].get<Regex>();
2124 return { 0_byte, cursor_pos,
2125 complete_filename(prefix, ignored_files, cursor_pos,
2126 FilenameFlags::Expand) };
2127 };
2128 else if (parser.get_switch("client-completion"))
2129 completer = [](const Context& context, CompletionFlags,
2130 StringView prefix, ByteCount cursor_pos) -> Completions {
2131 return { 0_byte, cursor_pos,
2132 ClientManager::instance().complete_client_name(prefix, cursor_pos) };
2133 };
2134 else if (parser.get_switch("buffer-completion"))
2135 completer = complete_buffer_name<false>;
2136 else if (parser.get_switch("command-completion"))
2137 completer = [](const Context& context, CompletionFlags flags,
2138 StringView prefix, ByteCount cursor_pos) -> Completions {
2139 return CommandManager::instance().complete(
2140 context, flags, prefix, cursor_pos);
2141 };
2142 else if (parser.get_switch("shell-completion"))
2143 completer = shell_complete;
2144 else if (auto shell_script = parser.get_switch("shell-script-completion"))
2145 completer = PromptCompleterAdapter{ShellScriptCompleter{shell_script->str()}};
2146 else if (auto shell_script = parser.get_switch("shell-script-candidates"))
2147 completer = PromptCompleterAdapter{ShellCandidatesCompleter{shell_script->str()}};
2148
2149 const auto flags = parser.get_switch("password") ?
2150 PromptFlags::Password : PromptFlags::None;
2151
2152 context.input_handler().prompt(
2153 parser[0], initstr.str(), {}, context.faces()["Prompt"],
2154 flags, '_', std::move(completer),
2155 [command,
2156 on_change = parser.get_switch("on-change").value_or("").str(),
2157 on_abort = parser.get_switch("on-abort").value_or("").str(),
2158 sc = CapturedShellContext{shell_context}]
2159 (StringView str, PromptEvent event, Context& context) mutable
2160 {
2161 if ((event == PromptEvent::Abort and on_abort.empty()) or
2162 (event == PromptEvent::Change and on_change.empty()))
2163 return;
2164
2165 sc.env_vars["text"_sv] = String{String::NoCopy{}, str};
2166 auto remove_text = on_scope_end([&] {
2167 sc.env_vars.erase("text"_sv);
2168 });
2169
2170 ScopedSetBool disable_history{context.history_disabled()};
2171
2172 StringView cmd;
2173 switch (event)
2174 {
2175 case PromptEvent::Validate: cmd = command; break;
2176 case PromptEvent::Change: cmd = on_change; break;
2177 case PromptEvent::Abort: cmd = on_abort; break;
2178 }
2179 try
2180 {
2181 CommandManager::instance().execute(cmd, context, sc);
2182 }
2183 catch (Kakoune::runtime_error& error)
2184 {
2185 context.print_status({error.what().str(), context.faces()["Error"]});
2186 context.hooks().run_hook(Hook::RuntimeError, error.what(), context);
2187 }
2188 });
2189 }
2190 };
2191
2192 const CommandDesc menu_cmd = {
2193 "menu",
2194 nullptr,
2195 "menu [<switches>] <name1> <commands1> <name2> <commands2>...: display a "
2196 "menu and execute commands for the selected item",
2197 ParameterDesc{
2198 { { "auto-single", { false, "instantly validate if only one item is available" } },
2199 { "select-cmds", { false, "each item specify an additional command to run when selected" } },
2200 { "markup", { false, "parse menu entries as markup text" } } }
2201 },
2202 CommandFlags::None,
2203 CommandHelper{},
2204 CommandCompleter{},
2205 [](const ParametersParser& parser, Context& context, const ShellContext& shell_context)
__anon866708e24502() 2206 {
2207 const bool with_select_cmds = (bool)parser.get_switch("select-cmds");
2208 const bool markup = (bool)parser.get_switch("markup");
2209 const size_t modulo = with_select_cmds ? 3 : 2;
2210
2211 const size_t count = parser.positional_count();
2212 if (count == 0 or (count % modulo) != 0)
2213 throw wrong_argument_count();
2214
2215 if (count == modulo and parser.get_switch("auto-single"))
2216 {
2217 ScopedSetBool disable_history{context.history_disabled()};
2218
2219 CommandManager::instance().execute(parser[1], context);
2220 return;
2221 }
2222
2223 Vector<DisplayLine> choices;
2224 Vector<String> commands;
2225 Vector<String> select_cmds;
2226 for (int i = 0; i < count; i += modulo)
2227 {
2228 if (parser[i].empty())
2229 throw runtime_error(format("entry #{} is empty", i+1));
2230
2231 choices.push_back(markup ? parse_display_line(parser[i], context.faces())
2232 : DisplayLine{ parser[i], {} });
2233 commands.push_back(parser[i+1]);
2234 if (with_select_cmds)
2235 select_cmds.push_back(parser[i+2]);
2236 }
2237
2238 CapturedShellContext sc{shell_context};
2239 context.input_handler().menu(std::move(choices),
2240 [=](int choice, MenuEvent event, Context& context) {
2241 ScopedSetBool disable_history{context.history_disabled()};
2242
2243 if (event == MenuEvent::Validate and choice >= 0 and choice < commands.size())
2244 CommandManager::instance().execute(commands[choice], context, sc);
2245 if (event == MenuEvent::Select and choice >= 0 and choice < select_cmds.size())
2246 CommandManager::instance().execute(select_cmds[choice], context, sc);
2247 });
2248 }
2249 };
2250
2251 const CommandDesc on_key_cmd = {
2252 "on-key",
2253 nullptr,
2254 "on-key [<switches>] <command>: wait for next user key and then execute <command>, "
2255 "with key available in the `key` value",
2256 ParameterDesc{
2257 { { "mode-name", { true, "set mode name to use" } } },
2258 ParameterDesc::Flags::None, 1, 1
2259 },
2260 CommandFlags::None,
2261 CommandHelper{},
2262 CommandCompleter{},
2263 [](const ParametersParser& parser, Context& context, const ShellContext& shell_context)
__anon866708e24702() 2264 {
2265 String command = parser[0];
2266
2267 CapturedShellContext sc{shell_context};
2268 context.input_handler().on_next_key(
2269 parser.get_switch("mode-name").value_or("on-key"),
2270 KeymapMode::None, [=](Key key, Context& context) mutable {
2271 sc.env_vars["key"_sv] = key_to_str(key);
2272 ScopedSetBool disable_history{context.history_disabled()};
2273
2274 CommandManager::instance().execute(command, context, sc);
2275 });
2276 }
2277 };
2278
2279 const CommandDesc info_cmd = {
2280 "info",
2281 nullptr,
2282 "info [<switches>] <text>: display an info box containing <text>",
2283 ParameterDesc{
2284 { { "anchor", { true, "set info anchoring <line>.<column>" } },
2285 { "style", { true, "set info style (above, below, menu, modal)" } },
2286 { "markup", { false, "parse markup" } },
2287 { "title", { true, "set info title" } } },
2288 ParameterDesc::Flags::None, 0, 1
2289 },
2290 CommandFlags::None,
2291 CommandHelper{},
2292 CommandCompleter{},
2293 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e24902() 2294 {
2295 if (not context.has_client())
2296 return;
2297
2298 const InfoStyle style = parser.get_switch("style").map(
2299 [](StringView style) -> Optional<InfoStyle> {
2300 if (style == "above") return InfoStyle::InlineAbove;
2301 if (style == "below") return InfoStyle::InlineBelow;
2302 if (style == "menu") return InfoStyle::MenuDoc;
2303 if (style == "modal") return InfoStyle::Modal;
2304 throw runtime_error(format("invalid style: '{}'", style));
2305 }).value_or(parser.get_switch("anchor") ? InfoStyle::Inline : InfoStyle::Prompt);
2306
2307 context.client().info_hide(style == InfoStyle::Modal);
2308 if (parser.positional_count() == 0)
2309 return;
2310
2311 const BufferCoord pos = parser.get_switch("anchor").map(
2312 [](StringView anchor) {
2313 auto dot = find(anchor, '.');
2314 if (dot == anchor.end())
2315 throw runtime_error("expected <line>.<column> for anchor");
2316
2317 return BufferCoord{str_to_int({anchor.begin(), dot})-1,
2318 str_to_int({dot+1, anchor.end()})-1};
2319 }).value_or(BufferCoord{});
2320
2321 auto title = parser.get_switch("title").value_or(StringView{});
2322 if (parser.get_switch("markup"))
2323 context.client().info_show(parse_display_line(title, context.faces()),
2324 parse_display_line_list(parser[0], context.faces()),
2325 pos, style);
2326 else
2327 context.client().info_show(title.str(), parser[0], pos, style);
2328 }
2329 };
2330
2331 const CommandDesc try_catch_cmd = {
2332 "try",
2333 nullptr,
2334 "try <cmds> [catch <error_cmds>]...: execute <cmds> in current context.\n"
2335 "if an error is raised and <error_cmds> is specified, execute it and do\n"
2336 "not propagate that error. If <error_cmds> raises an error and another\n"
2337 "<error_cmds> is provided, execute this one and so-on\n",
2338 ParameterDesc{{}, ParameterDesc::Flags::None, 1},
2339 CommandFlags::None,
2340 CommandHelper{},
2341 CommandCompleter{},
2342 [](const ParametersParser& parser, Context& context, const ShellContext& shell_context)
__anon866708e24c02() 2343 {
2344 if ((parser.positional_count() % 2) != 1)
2345 throw wrong_argument_count();
2346
2347 for (size_t i = 1; i < parser.positional_count(); i += 2)
2348 {
2349 if (parser[i] != "catch")
2350 throw runtime_error("usage: try <commands> [catch <on error commands>]...");
2351 }
2352
2353 CommandManager& command_manager = CommandManager::instance();
2354 Optional<ShellContext> shell_context_with_error;
2355 for (size_t i = 0; i < parser.positional_count(); i += 2)
2356 {
2357 if (i == 0 or i < parser.positional_count() - 1)
2358 {
2359 try
2360 {
2361 command_manager.execute(parser[i], context,
2362 shell_context_with_error.value_or(shell_context));
2363 return;
2364 }
2365 catch (const runtime_error& error)
2366 {
2367 shell_context_with_error.emplace(shell_context);
2368 shell_context_with_error->env_vars[StringView{"error"}] = error.what().str();
2369 }
2370 }
2371 else
2372 command_manager.execute(parser[i], context,
2373 shell_context_with_error.value_or(shell_context));
2374 }
2375 }
2376 };
2377
complete_face(const Context & context,CompletionFlags flags,const String & prefix,ByteCount cursor_pos)2378 static Completions complete_face(const Context& context, CompletionFlags flags,
2379 const String& prefix, ByteCount cursor_pos)
2380 {
2381 return {0_byte, cursor_pos,
2382 complete(prefix, cursor_pos, context.faces().flatten_faces() |
2383 transform([](auto& entry) -> const String& { return entry.key; }))};
2384 }
2385
face_doc_helper(const Context & context,CommandParameters params)2386 static String face_doc_helper(const Context& context, CommandParameters params)
2387 {
2388 if (params.size() < 2)
2389 return {};
2390 try
2391 {
2392 auto face = context.faces()[params[1]];
2393 return format("{}:\n{}", params[1], indent(to_string(face)));
2394 }
2395 catch (runtime_error&)
2396 {
2397 return {};
2398 }
2399 }
2400
2401 const CommandDesc set_face_cmd = {
2402 "set-face",
2403 "face",
2404 "set-face <scope> <name> <facespec>: set face <name> to <facespec> in <scope>\n"
2405 "\n"
2406 "facespec format is:\n"
2407 " <fg color>[,<bg color>[,<underline color>]][+<attributes>][@<base>]\n"
2408 "colors are either a color name, rgb:######, or rgba:######## values.\n"
2409 "attributes is a combination of:\n"
2410 " u: underline, c: curly underline, i: italic, b: bold,\n"
2411 " r: reverse, s: strikethrough, B: blink, d: dim,\n"
2412 " f: final foreground, g: final background,\n"
2413 " a: final attributes, F: same as +fga\n"
2414 "facespec can as well just be the name of another face.\n"
2415 "if a base face is specified, colors and attributes are applied on top of it",
2416 ParameterDesc{{}, ParameterDesc::Flags::None, 3, 3},
2417 CommandFlags::None,
2418 face_doc_helper,
2419 make_completer(complete_scope, complete_face, complete_face),
2420 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e24e02() 2421 {
2422 get_scope(parser[0], context).faces().add_face(parser[1], parser[2], true);
2423
2424 for (auto& client : ClientManager::instance())
2425 client->force_redraw();
2426 }
2427 };
2428
2429 const CommandDesc unset_face_cmd = {
2430 "unset-face",
2431 nullptr,
2432 "unset-face <scope> <name>: remove <face> from <scope>",
2433 double_params,
2434 CommandFlags::None,
2435 face_doc_helper,
2436 make_completer(complete_scope, complete_face),
2437 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e24f02() 2438 {
2439 get_scope(parser[0], context).faces().remove_face(parser[1]);
2440 }
2441 };
2442
2443 const CommandDesc rename_client_cmd = {
2444 "rename-client",
2445 nullptr,
2446 "rename-client <name>: set current client name to <name>",
2447 single_param,
2448 CommandFlags::None,
2449 CommandHelper{},
__anon866708e25002()2450 make_single_word_completer([](const Context& context){ return context.name(); }),
2451 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e25102() 2452 {
2453 const String& name = parser[0];
2454 if (not all_of(name, is_identifier))
2455 throw runtime_error{format("invalid client name: '{}'", name)};
2456 else if (ClientManager::instance().client_name_exists(name) and
2457 context.name() != name)
2458 throw runtime_error{format("client name '{}' is not unique", name)};
2459 else
2460 context.set_name(name);
2461 }
2462 };
2463
2464 const CommandDesc set_register_cmd = {
2465 "set-register",
2466 "reg",
2467 "set-register <name> <values>...: set register <name> to <values>",
2468 ParameterDesc{{}, ParameterDesc::Flags::SwitchesAsPositional, 1},
2469 CommandFlags::None,
2470 CommandHelper{},
2471 make_completer(
2472 [](const Context& context, CompletionFlags flags,
__anon866708e25202() 2473 const String& prefix, ByteCount cursor_pos) -> Completions {
2474 return { 0_byte, cursor_pos,
2475 RegisterManager::instance().complete_register_name(prefix, cursor_pos) };
2476 }),
2477 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e25302() 2478 {
2479 RegisterManager::instance()[parser[0]].set(context, parser.positionals_from(1));
2480 }
2481 };
2482
2483 const CommandDesc select_cmd = {
2484 "select",
2485 nullptr,
2486 "select <selection_desc>...: select given selections\n"
2487 "\n"
2488 "selection_desc format is <anchor_line>.<anchor_column>,<cursor_line>.<cursor_column>",
2489 ParameterDesc{{
2490 {"timestamp", {true, "specify buffer timestamp at which those selections are valid"}},
2491 {"codepoint", {false, "columns are specified in codepoints, not bytes"}},
2492 {"display-column", {false, "columns are specified in display columns, not bytes"}}
2493 },
2494 ParameterDesc::Flags::SwitchesOnlyAtStart, 1
2495 },
2496 CommandFlags::None,
2497 CommandHelper{},
2498 CommandCompleter{},
2499 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e25402() 2500 {
2501 auto& buffer = context.buffer();
2502 const size_t timestamp = parser.get_switch("timestamp").map(str_to_int_ifp).cast<size_t>().value_or(buffer.timestamp());
2503 ColumnType column_type = ColumnType::Byte;
2504 if (parser.get_switch("codepoint"))
2505 column_type = ColumnType::Codepoint;
2506 else if (parser.get_switch("display-column"))
2507 column_type = ColumnType::DisplayColumn;
2508 ColumnCount tabstop = context.options()["tabstop"].get<int>();
2509 context.selections_write_only() = selection_list_from_strings(buffer, column_type, parser.positionals_from(0), timestamp, 0, tabstop);
2510 }
2511 };
2512
2513 const CommandDesc change_directory_cmd = {
2514 "change-directory",
2515 "cd",
2516 "change-directory [<directory>]: change the server's working directory to <directory>, or the home directory if unspecified",
2517 single_optional_param,
2518 CommandFlags::None,
2519 CommandHelper{},
2520 make_completer(
2521 [](const Context& context, CompletionFlags flags,
__anon866708e25502() 2522 const String& prefix, ByteCount cursor_pos) -> Completions {
2523 return { 0_byte, cursor_pos,
2524 complete_filename(prefix,
2525 context.options()["ignored_files"].get<Regex>(),
2526 cursor_pos, FilenameFlags::OnlyDirectories) };
2527 }),
2528 [](const ParametersParser& parser, Context&, const ShellContext&)
__anon866708e25602() 2529 {
2530 StringView target = parser.positional_count() == 1 ? StringView{parser[0]} : "~";
2531 if (chdir(parse_filename(target).c_str()) != 0)
2532 throw runtime_error(format("unable to change to directory: '{}'", target));
2533 for (auto& buffer : BufferManager::instance())
2534 buffer->update_display_name();
2535 }
2536 };
2537
2538 const CommandDesc rename_session_cmd = {
2539 "rename-session",
2540 nullptr,
2541 "rename-session <name>: change remote session name",
2542 single_param,
2543 CommandFlags::None,
2544 CommandHelper{},
__anon866708e25702()2545 make_single_word_completer([](const Context&){ return Server::instance().session(); }),
2546 [](const ParametersParser& parser, Context&, const ShellContext&)
__anon866708e25802() 2547 {
2548 if (not Server::instance().rename_session(parser[0]))
2549 throw runtime_error(format("unable to rename current session: '{}' may be already in use", parser[0]));
2550 }
2551 };
2552
2553 const CommandDesc fail_cmd = {
2554 "fail",
2555 nullptr,
2556 "fail [<message>]: raise an error with the given message",
2557 ParameterDesc{},
2558 CommandFlags::None,
2559 CommandHelper{},
2560 CommandCompleter{},
2561 [](const ParametersParser& parser, Context&, const ShellContext&)
__anon866708e25902() 2562 {
2563 throw failure{join(parser, " ")};
2564 }
2565 };
2566
2567 const CommandDesc declare_user_mode_cmd = {
2568 "declare-user-mode",
2569 nullptr,
2570 "declare-user-mode <name>: add a new user keymap mode",
2571 single_param,
2572 CommandFlags::None,
2573 CommandHelper{},
2574 CommandCompleter{},
2575 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e25a02() 2576 {
2577 context.keymaps().add_user_mode(std::move(parser[0]));
2578 }
2579 };
2580
2581 // We need ownership of the mode_name in the lock case
enter_user_mode(Context & context,String mode_name,KeymapMode mode,bool lock)2582 void enter_user_mode(Context& context, String mode_name, KeymapMode mode, bool lock)
2583 {
2584 on_next_key_with_autoinfo(context, format("user.{}", mode_name), KeymapMode::None,
2585 [mode_name, mode, lock](Key key, Context& context) mutable {
2586 if (key == Key::Escape)
2587 return;
2588 if (not context.keymaps().is_mapped(key, mode))
2589 return;
2590
2591 auto& mapping = context.keymaps().get_mapping(key, mode);
2592 ScopedSetBool disable_keymaps(context.keymaps_disabled());
2593
2594 InputHandler::ScopedForceNormal force_normal{context.input_handler(), {}};
2595
2596 ScopedEdition edition(context);
2597 for (auto& key : mapping.keys)
2598 context.input_handler().handle_key(key);
2599
2600 if (lock)
2601 enter_user_mode(context, std::move(mode_name), mode, true);
2602 }, lock ? format("{} (lock)", mode_name) : mode_name,
2603 build_autoinfo_for_mapping(context, mode, {}));
2604 }
2605
2606 const CommandDesc enter_user_mode_cmd = {
2607 "enter-user-mode",
2608 nullptr,
2609 "enter-user-mode [<switches>] <name>: enable <name> keymap mode for next key",
2610 ParameterDesc{
2611 { { "lock", { false, "stay in mode until <esc> is pressed" } } },
2612 ParameterDesc::Flags::SwitchesOnlyAtStart, 1, 1
2613 },
2614 CommandFlags::None,
2615 CommandHelper{},
2616 [](const Context& context, CompletionFlags flags,
2617 CommandParameters params, size_t token_to_complete,
2618 ByteCount pos_in_token) -> Completions
__anon866708e25c02() 2619 {
2620 if (token_to_complete == 0)
2621 {
2622 return { 0_byte, params[0].length(),
2623 complete(params[0], pos_in_token, context.keymaps().user_modes()) };
2624 }
2625 return {};
2626 },
2627 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e25d02() 2628 {
2629 auto lock = (bool)parser.get_switch("lock");
2630 KeymapMode mode = parse_keymap_mode(parser[0], context.keymaps().user_modes());
2631 enter_user_mode(context, std::move(parser[0]), mode, lock);
2632 }
2633 };
2634
2635 const CommandDesc provide_module_cmd = {
2636 "provide-module",
2637 nullptr,
2638 "provide-module [<switches>] <name> <cmds>: declares a module <name> provided by <cmds>",
2639 ParameterDesc{
2640 { { "override", { false, "allow overriding an existing module" } } },
2641 ParameterDesc::Flags::None,
2642 2, 2
2643 },
2644 CommandFlags::None,
2645 CommandHelper{},
2646 CommandCompleter{},
2647 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e25e02() 2648 {
2649 const String& module_name = parser[0];
2650 auto& cm = CommandManager::instance();
2651
2652 if (not all_of(module_name, is_identifier))
2653 throw runtime_error(format("invalid module name: '{}'", module_name));
2654
2655 if (cm.module_defined(module_name) and not parser.get_switch("override"))
2656 throw runtime_error(format("module '{}' already defined", module_name));
2657 cm.register_module(module_name, parser[1]);
2658 }
2659 };
2660
2661 const CommandDesc require_module_cmd = {
2662 "require-module",
2663 nullptr,
2664 "require-module <name>: ensures that <name> module has been loaded",
2665 single_param,
2666 CommandFlags::None,
2667 CommandHelper{},
2668 make_completer(
__anon866708e25f02() 2669 [](const Context&, CompletionFlags, const String& prefix, ByteCount cursor_pos) {
2670 return CommandManager::instance().complete_module_name(prefix.substr(0, cursor_pos));
2671 }),
2672 [](const ParametersParser& parser, Context& context, const ShellContext&)
__anon866708e26002() 2673 {
2674 CommandManager::instance().load_module(parser[0], context);
2675 }
2676 };
2677
2678 }
2679
register_commands()2680 void register_commands()
2681 {
2682 CommandManager& cm = CommandManager::instance();
2683 cm.register_command("nop", [](const ParametersParser&, Context&, const ShellContext&){}, "do nothing",
2684 {{}, ParameterDesc::Flags::IgnoreUnknownSwitches});
2685
2686 auto register_command = [&](const CommandDesc& c)
2687 {
2688 cm.register_command(c.name, c.func, c.docstring, c.params, c.flags, c.helper, c.completer);
2689 if (c.alias)
2690 GlobalScope::instance().aliases().add_alias(c.alias, c.name);
2691 };
2692
2693 register_command(edit_cmd);
2694 register_command(force_edit_cmd);
2695 register_command(write_cmd);
2696 register_command(force_write_cmd);
2697 register_command(write_all_cmd);
2698 register_command(write_all_quit_cmd);
2699 register_command(kill_cmd);
2700 register_command(force_kill_cmd);
2701 register_command(quit_cmd);
2702 register_command(force_quit_cmd);
2703 register_command(write_quit_cmd);
2704 register_command(force_write_quit_cmd);
2705 register_command(buffer_cmd);
2706 register_command(buffer_next_cmd);
2707 register_command(buffer_previous_cmd);
2708 register_command(delete_buffer_cmd);
2709 register_command(force_delete_buffer_cmd);
2710 register_command(rename_buffer_cmd);
2711 register_command(arrange_buffers_cmd);
2712 register_command(add_highlighter_cmd);
2713 register_command(remove_highlighter_cmd);
2714 register_command(add_hook_cmd);
2715 register_command(remove_hook_cmd);
2716 register_command(trigger_user_hook_cmd);
2717 register_command(define_command_cmd);
2718 register_command(alias_cmd);
2719 register_command(unalias_cmd);
2720 register_command(echo_cmd);
2721 register_command(debug_cmd);
2722 register_command(source_cmd);
2723 register_command(set_option_cmd);
2724 register_command(unset_option_cmd);
2725 register_command(update_option_cmd);
2726 register_command(declare_option_cmd);
2727 register_command(map_key_cmd);
2728 register_command(unmap_key_cmd);
2729 register_command(execute_keys_cmd);
2730 register_command(evaluate_commands_cmd);
2731 register_command(prompt_cmd);
2732 register_command(menu_cmd);
2733 register_command(on_key_cmd);
2734 register_command(info_cmd);
2735 register_command(try_catch_cmd);
2736 register_command(set_face_cmd);
2737 register_command(unset_face_cmd);
2738 register_command(rename_client_cmd);
2739 register_command(set_register_cmd);
2740 register_command(select_cmd);
2741 register_command(change_directory_cmd);
2742 register_command(rename_session_cmd);
2743 register_command(fail_cmd);
2744 register_command(declare_user_mode_cmd);
2745 register_command(enter_user_mode_cmd);
2746 register_command(provide_module_cmd);
2747 register_command(require_module_cmd);
2748 }
2749
2750 }
2751