ADR 0128: One name, one command¶
Status¶
Accepted
Context¶
Two things turned up while writing the user documentation (ADR 0126), both of which had to be documented as behaviour rather than fixed at the time.
:editor.save reported an unknown command. A key binding resolved a
name through the viewport's registry, then the window's, then the plugin
host. The : line resolved it through the plugin host only. So
key.ctrl+s = editor.save worked and typing :editor.save did not —
the same name meaning two different things depending on how you said it.
A plugin command destroyed the undo history. A plugin is handed the
AseBuffer and edits it directly, so nothing it does passes through the
undo stack. The first version answered that honestly but bluntly:
ase_undo_destroy(m_undo);
m_undo = ase_undo_create();
Running a formatter therefore cost every step back to the start of the session, with no warning and no way to get it back.
Decision¶
One resolution order¶
runCommandByName() is the single answer to "what does this name mean":
this buffer's commands, then the window's, then any plugin's. Both the
key-binding path and the : line call it, so they cannot drift again.
: still takes its ex-commands first — w, q, s/from/to/, a bare
line number — because those are not commands in the registry and never
were. A name that is not one of those now reaches the registry instead
of skipping straight past it.
The plugin's edit is recorded, not mourned¶
The text before the command is already in m_cache. The text after is
read back from the buffer. When they differ, the change is put back and
redone through the undo stack as one step.
u then takes back the whole command at once, which is what a single
command should cost, and everything before it survives. When they do not
differ, no step is recorded — a command that changed nothing must not
leave something for u to spend itself on.
Consequences¶
gui/tests/test_plugin_flow.cpp is new: it writes a real Lua plugin into
the test config directory, loads it through the real plugin host, and
drives it through a real viewport. Three of its cases fail on the old
code.
That test is also the first automated coverage the plugin path has had from the GUI side at all — ADR 0113's claim that a plugin command is bindable like any other was true, but only demonstrated by hand.
The round-trip costs one copy of the buffer per plugin command. That is the same cost the crash snapshot already pays per pause in typing, and a plugin command is a deliberate, occasional act rather than something on the keystroke path.
Nothing here changes what a plugin can do. The ABI is still one function shape over a buffer; widening it is still EXTENSIBILITY.md's recommendation 2.