After nearly a decade of writing Vim plugins, I’ve decided to take a fundamentally different approach: write Vim plugins in Swift. This post is about writing Vim plugins with Swift and the first version of the system to do so, SwiftForVim.
Status: In Progress
Background
Vim is a powerful text editor with an extensive plugin system and programming language, VimScript. VimScript is the default language to write Vim plugins in and it’s integrated natively into the editor.
When plugins start getting complex, involving sockets, servers, and data processing, VimScript becomes the “glue” layer. Python, Lua, and Ruby integrations are optionally compiled into Vim. These languages are working well for many plugins, but Swift has a lot to offer!
As a high-performance systems programming language, Swift brings a lot to Vim plugins; from development to user experience. The type system has made it easier and more enjoyable to write stable Vim plugins. Features like performance, the open source community, the package managers, the functionalness, make it a hot alternative to the existing supported languages.
Implementation
First, there needs to be a way to access Vim state from Swift, and Swift state from Vim. SwiftForVim, exposes Vim’s state to Swift, and allows VimScript to call Swift.
The project was originally created as part of the first Swift Vim plugin, SwiftPackageManager.vim!
Building the Swift <-> Vim Bridge
The current process of language integration in Vim is:
- Implement X_LANGUAGE interpreter’s in Vim’s source code.
- Implement core classes, like
Buffer
andWindow
. - Eval code in X_LANGUAGE from VimScript
- Eval code in VimScript from X_LANGUAGE
This pattern is tried and true, and many great plugins rely on the bridges built this way.
Swift support is based on this pattern, except the interpreter bit. Adding a Swift interpreter into Vim’s source code is complex from an implementation, social, and maintainability perspective. Requiring users to compile a patched Vim would detract from usability of such a solution.
Since Python is already built into Vim, it was the natural alternative to the former. The Python interpreter has a strong, well documented API for embedding.
SwiftForVim, simply “embeds” Python: calling Python from Swift, and calling Swift from Python. The VimScript APIs are simply accessible through this bridge.
SwiftForVim API
There’s a few base types the user interacts with. The goal of these types is to provide a “View” into Vim’s state. In the context of text editing, performance is critical. The entire system is implemented with performance as the primary consideration. The notion of “Toll Free Bridging” applies: accessing raw memory of Vim: not copying it to Swift representations.
The module Vim
implements the main API for evaluating expressions and running
commands.
/// Command
Vim.command("echo 'Hello World!'")
/// Eval
let path = String(Vim.eval("expand('%:p')"))
The API exposes Vim’s memory via a VimValue
.
@discardableResult public static func eval(_ cmd: String) throws -> VimValue
VimValue
represents the object returned from Vim. Internally, it manages
memory management semantics and Swift primitives can be created from it.
Toll Free Bridging
Performance is a design goal and usability is not compromised. VimList
and
VimDictionary
are wrappers around Vim types.
Through the VimList
API, state can be accessed without copying data.
public final class VimList: Collection { }
The Collection
method, subscript
simply reads and writes to the underlying data type.
public subscript(index: Int) -> VimValue {
get {
return VimValue(swiftvim_list_get(value.reference, Int32(index)))
}
set {
swiftvim_list_set(value.reference, Int32(index), newValue.reference)
}
}
Since VimList
is a Collection
, standard operations, like map
are composed
on.
Challenges and Limitations
The implementation works quite well and there are several challenges due to the nature of Vim plugins.
Shared state and a Vim plugin’s runtime environment
In order to access editor state, all plugins are dynamically linked into the
Vim process. Symbol conflicts amongst plugin level code is somewhat unlikely
due to Swift’s ABI design: each plugin’s code is namespaced for the name of the
plugin ( i.e.
SwiftPackageManager.vim
’s code uses the namespace,
SPMVimPlugin
)
The namespaced function GetPluginDir()
in the swiftmodule SPMVimPlugin
:
# $ nm SPMVimPlugin.swift.o | awk '{ print $3 }' | xargs swift-demangle
...
_T012SPMVimPlugin03GetB3DirSSyF ---> SPMVimPlugin.GetPluginDir() -> Swift.String
The current build of Vim
namespaces all of the internal classes at the Module
level to prevent collisions.
Currently, this is visible to the user. Vim
is imported as:
import __PLUGIN_NAME__Vim
There is no great solution to this and namespacing transitive dependencies.
Ideally, the user would write:
import Vim
And the generated ABI would be namespaced, possibly controllable by a compiler option.
Whether or not support should be added to SPM
or Swift
to support
namespacing is out of scope of this post. This really needs more thought in the
future.
Dynamically typed in a Swift world
Much like other language integrations, VimScript support is built ontop of
evaling strings. SwiftForVim
has a way to create types from Vim state in
Swift. But, the entire string evaluation is untyped: for example, arguments to
functions, return values of functions, etc. It’d be awesome to have a typed
Swift API for the entire VimScript standard library. This is something to
consider going forward.
In the standard library of Vim
, types do not dynamically cast to Any
. The
user is required to explicitly convert types by using constructors. A
reflection based API would be possible to add: via converting VimValue
to
Any
. This would have some trade-offs but is worth considering in the future.
Memory Management
Python uses a reference counting API for memory management which bridges to
Swift quite nicely. With VimValue
it’s possible to automatically decrement
reference counts when necessary. As with the entire project, more work needs to
be done.
Auditing the actual application of this in practice will be required at some point.
Deployment user experience
Deployment of applications that build as part of installation in disparate environments is a non trivial problem. It’s not possible to predict, reason about, or all possible combinations of OS’s, compilers, etc.
Binary deployments of Swift plugins will solve most deployment problems: Vim plugins written in Swift can theoretically be compiled ahead of time and distributed to the users machine. More work needs to happen in the build to achieve this in practice.
This is certainly a long term goal.
Looking forward
SwiftForVim started out as a means
to build a foundation for SwiftPackageManager.vim
and SwiftPlayground.vim
.
It’s now live in it’s own github repo, so it can be developed in isolation of the plugins it supports. Contributions and feedback always welcome!