From 3d7d0fecca885b00a7ae80180ea1841fab2c5993 Mon Sep 17 00:00:00 2001 From: Maxime Coste Date: Thu, 11 Apr 2024 11:37:18 +1000 Subject: [PATCH] Introduce "local" scope in evaluate-commands When using `eval` a new scope named 'local' gets pushed for the whole evaluation, this makes it possible to temporarily set an option/hook/alias... Local scopes nest so nested evals do work as expected. Remove the now trivial with-option command --- doc/pages/execeval.asciidoc | 5 +++++ doc/pages/scopes.asciidoc | 22 ++++++++++++++++------ rc/windowing/detection.kak | 25 +------------------------ src/alias_registry.hh | 3 +++ src/commands.cc | 28 +++++++++++++++++++++++++--- src/context.cc | 8 +++++++- src/context.hh | 8 +++++++- src/face_registry.hh | 2 ++ src/highlighter_group.hh | 2 ++ src/hook_manager.hh | 2 ++ src/insert_completer.cc | 5 ++++- src/keymap_manager.hh | 2 ++ src/option_manager.cc | 9 +++++++++ src/option_manager.hh | 2 ++ src/scope.cc | 10 ++++++++++ src/scope.hh | 2 ++ 16 files changed, 99 insertions(+), 36 deletions(-) diff --git a/doc/pages/execeval.asciidoc b/doc/pages/execeval.asciidoc index 4606c634..98ad8cf0 100644 --- a/doc/pages/execeval.asciidoc +++ b/doc/pages/execeval.asciidoc @@ -64,3 +64,8 @@ are then restored when the keys have been executed: */*, *"*, *|*, *^*, *-with-hooks*:: Execute keys and trigger existing hooks. (See <>) + +== Local scope in *evaluate-commands* + +When using *evaluate-commands* a new scope, named `local` is inserted. +See <> diff --git a/doc/pages/scopes.asciidoc b/doc/pages/scopes.asciidoc index b6711278..02bb5470 100644 --- a/doc/pages/scopes.asciidoc +++ b/doc/pages/scopes.asciidoc @@ -36,15 +36,25 @@ Scopes are named as follows: *global*:: global context linked to the instance of Kakoune +*local*:: + A local scope is inserted by each *evaluate-commands* invocations + for its duration. Nested *evaluate-commands* each inject a new + local scope whose parent is the previous local scope. + + A local scope is intended for temporarily overwriting some scoped + value, such as an option or an alias. + + The following order of priority applies to the above scopes: --------------------------- -window ]> buffer ]> global --------------------------- +----------------------------------- +local ]> window ]> buffer ]> global +----------------------------------- -The above priority line implies that objects can have individual values that -will be resolved first in the *window* scope (highest priority), then in -the *buffer* scope, and finally in the *global* scope (lowest priority). +The above priority line implies that objects can have individual values +that will be resolved first in the *local* scope (if it exists), then the +*window* scope, then in the *buffer* scope, and finally in the *global* +scope. Normally, the *buffer* scope keyword means the scope associated with the currently active buffer, but it's possible to specify any existing buffer by diff --git a/rc/windowing/detection.kak b/rc/windowing/detection.kak index ebdaf208..2fde81cd 100644 --- a/rc/windowing/detection.kak +++ b/rc/windowing/detection.kak @@ -48,7 +48,7 @@ define-command terminal -params 1.. -docstring %{ Example usage: terminal sh - with-option windowing_placement horizontal terminal sh + evaluate-commands %{ set local windowing_placement horizontal; terminal sh } See also the 'new' command. } %{ @@ -56,29 +56,6 @@ define-command terminal -params 1.. -docstring %{ } complete-command terminal shell -# TODO Move this? -define-command with-option -params 3.. -docstring %{ - with-option []: evaluate a command with a modified option -} %{ - evaluate-commands -save-regs s %{ - evaluate-commands set-register s %exp{%%opt{%arg{1}}} - set-option current %arg{1} %arg{2} - try %{ - evaluate-commands %sh{ - shift 2 - for arg - do - printf "'%s' " "$(printf %s "$arg" | sed "s/'/''/g")" - done - } - } catch %{ - set-option current %arg{1} %reg{s} - fail "with-option: %val{error}" - } - set-option current %arg{1} %reg{s} - } -} - hook -group windowing global KakBegin .* %{ evaluate-commands %sh{ set -- ${kak_opt_windowing_modules} diff --git a/src/alias_registry.hh b/src/alias_registry.hh index c35cdf70..1b7a49eb 100644 --- a/src/alias_registry.hh +++ b/src/alias_registry.hh @@ -12,6 +12,9 @@ class AliasRegistry : public SafeCountable { public: AliasRegistry(AliasRegistry& parent) : SafeCountable{}, m_parent(&parent) {} + + void reparent(AliasRegistry& parent) { m_parent = &parent; } + void add_alias(String alias, String command); void remove_alias(StringView alias); StringView operator[](StringView alias) const; diff --git a/src/commands.cc b/src/commands.cc index 4404ee70..f217a9fa 100644 --- a/src/commands.cc +++ b/src/commands.cc @@ -48,6 +48,24 @@ namespace Kakoune extern const char* version; +struct LocalScope : Scope +{ + LocalScope(Context& context) + : Scope(context.scope()), m_context{context} + { + m_context.m_local_scopes.push_back(this); + } + + ~LocalScope() + { + kak_assert(not m_context.m_local_scopes.empty() and m_context.m_local_scopes.back() == this); + m_context.m_local_scopes.pop_back(); + } + +private: + Context& m_context; +}; + namespace { @@ -215,21 +233,21 @@ const ParameterDesc double_params{ {}, ParameterDesc::Flags::None, 2, 2 }; static Completions complete_scope(const Context&, CompletionFlags, StringView prefix, ByteCount cursor_pos) { - static constexpr StringView scopes[] = { "global", "buffer", "window", }; + static constexpr StringView scopes[] = { "global", "buffer", "window", "local"}; return { 0_byte, cursor_pos, complete(prefix, cursor_pos, scopes) }; } static Completions complete_scope_including_current(const Context&, CompletionFlags, StringView prefix, ByteCount cursor_pos) { - static constexpr StringView scopes[] = { "global", "buffer", "window", "current" }; + static constexpr StringView scopes[] = { "global", "buffer", "window", "local", "current" }; return { 0_byte, cursor_pos, complete(prefix, cursor_pos, scopes) }; } static Completions complete_scope_no_global(const Context&, CompletionFlags, StringView prefix, ByteCount cursor_pos) { - static constexpr StringView scopes[] = { "buffer", "window", "current" }; + static constexpr StringView scopes[] = { "buffer", "window", "local", "current" }; return { 0_byte, cursor_pos, complete(prefix, cursor_pos, scopes) }; } @@ -395,6 +413,8 @@ Scope* get_scope_ifp(StringView scope, const Context& context) return &context.buffer(); else if (prefix_match("window", scope)) return &context.window(); + else if (prefix_match("local", scope)) + return context.local_scope(); else if (prefix_match(scope, "buffer=")) return &BufferManager::instance().get_buffer(scope.substr(7_byte)); return nullptr; @@ -2168,6 +2188,7 @@ const CommandDesc execute_keys_cmd = { } }; + const CommandDesc evaluate_commands_cmd = { "evaluate-commands", "eval", @@ -2186,6 +2207,7 @@ const CommandDesc evaluate_commands_cmd = { const bool no_hooks = context.hooks_disabled() or parser.get_switch("no-hooks"); ScopedSetBool disable_hooks(context.hooks_disabled(), no_hooks); + LocalScope local_scope{context}; if (parser.get_switch("verbatim")) CommandManager::instance().execute_single_command(parser | gather>(), context, shell_context); else diff --git a/src/context.cc b/src/context.cc index 71a5becd..62171674 100644 --- a/src/context.cc +++ b/src/context.cc @@ -51,8 +51,10 @@ Client& Context::client() const return *m_client; } -Scope& Context::scope() const +Scope& Context::scope(bool allow_local) const { + if (allow_local and not m_local_scopes.empty()) + return *m_local_scopes.back(); if (has_window()) return window(); if (has_buffer()) @@ -70,6 +72,8 @@ void Context::set_window(Window& window) { kak_assert(&window.buffer() == &buffer()); m_window.reset(&window); + if (not m_local_scopes.empty()) + m_local_scopes.front()->reparent(window); } void Context::print_status(DisplayLine status) const @@ -314,6 +318,8 @@ void Context::change_buffer(Buffer& buffer, Optional> set_se ScopedSelectionEdition selection_edition{*this}; selections_write_only() = SelectionList{buffer, Selection{}}; } + if (not m_local_scopes.empty()) + m_local_scopes.front()->reparent(buffer); } if (has_input_handler()) diff --git a/src/context.hh b/src/context.hh index 7834bde4..7ff4166e 100644 --- a/src/context.hh +++ b/src/context.hh @@ -42,6 +42,8 @@ private: using LastSelectFunc = std::function; +struct LocalScope; + // A Context is used to access non singleton objects for various services // in commands. // @@ -97,7 +99,10 @@ public: void set_client(Client& client); void set_window(Window& window); - Scope& scope() const; + friend struct LocalScope; + + Scope& scope(bool allow_local = true) const; + Scope* local_scope() const { return m_local_scopes.empty() ? nullptr : m_local_scopes.back(); } OptionManager& options() const { return scope().options(); } HookManager& hooks() const { return scope().hooks(); } @@ -155,6 +160,7 @@ private: SafePtr m_input_handler; SafePtr m_window; SafePtr m_client; + std::vector m_local_scopes; class SelectionHistory { public: diff --git a/src/face_registry.hh b/src/face_registry.hh index e95a02fd..88e52980 100644 --- a/src/face_registry.hh +++ b/src/face_registry.hh @@ -24,6 +24,8 @@ class FaceRegistry : public SafeCountable public: FaceRegistry(FaceRegistry& parent) : SafeCountable{}, m_parent(&parent) {} + void reparent(FaceRegistry& parent) { m_parent = &parent; } + Face operator[](StringView facedesc) const; Face operator[](const FaceSpec& facespec) const; void add_face(StringView name, StringView facedesc, bool override = false); diff --git a/src/highlighter_group.hh b/src/highlighter_group.hh index a0873dc0..1502b796 100644 --- a/src/highlighter_group.hh +++ b/src/highlighter_group.hh @@ -43,6 +43,8 @@ class Highlighters : public SafeCountable public: Highlighters(Highlighters& parent) : SafeCountable{}, m_parent{&parent}, m_group{HighlightPass::All} {} + void reparent(Highlighters& parent) { m_parent = &parent; } + HighlighterGroup& group() { return m_group; } const HighlighterGroup& group() const { return m_group; } diff --git a/src/hook_manager.hh b/src/hook_manager.hh index 29baa033..228a4f23 100644 --- a/src/hook_manager.hh +++ b/src/hook_manager.hh @@ -123,6 +123,8 @@ public: HookManager(HookManager& parent); ~HookManager(); + void reparent(HookManager& parent) { m_parent = &parent; } + void add_hook(Hook hook, String group, HookFlags flags, Regex filter, String commands, Context& context); void remove_hooks(const Regex& regex); diff --git a/src/insert_completer.cc b/src/insert_completer.cc index c50fbe00..3e1bcd2d 100644 --- a/src/insert_completer.cc +++ b/src/insert_completer.cc @@ -403,7 +403,10 @@ InsertCompletion complete_line(const SelectionList& sels, } InsertCompleter::InsertCompleter(Context& context) - : m_context(context), m_options(context.options()), m_faces(context.faces()) + : m_context(context), + // local scopes might go away before completion ends, make sure to register on a long lived one + m_options(context.scope(false).options()), + m_faces(context.scope(false).faces()) { m_options.register_watcher(*this); } diff --git a/src/keymap_manager.hh b/src/keymap_manager.hh index 208b44f1..4819be05 100644 --- a/src/keymap_manager.hh +++ b/src/keymap_manager.hh @@ -31,6 +31,8 @@ class KeymapManager public: KeymapManager(KeymapManager& parent) : m_parent(&parent) {} + void reparent(KeymapManager& parent) { m_parent = &parent; } + using KeyList = Vector; void map_key(Key key, KeymapMode mode, KeyList mapping, String docstring); void unmap_key(Key key, KeymapMode mode); diff --git a/src/option_manager.cc b/src/option_manager.cc index 5dd3d612..0a77548f 100644 --- a/src/option_manager.cc +++ b/src/option_manager.cc @@ -28,6 +28,15 @@ OptionManager::~OptionManager() kak_assert(m_watchers.empty()); } +void OptionManager::reparent(OptionManager& parent) +{ + if (m_parent) + m_parent->unregister_watcher(*this); + + m_parent = &parent; + parent.register_watcher(*this); +} + void OptionManager::register_watcher(OptionManagerWatcher& watcher) const { kak_assert(not contains(m_watchers, &watcher)); diff --git a/src/option_manager.hh b/src/option_manager.hh index e4dc230f..3a4bb5a5 100644 --- a/src/option_manager.hh +++ b/src/option_manager.hh @@ -89,6 +89,8 @@ public: OptionManager(OptionManager& parent); ~OptionManager(); + void reparent(OptionManager& parent); + Option& operator[] (StringView name); const Option& operator[] (StringView name) const; Option& get_local_option(StringView name); diff --git a/src/scope.cc b/src/scope.cc index 64bcd445..fe33f6f5 100644 --- a/src/scope.cc +++ b/src/scope.cc @@ -4,6 +4,16 @@ namespace Kakoune { +void Scope::reparent(Scope& parent) +{ + m_options.reparent(parent.m_options); + m_hooks.reparent(parent.m_hooks); + m_keymaps.reparent(parent.m_keymaps); + m_aliases.reparent(parent.m_aliases); + m_faces.reparent(parent.m_faces); + m_highlighters.reparent(parent.m_highlighters); +} + GlobalScope::GlobalScope() : m_option_registry(m_options) { diff --git a/src/scope.hh b/src/scope.hh index 870e1159..b98be7bb 100644 --- a/src/scope.hh +++ b/src/scope.hh @@ -36,6 +36,8 @@ public: Highlighters& highlighters() { return m_highlighters; } const Highlighters& highlighters() const { return m_highlighters; } + void reparent(Scope& parent); + private: friend class GlobalScope; Scope() = default;