Swift For Vim

20 Jun 2018


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 and Window.
  • 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!

Published on 20 Jun 2018 Find me on Twitter!