Xed Plugin - maybe

Robert Crowther Jul 2025

Xed is a text editor. It’s coded for Linux Mint, but is available across Gnome desktops. It was a replacement for the earlier and similar Gedit and Pulma. How do I make a plugin for it? Before we start…

Limitations of Xed and Gtk

This is not Xed’s fault, this is missing in Gtk, the toolkit Xed is built on. You’ll see on the web the system repeatedly described as ‘powerful’. Well, it can’t be disputed that the system is heavily worked and includes all the user interface items you could ever need (like stackable signals).

But there are limitations here and there. For example, back in the old days Gtk had a grotesque progress meter which stayed there for years. And talking directly about this project, the GtkIters used to rove across the displays you look at in Xed (a TextView) are limited in search ability. Very limited. TextView was built for shunting text about, no more, so the iters can only search for a string. They can handle Gtk’s idea of a word or a sentence… but not a regex search, or even a multiple character/codepoint search. If you conceived of building a parser for Xed, get out now—EMACS this is not.

Now let’s start plugin questions…

What do I want to do?

I’d like a plugin, meaning the code would preferably work with another piece of code, Xed. My hope of achieving anything is very, very low. See my wider discussion of the plugin scene around the GIMP visual editor for why.

So I will set my expectations very low. I would like to highlight every full‐stop (‘period’) in the current text. That’s all. If, by any miracle, I get there, then I can try more.

How do I want to do it?

Most Linux Mint material is reputedly written in Python. I’d like to write in Python—‐I do not want to involve myself with C build tools for a need simple. We’ll see….

Right… I would really like this code that can highlight every full‐stop in a document. I would value that action so much that I am prepared to give up on making a Xed plugin. I will give myself fallback possibilities.

First, I could write in Python code to analyse a text file, then print in a terminal—output in the wrong place for the user, horrible to look at, no further tools available but it would work and be clean code. Second, I know that constructing a GTK interface is not so hard in Python, so I could get the output looking better like that—ironic, when Xed is likely written in Python, but it may be easier to construct my own. Two ways out.

What resources do I have?

Evil number one of plugin writing, no reasonable documentation. A blog post tells me plugins are likely kept in,

/usr/lib/x86_64-linux-gnu/xed/plugins

Turns out on my computer they are.

A look in that directory reveals worrying issues. Massive ‘plugin’ files, which seem to refer to translations—will this work without? ‘xxx.so’ files which means some of this is written in or refers to ‘c’. Thankfully, the ‘bracket complete’ plugin is written in Python. Project not crushed yet.

Source code for the originals is on GitHub, but this is Python, we can go look at the code in the folder. There’s a Plugin template for ‘c’ here—near useless.

What’s missing?

I came across a massive, self‐important post about writing an Xed highlighter in C. The post highlights what is missing. It mostly fails to answer these questions,

Is it possible to construct a plugin in‐place?

Some so‐called plugin systems are no more than an allocated folder that needs full application recompile. That’s a fail

How does a plugin get registered?

It’s pretty much a basic for a plugin that it comes with some form of data to tell the system what it is, if only a title

What hooks are available?

Plugins work by being gathered into a chain, which chain of processing is then triggered by the application code at hooked processing points. What are these points? Where is documentation?

What options are there for user triggering and preferences?

Some plugins only need switching on or off. Some will need manual triggering, ‘do this’. Some will want their own controls, ‘do this to this extent’. What user interface is available?

This project is looking bad.

Stab

I’m gonna try something stupid. Gonna close all Xed instances, including the one I’m writing this post in. Then, in the folder named above, copy the ‘bracket complete’ plugin, rename it, then restart Xed. Will Xed see the renamed plugin?

This is gonna need a terminal and permissions, Remove ‘__pycache__’ because it’s a compiled python and will be regenerated. When using ‘nano’, replace the translations for your locale, and rename the Python Module and plugin name,

cd /usr/lib/x86_64-linux-gnu/xed/plugins
sudo cp -r bracket-complete/ full-stop
cd full-stop/
sudo rm -r __pycache__/
sudo mv bracketcompletion.py fullstop.py
sudo mv bracketcompletion.plugin fullstop.plugin
sudo nano fullstop.py
sudo nano fullstop.py

This is depressing—it worked.

Edit > Preferences > Plugins

It’s there. Switch it on and it will start bracket completion.

It’s depressing because,

I learned sonething. The ‘.plugin’ file is not only for translations, it’s not optional, it registers the Python module with Xed.

Into userspace

I can address one issue straight away. Let’s try move to a userspace—copy out, softlink into the plugins space, and free up permissions so this thing can be edited. ‘cp’ because ‘mv’ will not work well if at all recursively,

sudo cp -R full-stop /home/me/PluginDev
sudo rm -R full-stop
sudo ln -s /home/rob/Code/full-stop full-stop
cd /home/me/PluginDev/
sudo chown -R me: full-stop

Now my depression has turned to trepidation. Because it worked. With near‐zero documentation, I’m heading for a waste of hours of time on a dead‐end crash.

One more step. With all Xed’s shut down, start a terminal, then launch Xed in the terminal (on some kind of text, no ‘sudo’ needed). Now, whenever we activate the plugin, we’ll see error messages pour down the terminal,

xed sometext.txt

Headscratch

How will we trigger highlighting? ‘bracket complete’ looks for keystrokes, so we could detect full‐stops as typed. But that will miss already typed stops. So maybe scan the document every time a full‐stop is typed? Inefficient, especially as we are working in Python, not ‘c‘. Course, we could make a defence—check if this has been done—but it seemed cobbled together,

Xed has a plugin for sorting selected lines—we could take that approach, a one‐off highlight is appealing, if not ideal. The ‘sort‘ plugin uses a button press, the button added to the Xed ‘Edit’ menu. But the plugin is written in ‘c’ and what are the chances I can figure out a menu addition and triggering in Python… if even possible?

Without a button. an ‘onDocumentLoad’ hook would be a slightly less‐than‐ideal but workable solution. If I could find one. As I said, I want this plugin, so anything that might work…

First hack ‐ button?

Sort puts buttons in the UI, but is written in ‘C’. The ‘’textsize’ plugin is Python, gives several clues about connecting widgets. Problem, following ‘textsize’ means casting the GObject to something else, so the ‘autoBracket’ code is history,

This will put a button in the ‘Tools’ menu and the button will trigger too. I’ve also added the gear for ‘acceleration’ (read, ‘keystrokes as alternative to button presses’), and an ‘action group’ (read, ‘items bordered in a menu list and kept together’). A lot of what you are looking at here is nothing to do with Python, or Xed, it’s the detail of Gtk,

# need all of these
from gi.repository import GObject, Gio, Gtk, Gdk, Xed

import gettext
gettext.install("xed")

# Your guess as good as mine
MENU_PATH = "/MenuBar/ToolsMenu"

# Note different inheritance, and cant have this and an Xed.ViewActivatable
class FullStopPlugin(GObject.Object, Xed.WindowActivatable):
    __gtype_name__ = "FullStopPlugin"

    window = GObject.Property(type=Xed.Window)

    def __init__(self):
        GObject.Object.__init__(self)


    def do_activate(self):
        # Insert menu items
        self._insert_menu()


    def do_deactivate(self):
        # Remove any installed menu items
        self._remove_menu()


    def on_fullstop_activate(self, action, user_data=None):
        print("activated button!")

    def _insert_menu(self):
        manager = self.window.get_ui_manager()

        # Create a new action group
        self._action_group = Gtk.ActionGroup(name="XedFullstopActions")
        self._action_group.add_actions([
            ("FullstopAction", None, _("_Fullstops"),
            "<Ctrl>3", None,
            self.on_fullstop_activate)
            ])

        # Insert the action group
        manager.insert_action_group(self._action_group)

        self._ui_id = manager.new_merge_id();

        manager.add_ui(
            self._ui_id,
            MENU_PATH,
            "FullstopAction",
            "FullstopAction",
            Gtk.UIManagerItemType.MENUITEM,
            False
            )


    def _remove_menu(self):
        # Get the GtkUIManager
        manager = self.window.get_ui_manager()

        # Remove the ui
        manager.remove_ui(self._ui_id)

        # Remove the action group
        manager.remove_action_group(self._action_group)

        # Make sure the manager updates
        manager.ensure_update()

I’m now caught between the feeling of trepidation, which has if anything increased, and a puzzle. Despite knowing the way Gtk works well enough, no way I could have got this far without the rip from ’the ’textsize’ plugin, it’s cost me a lot of time, and I’m still in the position of no‐documentation. But I do have a button that comes and goes with plugin activation, and triggers on a key press.

Get the current views

When I moved from ‘sort’ to ‘textsize’. I lost all easy connection with the views that the plugin was using. So how do I get to a view, meaning the Xed display? So I can do something there?

This is not bad. Both plugins do complex things, ‘textsize’ especially makes new signals and wires into tab creation and removal, so it can be consistent across tabs. But for now I only need a one‐off action on the current/‘active’ view.

But I know something here. The TextView is only for the display gadget itself, and includes several conveniences, as shown in the documentation. What I need, to get in detail on the text, is the TextBuffer attached—‐which is not inherited, I need to go get it.

    def on_fullstop_activate(self, action, user_data=None):
        view = self.window.get_active_view()
        buf = view.get_buffer()
        return Gdk.EVENT_STOP

Do something in the TextBuffer

Now this will be a long road. But I know what GtkTextView is like, and the Python binding too. Does that make it better, because I can get clever with some issues, or worse, because I know what can crumble to useless?

So let’s keep it basic as possible. I start by counting full stops, then printing to the terminal. Gives me a minimal fight with functions and bindings, plus guaranteed visible output.

Anyway, Gtk TextBuffer documentation. And it turned out I needed PyGObject TextIter documentation because my guessing days are over (in the past, I’ve had joy by downloading and searching the binding source)… then it turned up the documentation was out‐of‐date, so that was another battle. Try to use the builtin functions where possible, as they are in ‘c’—looping the iterator character by character in Python is asking for a performance hit.

Python people would hate this code, but whatever. It’s odd partly because the search method returns two new Iters, not updates the old Iter (so keeping Iters immutable). And the ‘forward_search’ function binding returns None when it reaches the buffer end,

    def on_fullstop_activate(self, action, user_data=None):
        ...
        # add this code
        iter = buf.get_start_iter()
        count = 0

        # could also use forward_sentence_end(), but keep more general
        while(True):
            ret = it.forward_search('.',  Gtk.TextSearchFlags.TEXT_ONLY, None)
            if ret is None:
                break
            it = ret[1]
            count = count + 1
        print("full stop count in text: " + str(count))
        return Gdk.EVENT_STOP

Well, it works. I’m now too tired to feel much. Breakfast and bin emptying for me.

Marks, tags, what?

I know the effect I want, and that’s important. Text editors look simple, but they are a stack of gear handling ‘characters’ which are in practice Unicode codepoints (hence the Iters), with partial load gearing and more. Simple, yes, but deep also.

What I’m after is an effect like a ‘find’, highlighting any matches, but the highlight disappears on a click. Don’t know if that’s a ‘tag’ or a ’mark’, both words I’ve seen, or something else… and note, it is not a ‘selection’. Time to root in source code for ‘find’ which is called ‘search’ and is not a plugin.

Te code has a signal to update counts—maybe a statusbar display would be a nice addition anyhow, but unless I can find a signal, there will not be a Python wrap? And a lot of extra code for search replacements, which I don’t need. Uh hu, it uses a Gnome class called SearchContext near a GtkSourceView.

I ’fess, I want to do more than a generic highlight of all full‐stops. I’d like different types of marks for different types of search results. Looks like I may need to dig into Gnome or Gtk source code for this, to see how they do it underneath—the road is long.

Well, looks like Tags apply text changes like highlighting—see the text widget overview. And then the overview delivers an example, needing only some Python guesswork to deliver,

    def on_fullstop_activate(self, action, user_data=None):
        ...
        # add this code

        # Make this on the buffer
        tag0 = buf.create_tag (
            "blue_background",
            background= "DodgerBlue"
            )

        it = buf.get_start_iter()
        count = 0

        # could also use forward_sentence_end()
        while(True):
            ret = it.forward_search('.',  Gtk.TextSearchFlags.TEXT_ONLY, None)
            if ret is None:
                break

            # ret[1] is end of match, start next search from there
            it = ret[1]

            # apply the tag between the buffer points
            buf.apply_tag (tag0, ret[0], ret[1])
            count = count + 1

I’m getting someplace here. Some things I need… to remove the tags on a keypress is one (and maybe a tab switch also, if I could)?

Removing marked text

Xed ‘search’ offers removal if the text is altered, so fails the search, or the search bar is closed. Removal on alteration is already working, due to buffer changes killing the tags. When else would be a useful time to remove the marks? I’d suggest soon as the TextView is clicked on, as the marks offer no interactivity, and the user can always scroll about to look at results.

So, removal on click. How is that done? Temp connection of a signal… Except it’s not a ‘key‐press‐event’ like my model plugin, it’s more of a ‘motion‐event’ to detect mouse‐clicks also, minimal‐documented deep in Gdk, not Gtk, the forms not documented anyplace I can find. Let’s try something from elsewhere on the web.

We only connect while the text is highlighted, then dispose of the connection,

    def connect(self, view):
        self._handlers[0] = view.connect(
            # or try 'key-press-event'
            'button-press-event',
            self.on_key_press_event
            )

    def disconnect(self, view):
        view.disconnect(self._handlers[0])
        self._handlers[0] = None

    def on_fullstop_activate(self, action, user_data=None):
        ...
        self.connect(view)
        return Gdk.EVENT_STOP

    def on_key_press_event(self, view, event):
        # ignore CNTRL and ALT presses
        if event.state & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.MOD1_MASK):
            return False

        self.on_fullstop_deactivate()
        self.disconnect(view)

        # The boolean return is to express propagation of this
        # event. For this usage, I think we will allow further
        # propagation
        return True

The mask code got ripped with the other plugin code. See the documentation buried in Gdk. I’ll leave it as an example of how Gtk/Gdk code is deep.

More to do?

Oh, plenty,

Not addressed if these views are editable

It’s view.get_editable()

No handling of preferences

Can it be done in Python? It’s a matter of getting that ‘Preferences’ dialogue button reacting. Only ‘spellchecker’ and ‘datetime’ offer this, both in ‘c’. Could load a file for config…

The GUI should offer ‘search within selection’

Possible. An extra menu button would be ok

With any extra parsing, the search code will be slow

Especially as it’s implemented in Python. Ha, Ha, async Python/Gtk documentation. But async may not be an answer, bear in mind ‘fullstop’ is modifying the UI, so it may not multi‐task, iters wandering etc.

Ought to tidy signals

There’s more to signals than what I’ve ripped, much more. Though the universal button press capture with no propagation works well

Good to access the statusbar

…but I don’t see that happening in Python

Could translate everything

…but why? I mean, who cares?

There’s evil in the code

See if I care. I only got one life

Rumbles

Well, it didn’t go bad as expected—this plugin works though hangs on long texts—more than I can say for most apps. The plugin code is presented as a box you can throw into the appropriate folder, which would give the Xed guys shock and the Gtk code is rotten, but it’ll work for you and anyone else if you change computer. Far short of polish, but viable.