How we integrated a code editor on the Miro canvas

Bogdan Zvyagintsev
Miro Engineering
Published in
7 min readOct 30, 2023

Recently we shared why we developed a code block widget in Miro. We explained what a code widget is, the context for developing a code block in Miro, a few options we considered based on the architecture and limitations, and the final approach we settled on. In this post, we’ll continue the story and share how we actually integrated the code editor onto the Miro canvas.

Code editor options

Surprisingly, there are limited options when it comes to web-based code editors. The code editors most often used are CodeMirror 5 and 6, Monaco, and Ace. There are pros and cons to each option, as some have outlined. This article compares Ace, CodeMirror, and Monaco, while this one favors CodeMirror.

Given that we wanted to build our code block for Miro and not a full-powered IDE, we had really simple functional requirements: syntax highlighting, line numbers, indentation, and auto-closing brackets. All of the code editors provided an acceptable solution to the problem, but the devil is in the details. Miro is often used for fuzzy and creative work: you may want to create tons of widgets of different forms and sizes, requiring a lot of flexibility from the tools such as scaling, resizing, and performance.

Looking for the right tool, we ran into several critical challenges that needed to be resolved before we could integrate a web-based code editor on our canvas.

Technical considerations

The bundle size

The bundle size can affect the load time, impacting the user experience. That’s why the editor’s size was a crucial consideration for us. For example, at the time of development, the minimum minified + gzipped bundle of the Monaco editor was > 2MB, Ace was 98KB, CodeMirror 5 was 55.7KB, and CodeMirror 6 was 124KB. In the case of Monaco, integrating the 2MB library for a really simple functionality seemed excessive. Thus, we focused our further research on the CodeMirror and Ace editors.

Bar chart showing 4 code editor options in the X-axis and the bundle size in the y-axis, used for comparing the options

Scaling support

When we started experimenting with web-based code editors in Miro, we realized the importance of the editor’s ability to scale. The scaling of the rendered widgets is managed by our canvas engine, but in Edit mode we need to scale the HTML element with the editor manually. The scale factor is a multiplication of the board scale (zoom in/out) and the widget scale (widget size). To scale the widget, we apply the CSS property on the editor container: `transform: scale($scale)`.

During the prototyping with CodeMirror 5 and CodeMirror 6, we met this problem: if the editor is scaled, then the mouse events’ positions, as well as the visual elements’ sizes and positions are calculated wrong. Attempts to deal with those issues were unsuccessful: there is no easy fix or built-in support for this in CodeMirror.

GIF of CodeMirror editor showing the scaling issue.

This happens because the visual part of the editor (including the selection) is a tree of HTML elements. In CodeMirror, all the calculations of the mouse events’ positions, and the visual elements’ sizes and positions are based on the `getBoundingClientRect()` method, which doesn’t consider CSS transformations.

We discovered that the Ace and Monaco editors, on the other hand, support the CSS transformations. Here’s an example of the scaling and rotating support in Ace:

GIF demonstrating the Ace editor supporting scaling and rotating CSS transformation.

Static syntax highlighting

We decided to store plain text and needed an HTML string with inline styles to render text on the canvas. That’s why it was important that the code editor had a built-in API or a separate library for static syntax highlighting — so we could highlight the plain text without initializing the editor, and have the same highlighting on the canvas as in the editor.

To highlight the plain text, we’re using the native static highlighter extension for the Ace editor. It starts a minimal version of the Ace editor that outputs the HTML string. So, the line of code `console.log(‘ThisIsAVeryLongLine’)` will be transformed by the static highlighter extension into the following piece of code:

<div class="line">
<span class="ace_type">console</span>
<span class="ace_operator">.</span>
<span class="ace_function">log</span>
<span class="ace_paren">(</span>
<span class="ace_string">'ThisIsAVeryLongLine'</span>
<span class="ace_paren">)</span>
</div>

But some logic didn’t fit our requirements:

  • It doesn’t support line wrapping
  • Line numbers are built using the CSS
  • Highlighting is applied by the class names

To address these problems, our goal was to transform the output of the Ace extension.

Tuning the Ace extension

Out of the box, this extension doesn’t support width property, so it outputs code ignoring line wrapping: each line is basically placed in one div element. That creates an inconsistent view between the Edit and View modes if the line doesn’t fit the width of the widget because our rendering engine doesn’t know anything about the line wrapping and indentation logic in the code editor, so it wraps lines when they reach the right widget border (similar to CSS property `word-break: break-all;`):

Ace editor versus Miro rendering engine

However, we discovered that it’s possible to set the line limit property (the number of characters in a line) in the Ace editor from the extension. Though it’s not the same as adjusting the width, we can easily calculate the line limit since we use a monospaced font. So, with some modifications to this extension, we were able to split the output by lines correctly.

To support line numbers, we changed the group element from <div> to <li>, so we could reuse our existing logic for rendering numbered lists in the Text widget to render line numbers for the code block.

<li class="line">
<span class="ace_type">console</span>
<span class="ace_operator">.</span>
<span class="ace_function">log</span>
<br/>&nbsp;&nbsp;&nbsp;&nbsp;
<span class="ace_paren">(</span>
<span class="ace_string">'ThisIsAVeryLongLine'</span>
<span class="ace_paren">)</span>
</li>

Mapping inline styles

This extension outputs HTML with class names, which means we needed to map them to inline styles manually. Many existing libraries were too heavy and relied on the DOM, adding to the complexity of the process (e.g. inline-css or Juice). The complexity is connected with the CSS Specifity, for example, classes have more weight than tags, nested styles, and so on.

Luckily for us, the logic of highlighting in Ace is simple and the traversal through highlighted words — mapping their classes to corresponding styles and applying them back to the elements — does the job.

<li class="line">
<span style="color: #66D9EF">console</span>
<span>.</span>
<span style="color: #66D9EF">log</span>
<br/>&nbsp;&nbsp;&nbsp;&nbsp;
<span>(</span>
<span style="color: #E6DB74">'ThisIsAVeryLongLine'</span>
<span>)</span>
</li>

Resizing

Resizing text-based widgets, especially the code widget, is an expensive and frequent operation that impacts performance significantly. Even though we use a static highlighter and don’t initialize the editor for code re-highlighting, we still faced performance issues during resizing operations.

We use line wrapping: if the line doesn’t fit the widget’s width, it wraps to the next line. Thus, on the width changes, we need to re-highlight the entire code and re-render the widget. This operation is not only expensive but also very frequent — to provide a smooth user experience it should run multiple times during resizing. Additionally, resizing can be performed for multiple widgets at once. Optimizations like debouncing or throttling don’t solve the problem and affect the user experience’s smoothness.

As such, we decided to create a simplified algorithm without static highlighting: this would calculate line wrapping indexes and indentation size, and transform a text string to make it look consistent with the highlighted version.

GIF demonstrating the code block editor supporting resizing.

We believe that during resizing, proper and visually recognizable line wrapping is more important than having syntax highlighting. Also, in terms of performance, now resizing the code widget works as fast as resizing simple text, and it’s possible to resize multiple widgets at once without performance drawdowns.

Setting the caret in the right place

When you click on the widget, you expect that the caret in the editor will appear in the place of the click. But at the time of the click, there is no editor yet and the click actually happens on the canvas, and then the editor is loaded and placed on top of the widget.

Knowing the screen coordinates of the click and using the Ace API, we could convert the screen coordinates to the line and column index in the editor, and move the caret in the right place when the editor is loaded. So, for the users transitioning between modes, the positioning of the caret happens very smoothly.

GIF showing the code block editor supporting smooth caret positioning when transitioning between modes.

Final thoughts

Although each editor provides similar and sufficient functionality, some important things may differ and can even completely block the implementation, as was the case with CodeMirror and scaling support. Depending on the architecture of the canvas, one or another editor may suit better.

For us, although it was less attractive at the beginning, the Ace editor was the best and the only option. It is very lightweight, supports scale out of the box, has the static syntax highlighting extension, and has been battle-tested for more than a decade. We managed to solve all our challenges and successfully integrated it into the canvas’ general use cases.

In the end, we were able to develop a code block with unique features and a great editing experience.

Stay tuned for the third and final part of this blog series in which we’ll share the research behind our approach. Until then, feel free to share your thoughts as a comment below. And of course, don’t forget to try Miro or the Miro Developer Platform, where you can build apps on top of Miro.

--

--