GJS plugins in GNOME Builder

As I mentioned in my last post, Builder has switched to GJS as it’s dynamic language for plugins. We already support a number of compiled languages including C, C++, Rust, and Vala.

Previously we had used PyGObject. Do to the lack of GTypeInstance support in PyGObject, that isn’t an option currently. I already ported all of Builder’s plugins written in Python to C over the course of a week last summer. That ended up making things both more stable and allow us to ship the GTK 4 port on time.

This past year I wrote a new async/futures framework for GLib called libdex which provides Fibers, Futures, Channels, await, threadpools, io_uring support, and more. That tool heavily uses the same GTypeInstance features that GTK 4 uses.

GJS has improved a lot over the years due to how it is being maintained and it’s importance in the GNOME Shell stack. I’d like to double down on that so Builder can benefit from their hard work. Therefore, if you want to write plugins in JavaScript and maintain them upstream, that’s something I’m happy to see happen.

You can see some examples for how to write a JavaScript plugin for Builder in the examples directory.

GJS plugins for libpeas-2.0

One of the main features I want to land for the libpeas-2.0 ABI break is support for plugins in JavaScript.

With the right set of patches, you can get that. Thanks to Philip Chimento, GJS will hopefully soon land support for running code in a SpiderMonkey realm. Philip also did us a solid and wrote the code to exfiltrate enough GType information from an imported JavaScript module. That allows libpeas to correlate which GTypes are provided by a plugin.

With the GJS realm support in place, we can land the new GJS loader for libpeas-2.0.

My personal goal for this is to enable JavaScript-based plugins in GNOME Builder. With how much GJS has improved over the years to support GNOME Shell, it is probably our most-maintained language binding for a dynamic language with modern JIT features.

For example, if you wanted to make an addin in Builder which responded to changes of a file within the editor, you might write something like this as your plugin. Keep in mind I’m not a JavaScript developer and GJS developers may tell you there are fancy new language features you can use to simplify this code further.

import GObject from 'gi://GObject';
import Ide from 'gi://Ide';

export var TestBufferAddin = GObject.registerClass({
    Implements: [Ide.BufferAddin],
}, class TestBufferAddin extends GObject.Object {

    vfunc_language_set(buffer, language_id) {
        print('language set to', language_id);
    }

    vfunc_file_loaded(buffer, file) {
        print(file.get_uri(), 'loaded');
    }

    vfunc_save_file(buffer, file) {
        print('before saving buffer to', file.get_uri());
    }

    vfunc_file_saved(buffer, file) {
        print('after buffer saved to', file.get_uri());
    }

    vfunc_change_settled(buffer) {
        print('spurious changes have settled');
    }

    vfunc_load(buffer) {
        print('load buffer addin');
    }

    vfunc_unload(buffer) {
        print('unload buffer addin');
    }

    vfunc_style_scheme_changed(buffer) {
        let scheme = buffer.get_style_scheme();
        print('style scheme changed to', scheme ? scheme.get_id() : scheme);
    }
});

You can easily correlate that to the IdeBufferAddin interface definition.