Identifying a low level editor bug by inspecting wrapped Rust objects

For quite some time we had an issue in the text editor related to the cursor getting stuck when it was at the end of a paragraph. It was annoying, but we did not know how to address it and it stayed open for half a year.

We knew that the issue was likely due to something in the lower levels, but we did not know what it could be. We did not even have a hypothesis.

Indeed, Glamorous Toolkit relies on Rust plugins. For example, the current implementation of the text editor is based on a wrapper over Rust objects. This is nice because it allows us to reuse external libraries. At the same time, to find issues visible in the high level editor, we need to understand how the low level objects work. To this end, we need at least the ability to inspect.

It was only until we extended the inspector to allow us to explore Rust objects that we could find the issue. The screenshot below shows the actual exploration that led to the bug resolution.

A screenshot of the inspection session that led to the bug resolution. The inspector shows the Pharo object and the wrapped Rust object in a seamless exploration

Inspecting the SkiaParagraph SkiaExternalObject subclass: #SkiaParagraph instanceVariableNames: '' classVariableNames: '' package: 'Sparta-Skia-External' instance shows an Info view that is actually served by the wrapped Rust object. We can see how selecting the Line ranges item, leads to an inspector over a Rust object.

So, what was the issue? Looking at the line ranges, we notice that index 6 is both the end of the first interval, and the beginning of the second interval. Once we saw this, it became obvious that 6 cannot belong in both. It turned out that the end of the interval was expected by the low level library to be exclusive, while our implementation assumed it is inclusive. Once we saw the problem, the fix was rather straightforward.

The relevant fix was mostly changing the condition from <= to <

The differentiator between not knowing how to approach the problem at all and actually solving it was just a matter of seeing the problem in the first place. That is the power of Moldable Development.

Reproducing the exploration

Consider this editor:

stream := BlTextStream new.
stream glamorousRegularFont fontSize: 40.
stream next
	putAll: 'This is a piece of text in an editor. The editor is made out of '.
stream next
	foreground: Color blue;
	putAll: 'Pharo'.
stream next
	putAll: ' paragraph objects. These paragraph objects have correspondent objects in the '.
stream next
	foreground: Color red;
	putAll: 'Rust'.
stream next putAll: ' world which can be inspected seamlessly, too.'.
editor := BrEditor new
		text: stream contents;
		aptitude: BrGlamorousRegularEditorAptitude new

The editor is made out of paragraph elements. In the current implenentation they are instances of BrTextEditorParagraphElement BrTextEditorSegmentElement subclass: #BrTextEditorParagraphElement instanceVariableNames: 'paragraph text cursorElements cursorStencil selection segment' classVariableNames: 'Slants Stretches' package: 'Brick-Editor-UI' . To draw themselves, these objects depend on SkiaParagraph SkiaExternalObject subclass: #SkiaParagraph instanceVariableNames: '' classVariableNames: '' package: 'Sparta-Skia-External' instances that are actually wrappers over Rust objects.

How does it work? Follow this:

First, inspect the editor above to make it render.

Then let's inspect one of the children and access the lower SkiaParagraph SkiaExternalObject subclass: #SkiaParagraph instanceVariableNames: '' classVariableNames: '' package: 'Sparta-Skia-External' instance:

editor children first buildParagraph layoutWithWidth: 400

The resulting inspector has an R icon. If you press it, you will get to the Rust object.