Building a High Performance Text Editor

February 17, 2021 Will Bond

Graphic of a generic text editor interface

Developer tools have always been a passion of mine, from libraries to the command line. Ten years ago I downloaded the beta of a newly cross-platform text editor. Little did I know how much that product would end up influencing my career.

In 2016, after years of involvement in the community, I had an opportunity to join the team. The following are some of my reflections on what defines Sublime Text – both now and looking towards the future.

A Brief Background

I spent the early part of my career working at a web agency, where we built sites and apps for many different clients. In such an environment, I was able to learn quite a bit about programming, design and even a little about business development. Meeting with clients and discussing their projects, it became clear how important it was to understand the Why of a project. It empowered us to make better decisions as we worked. The result was really solving for user needs, instead of just building to a spec.

Sublime Text X

Always on the lookout for improving my tools, I discovered Sublime Text X in early 2011. Previously it was Windows-only, but the new version supported Linux and Mac also. Especially on Linux, the editor experience was unlike anything else available. The combination of performance, multiple cursors and the extensibility made it a shoo-in. It felt similar to TextMate, only I didn’t need a Mac. Little details caught my attention, like the rounding of selection corners.

Package Control

A couple of weeks in I started writing a Python plugin for deploying to our development servers. After iterating on it for my own uses, I decided to turn it into a product. Realizing that the plugin would need frequent bug fixes, I knew I needed an easy way to release updates. At the time, the Sublime Text community was primarily using zip files and source code repos to distribute plugins. The user experience was not great, and required too much manual work.

In the summer of 2011 I spent my evenings hacking on Package Control. One of the unique features was a bundled list of available plugins. Another defining feature was out-of-the-box automatic updates. It ended up not being the first package manager released for Sublime Text, but I attribute a large part of its success to the ease-of-use.

Illustration of approximate growth of bandwidth usage of Package Control from 2011 through 2014

Popularity

Over the course of 2011 and 2012, Sublime Text 2 became quite popular. The combination of features and vision resonated with a large number of programmers, and the number of available packages grew steadily.

In 2013 I developed a new website for Package Control, moving it off of my personal site onto a subdomain. There were over 1,500 packages available, and the site was consuming over a terabyte of bandwidth per month. In 2014 the site migrated to its own domain, now listing around 2,500 packages, and serving one million copies of the package list per day. Even after implementing bzip2 HTTP compression, the server was consuming over 4TB of bandwidth a month.

Joining Sublime HQ

In mid 2014 I left my day job and focused my efforts on my plugins and new software products. Late in 2015, we learned our fourth child was on the way, prompting me to seek out more consistent work. I sent an email to the author of Sublime Text, Jon. Subject: Full Time Work? Three weeks later I joined the company as the second engineer.

A New Environment

One of the biggest changes was adjusting to working for a company on the other side of the world. Sydney is 16 hours ahead of Boston. Traditionally I had really enjoyed working in collaborative spaces with co-workers. However, I think the small company size, combined with my background and self-motivation has resulted in a very positive experience.

Illustration of the earth, highlighting where Sydney and Boston are located

Being a Generalist

One of the aspects I’ve loved about working at Sublime HQ is that the size allows, and almost requires, that the engineering team skew more towards generalists. In addition to a majority of “pure” engineering work, I’ve also had the opportunity to drive our visual design and identity work, revamp our infrastructure, improve our documentation and engage with our community through some of our open source components. I’ve grown to really appreciate and be engaged by the variety of work that goes making a great product with a good user experience.

One significant upside to being a generalist is how it exposes you to the results of your decisions, across the product and company. When you are responsible for engineering, designing, documenting and publishing your work, I believe you gain a unique perspective. You can see the work it takes to implement a design, or use feedback from documenting a feature to make it easier to use. Beyond that, you are equipped to be able to take a vision and see it through from end-to-end.

Working in C++

Prior to joining the Sublime team, I hadn’t touched C++ since my undergraduate days. The majority of my career was spent as a full-stack engineer, working in various high-level, dynamic languages. I had done some light hacking on some C codebases, but my knowledge was definitely surface-level.

One perspective I’ve gained from working in C++ is how much computation higher-level languages can gloss over. A simple operator like `in` can very easily obscure the work of iterating over every element in an array. Working closer to the metal has helped me form a broader intuition about the performance impact of the code I write.

Performance Foundations

Frequently people ask what UI toolkit we use in Sublime Text. We also get asked about what programming language it is written in. Sometimes people want to know what libraries we use. I think a fair number of these questions are implicitly asking how we make Sublime fast. The answer is probably more mundane than most people expect.

At our core, we do less.

Our codebase is full of purpose-built code that does exactly what we need it to, and no more. I think this has been a combination of intentionally pursuing the simplest solution, and the fact that Jon built Sublime Text by himself for the majority of its existence. As the product has matured and we’ve added more engineering resources, we’ve tried to continue walking the balance of making the codebase more robust, while continuing to pursue simplicity.

Sublime Text is written in C++. I could talk about how we avoid exceptions, use move semantics, and how a bunch of our code looks rather like C. These aren’t why Sublime products are fast, but they contribute some. Choosing the right data structures is important, yes. Surprisingly, a vector is more often the correct choice than I would have originally presumed. Our code pre-allocates vectors to their final size. We use memory arenas and intern strings. We pay attention to struct padding and use contiguous memory allocations for improved cache locality.

You’ll probably notice there is an overarching theme. We use less memory. We allocate fewer times. We invalidate the cache fewer times. We do less work. In reality, we have the user’s machine do less work. Sometime we end up doing more work, to chase that goal. Yes, we profile things to improve performance, but the little decisions add up!

Sublime Text runs fast because we make a point of doing less.

Native, but Not

It is not infrequent to hear about the native feel of Sublime Text. Others are quick to point out that we aren’t really native. The truth lies somewhere in the middle.

Illustration of a venn diagram of custom, native and web UIs and Sublime Text in the intersection of custom and native

Native APIs that Make Sense

There are a number of things that help to make a user interface feel native, even when it isn’t using the OS-provided UI toolkit. One of the biggest things is to follow platform conventions. Jon has previously explained our platform-specific layer is more a union of features, as opposed to targeting the lowest common denominator.

We implement platform-specific elements, such as native tabs and proxy icons on macOS and backslashes in file paths on Windows. On all platforms we use native menus and dialogs. Our text rendering uses glyph rasterization from the OS, respecting the user’s anti aliasing settings. We make a point of implementing standard modifier keys and caret behaviors.

These details help to give the interface a feeling of being native. Being fast helps too.

Custom UI

In other cases, native would be the wrong choice. Our editor control is the core of our interface, and how it is implemented is fairly central to making the interface lag-free. Owning it from top to bottom allows us to innovate and tweak the UI, all while keeping performance great. We don’t need to work around flexibility and features that were designed for other use cases.

For example, we use a relatively static layout engine for positioning text. We don’t need to worry about the performance impact of re-flowing text from an animated, expanding box in the middle of the layout. There is no implementation of such a feature. Beyond that, we can implement UI layers on top of the base text layout. For example, in 3.0 we introduced the concept of “phantoms”, which are small HTML documents that sit inline with a file’s text content.

And, yes, we even have our own simple HTML parser and layout engine. It supports basic CSS that is useful when formatting text. We own that from top to bottom also. That means it ends up using the same text rendering as the editor control, and we can support useful experimental features. One such example being the CSS color mod function. It lets plugin authors blend colors from the user’s color scheme, and even ensure minimum contrast levels.

Trade-offs

Clearly, making Sublime Text fast isn’t a single decision or library, but rather a coordinated approach. Sure, we do have some pathological cases that are slow, but we tend to focus on making the 95% use case work really well.

Illustration of a triangle showing the tradeoffs of performance, features and schedule

So, what’s the catch? We have to write and maintain a lot of low-level code. We are also a very small team, with only six engineers. Our engineering team is spread across two products, and handles everything product related – from engineering to design, documentation to infrastructure.

Because we are a lean team, we have to be focused on what we think fits the vision for Sublime Text as a product. We also are paid product in a sea of free alternatives. To thrive as a product, we have to provide a clear benefit to users.

These factors all contribute to a more measured pace of product development. In practice I’ve found this ends up feeling like a benefit about as much as it is a limitation. Constraints force you to be creative in solving your goals. But no, we aren’t going to be pumping out changes at the pace of a SaaS startup using Rails.

Owning your Stack

One of the biggest upsides to owning so much of our stack is that we can often tweak and fix things that are out of practical reach of other projects. We have no large open source projects we need to convince that our feature is worth the implementation. We can make that decision ourselves. Since we do not license or release our UI toolkit, we can make sweeping changes without a lot of fuss.

Another upside is consistency and sharing code across platforms. We have three very low-level, platform-specific adapters that provide a base layer our UI is built upon. Our UI toolkit is 100% custom, and thus we don’t need to implement UI components against three different APIs. The only place the platform bleeds into the UI is in behaviors, and since we own everything, we can pretty easily customize those as we see fit.

Vision

I’ve talked a lot about performance so far, but that in and of itself isn’t a compelling user experience. It is the blend of features, and making those fast, that creates a product worth using.

Illustration of the six main vision components outlined in this article

In the realm of editors, there is a spectrum that ranges from simplistic, bare bones text editors, to full-featured IDEs that can do things like automatically refactor code. Having a vision for where Sublime Text sits in this spectrum helps us to determine where to invest our resources, and what sort of experience we want to focus on.

As I've spent the past five years working on Sublime Text, I’ve come to consider the following to be some of our guiding principles:

Performant

Performance is a feature that enables using a tool in robust ways. When actions are instantaneous, users will use them more. Pauses, hangs and slow responses result in the tool interrupting the task at hand. Whenever possible, we should respond immediately to user input.

Very few components of our UIs have the concept of being asynchronous. The helps simplify the interface, and makes performance issues stand out.

Navigable

Helping users easily navigate code is essential. While writing software, quite a bit of time is spent in reading code. Making navigation seamless and predictable can make a big impact on user experience. Ideally navigation should feel fluid and uncomplicated so the user can focus on understanding.

Extensible

It should be simple to customize behavior, straightforward to add support for new languages, and possible to add functionality. Due to our size, we can only ship support for a limited set of languages. When we add new features or interaction paradigms, we try to make it possible for plugin authors to take advantage of this work.

Because of our constraints, completely changing behavior or adding new concepts via the API is not our primary goal. We’d rather develop purpose-built components over time, than a general-purpose UI toolkit with an editor control.

In the realm of plugins, breaking backwards compatibility should be done rarely. A high bar should be set: significant, clear benefits should exist and no reasonable way to maintain compatibility.

Simple

The interface should be uncluttered, to the point, and consistent. Putting everything as a button, icon or tab can be as much of a detriment as having nothing visible. When many different things beg for the user’s attention, they start becoming noise. Provide the essentials and rely on plugins to augment.

Refined

Don’t underestimate the value of an attractive interface. It won’t be possible to fit everyone’s taste, but it is possible to have nice defaults. The interface should look crisp, have reasonable contrast, follow contemporary design styles, and use simple, clear iconography. Allow users to customize the UI to suit their taste, and provide helpers where practical.

Familiar

Follow the conventions of the platform, and choose defaults based on the expectations of the majority. By acting like other applications on the user’s OS, it helps the interface get out of the way. Standard key bindings and behaviors are as much of the interface as the visual components. Add settings to customize behavior when reasonable choices exist.

Evolution

As I look forward to our future, I also spent a little time reflecting on our past.

Product

During the development of Sublime Text 2, there were 130 public development builds and 12 stable builds. Sublime Text 3 had 166 development builds and 8 stable builds. Version 3.0 launched in 2017, 3.1 in 2018 and 3.2 in 2019. It is really eye-opening to grab a copy of Sublime Text build 3095 and compare it to where we are now.

A few of my favorite changes since 2016 include:

Some of these are more significant than they may seem. For instance, our syntax definitions power not only syntax highlighting, but also Goto Symbol, the Definitions popup and Goto Symbol in Project. Improving the quality of these can make a distinct impact on the user experience.

Illustration of the Sublime Merge logo

In that time, we also launched Sublime Merge. Merge shares a whole bunch of core technology with Sublime Text, but focuses on making an interface specialized for Git. This dovetails with the vision of our products being simple and refined. Instead of trying to meld two workflows into a single interface, we decided to build a UI specialized for the task. While Sublime Merge has built upon many components of Sublime Text, it has also contributed features back, including the Git integration added in version 3.2.

Team

What we’ve accomplished over the past five years has been enabled by the fact that we’ve grown as a team also. Before I joined the company in 2016, Jon was the entire product team and Kari was responsible for operations. During the development of Sublime Merge, Dylan and Benjamin joined as engineers. In 2019 we added two more engineers, Tim and David.

Even though we’ve grown, we are still a very small and nimble team. It also means that we continue to have the opportunity to be generalists and get involved in all different aspects of the products.

Community

Beyond growing the team, we’ve also improved our engagement with the Sublime Text community at large. A number of smart, dedicated community members have contributed significantly to our bug tracker and our open source syntax definitions. These contributions have made it possible for us to more quickly fix bugs, and more robustly support various programming languages.

In addition to the forum, we also have an active community on our Discord server.

The Future

Over the past year we’ve invested a bunch of work into the next major version of Sublime Text. To date, we’ve released over 40 dev builds, with a public release right around the corner. We’ve got everything from rendering enhancements to user interface additions, indexer improvements, and a slew of new APIs. A few of my favorites include:

Some of these may sound trivial in isolation, but in combination I believe they really shine. I can’t wait for everyone to get a chance to try them out!

Thank you to Terence Martin and Jessica Bond for feedback on this article