-
Notifications
You must be signed in to change notification settings - Fork 0
[Draft] Proposal: Plugin-based Architecture, Extension API #366
Description
This should eventually supersede #64, the idea is to refine notes in this issue into a proposal/spec over time. I might move this to a wiki page as it becomes more spec than background story, so we can track history of edits by collaborators, but at the moment the GitHub wiki is disabled on this project.
I've mixed terms a bit here—should we adopt nomenclature of ensime-vim "extensions" to avoid confusion when we're talking in the context of Vim "plugins"?
I was thinking about our current architecture as it has been evolving, and it occurred to me that it might be improved by embracing a more plugin-based approach, and almost by construction we would end up with an extensible API that makes it easier for users to do custom scripting against ensime-vim and thus contribute new features more easily (pretty much the essence of #64).
I'm referring primarily to message handlers, functions invoked when particular messages are received from ENSIME. We began with a monolithic client that did just about everything:
- Implemented low-level client/server communication logic.
- Defined the higher-level feature-based function interface for sending calls to the server.
- Implemented all the logic for each specific response message handler.
- Had a sprawling/entangled interface to Vim functionality for UI interaction.
- Even started the server itself (via
EnsimeLauncher, but the chain of calls originated from the client).
Piece by piece we're breaking this down, decoupling, making testing easier. It's still in progress.
One tool that we've applied crudely to-date is mixins. Look at debugger.py and typecheck.py and they're just moving food around on the plate: they're still tightly coupled to EnsimeClient, directly meddling in its state as self, they just offered some code organization into more manageable files. protocol.py is the same story but it was additionally solving the problem of alternate versions. It was a reasonable evolutionary solution for the state of the code at the time.
My thinking is that these things can become plugins to the low-level client core, instead of mixins. Rough ideas:
- Plugins are classes that get access to an
EnsimeClientby constructor injection. - Plugins are registered with
Ensime, and when it creates instances ofEnsimeClientit tells them all the known plugins. - We provide a function decorator for plugins like
@handles('SymbolInfo')to declaratively register handlers for received messages. EnsimeClientdispatches the appropriate messages to each registered handler when received.- "Everything is a plugin"—all feature-level API of
EnsimeClient(just about any function that sends or receives a specific ENSIME message) is moved out of it. This will include current "core" functionality but a relatively focused feature area like debugger support might be a good initial use case for hashing out the design. - Handlers can interact with the UI via the
editorobject of their injectedclientinstance (is there a better way?).
With this, in theory, users could compose features and add new ones without needing to touch core code—drop a .vim script in ~/.vim/after/plugin/ with a :python block that imports a few things from us like the @handles decorator, and off you go. We reap the benefits of eating our own dogfood by using plugins internally: the API is constantly tested, and if its design is flawed we feel the pain.
This likely converges with the direction that I'm (ever so slowly) trying to go with my pip install-able Python ENSIME client project, so it ought to be straightforward to replace ensime-vim's client impl with it if and when that's ready (hoping ensime-sublime would adopt it too).
These are rough, possibly bad ideas. Feedback welcome, and getting to a point of some working code will help to validate. Some concerns to consider further:
- The above is mainly focused on response handlers—is anything needed to make adding new requests simple? You can just define your own new Vim commands/functions/etc. and wire them to your plugin class that sends via its client, but could any helpers we provide make this easier, avoid name clashes, etc.?
- I haven't thought much about how plugins might deal with protocol versions.
- I haven't thought much about surfacing the functionality through VimL APIs, but it seems like it should be straightforward (?). Might be able to leverage
Userautocmds as I mentioned on Provide generic API to ensime for other Vim plugins to integrate. #64 for events. - Making unit testing of plugins easy: requests will probably mainly need to assert on a mock client's
send_request, response handlers will assert on various operations of a mockEditor. I'm not sure how cumbersome this will be in practice—need to try some code, and theEditorandEnsimeClientAPIs are steadily improving which helps. Any room for improvement on coupling to these dependencies? - Requires a lot more thought/research, but it would be good to consider how handlers could be async in the future—coroutines in Python 3 maybe; Neovim can surely help a lot but how far can we go without substantially diverging code?
Sorry for the wall of text. 😓