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
This commit is contained in:
Maxime Coste 2024-04-11 11:37:18 +10:00
parent b1c114bf6d
commit 3d7d0fecca
16 changed files with 99 additions and 36 deletions

View File

@ -64,3 +64,8 @@ are then restored when the keys have been executed: */*, *"*, *|*, *^*,
*-with-hooks*::
Execute keys and trigger existing hooks.
(See <<hooks#,`:doc hooks`>>)
== Local scope in *evaluate-commands*
When using *evaluate-commands* a new scope, named `local` is inserted.
See <<scopes#,`:doc scopes`>>

View File

@ -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

View File

@ -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 <option_name> <new_value> <command> [<arguments>]: 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}

View File

@ -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;

View File

@ -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<Vector<String>>(), context, shell_context);
else

View File

@ -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<FunctionRef<void()>> 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())

View File

@ -42,6 +42,8 @@ private:
using LastSelectFunc = std::function<void (Context&)>;
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<InputHandler> m_input_handler;
SafePtr<Window> m_window;
SafePtr<Client> m_client;
std::vector<Scope*> m_local_scopes;
class SelectionHistory {
public:

View File

@ -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);

View File

@ -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; }

View File

@ -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);

View File

@ -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);
}

View File

@ -31,6 +31,8 @@ class KeymapManager
public:
KeymapManager(KeymapManager& parent) : m_parent(&parent) {}
void reparent(KeymapManager& parent) { m_parent = &parent; }
using KeyList = Vector<Key, MemoryDomain::Mapping>;
void map_key(Key key, KeymapMode mode, KeyList mapping, String docstring);
void unmap_key(Key key, KeymapMode mode);

View File

@ -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));

View File

@ -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);

View File

@ -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)
{

View File

@ -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;