A new completion engine for Builder

Since my initial announcement of Builder at GUADEC in 2014, I’ve had a vision in the back of my mind about how I’d like completion to work in Builder. However, there have been more important issues to solve and I’m just one person. So it was largely put on the back burner because after a few upstream patches, the GtkSourceView design was good enough.

However, as we start to integrate more external tooling into Builder, the demands and design of what those completion layers expect of the application have changed. And some of that is in conflict with the API/ABI we have in the long-term stable versions of GtkSourceView.

So over the past couple of weeks, I’ve built a new completion engine for Builder that takes these new realities into account.

A screenshot of Builder's new completion engine showing results from clang in a C source file.

It has a number of properties I wanted for Builder such as:

Reduced Memory and CPU Usage

Some tooling wants to give you a large set of proposals for completion and then expects the IDE to filter in the UI process. Notably, this is how Clang works. That means that a typical Gtk application written in C could easily have 25,000 potential completion proposals.

In the past we mitigated this through a number of performance tricks, but it still required creating thousands of GObjects, linked lists, queues, and such. That is an expensive thing to do on a key-press, especially when communicating with a sub-process used for crash-isolation.

So the new completion provider API takes advantage of GListModel which is an interface that focuses on how to have a collection of GObjects which don’t need to be “inflated” until they’ve been requested. In doing so, we can get our GVariant IPC message from the gnome-builder-clang sub-process as a single allocation. Then, as results are requested by the completion display, a GObject is inflated on demand to reference an element of that larger GVariant.

In doing so, we provide a rough upper bound on how many objects need to be created at any time to display the results to the user. We can also still sort and filter the result set without having to create a GObject to represent the proposal. That’s a huge win on memory allocator churn.

Consistent and Convenient Refiltering

Now that we have external tooling that expects UI-side refiltering of proposals, we need to make that easier for tooling to do without having to re-query. So the fuzzy search and highlighting tools have been moved into IdeCompletion for easy access by completion providers.

As additional text is provided for completion, the providers are notified to perform filters on their result set. Since the results are GListModel-based, everything updates in the UI out-of-band nicely with a minimal number of gsignal emissions. Compare this to GtkTreeModel which has to emit signals for every row insertion, change, or deletion!

Alternative Styling

When working with completions for programming languages, we’re often dealing with 3 potential groups of content. The return value, the name and possible parameters, and miscellaneous data. To get the styling we want for all of this, I chose to forgo the use of GtkTreeView and use widgets directly. That means that we can use CSS like we do everywhere else. But also, it means that some additional engineering is required.

We only want to create widgets for the visible rows, because otherwise we’re wasting memory and time crunching CSS for things that won’t be seen. We also want to avoid creating new widgets every time the visible range of proposals is changed.

The result is IdeCompletionListBox which is a GtkBox containing GtkListBoxRow and some GtkSizeGroups to give things a columnar effect. Because the number of created widgets is small things stay fast and snappy while giving us the desired effect. Notably, it implements GtkScrollable so if you place it in a GtkScrolledWindow you still get the expected behavior.

Further more, we can adjust the window sizing and placement to be more natural for code-related proposals.

Dynamic Priority Control

We want the ability to change the priority of some completion providers based on the context of the completion. The new design allows for providers to increase their priority when they know they have something of high-importance given some piece of contextual knowledge.

Long term, we also want to provide an API for providers to register a small number of suggested completions that will be raised to the top-level, regardless of what provider gave them. This is necessary instead of having global “scoring” since that would require both O(n) scans of the data set as well as coming up with a strategy to score disparate systems (and search engines prove that rarely works well).

More to do

There are still a couple things that I think we should address that may influence the API design more. For example:

  • How should we handle string interpolation? A simplified API for completions when working inside of strings might be useful. Think strftime(), printf(), etc as potential examples here.
  • The upcoming Gtk+ 3.24 release will give us access to the move_to_rect() API. Combined with some Wayland xdg_popup improvements, this could allow us to make our display widget more flexible.
  • Parameter completion is still a bit of an annoying process. We could probably come up with a strategy to make the display look a lot better here.
  • Give some tweaks and knobs for how much and what to complete (just function name vs parameters and types).

Conclusions

Rarely do I write any code that doesn’t have bugs. Now that this is landing in Builder Nightly soon, I could use some more testing and bug filing from the community at large.

I’m very happy with the improvements over the past couple of months. Between getting Clang out of process and this to allow us to make clang completion fast, I think we’re in a much better place.

We can’t get this design into older GtkSourceView releases, but we can probably look at some form of integration into what will eventually integrate with Gtk4. I would be very happy if it influenced new API releases of the library so that we don’t need to carry the implementation downstream.