Vooruit, ik bouw mijn eigen teksteditor!

Waarom zou ik niet mijn eigen teksteditor bouwen?

VS Code is gebouwd op de Monaco Editor, wat een "div soup hellscape" is. Ik stapte laat in op de VS Code-trein omdat mijn Intel-Mac jarenlang te traag was. Dat probleem werd opgelost toen ik Apple silicon kocht. Als dat de standaard is, heb ik veel ruimte om fouten te maken.

Canvas

Mijn eerste experiment rendert alles op een <canvas>-element.

Je ziet het niet, maar je CPU doet veel werk om dat plaatje op 60–120 frames per seconde te renderen. Het gebrek aan interactiviteit is een overduidelijk probleem voor een teksteditor.

Ik maakte een lijst van de "minimum viable" functies en implementeerde deze:

  • Pointer down om de tekstcursor te positioneren
  • Pijltjestoetsen om de tekstcursor te verplaatsen
  • De huidige regel markeren
  • Typen om tekst in te voeren
  • Een mooie cursor-animatie

Voordat je me aanspreekt over Vim-bindings: hou je mond, ik heb dringender problemen. Canvas geeft me niets gratis. Onder andere mis ik:

  • Tekstselectie
  • Undo/redo geschiedenis
  • Plakken van meerdere regels
  • Overflow-scrolling

Die laatste is cruciaal. Het leven is te kort om eigen elastische scrollbalken te implementeren. Ik besloot te sjoemelen en de native browser-overflow van een verborgen element te gebruiken. Een <div> wordt zo gepositioneerd dat deze overeenkomt met de canvas-tekst, en de scrollpositie wordt gebruikt om de render-offsets op het canvas te berekenen.

Ik ben tevreden met hoe dat vordert, maar ik ben ook ontmoedigd omdat <canvas> volledig ontoegankelijk is. Ik zou kunnen doorgaan met het toevoegen van tekstselectie en andere functies, maar ik los daarmee het fundamentele toegankelijkheidsprobleem niet op.

Ik had een beter idee.

Content editable

In plaats van tekst op het <canvas> te renderen, kan ik het gewoon native renderen in de overflow-<div> en deze bewerkbaar maken met een contenteditable-attribuut. Dat attribuut heeft een waarde voor alleen platte tekst die perfect is voor code. Alle inhoud blijft binnen één tekstnode.

<div
  contenteditable="plaintext-only"
  autocapitalize="off"
  autocorrect="off"
  spellcheck="false"
  translate="no">
  <!-- tekst komt hier -->
</div>

Attributen zoals spellcheck moeten worden uitgeschakeld om pieken in invoerlatentie te voorkomen. Raad eens hoeveel dagen ik erover deed om die oplossing te ontdekken? Dagen!

Het gebruik van contenteditable biedt native tekstselectie, undo-geschiedenis, etc. Al die toegankelijkheidsvoordelen worden gratis door de browser geregeld.

De Selection API biedt statistieken die ik gebruik om een aangepaste tekstcursor te blijven renderen. ::selection is beschikbaar, dus dat kan ik ook stylen. Ik heb de native caret-color onzichtbaar gemaakt, wat waarschijnlijk niet is toegestaan.

De contenteditable-techniek is veelbelovend, maar ik merkte vreemde prestatieproblemen bij een bepaald aantal tekens. Chromium-browsers presteren slechter dan WebKit en wat Firefox tegenwoordig ook is, maar het is onvoorspelbaar.

Textarea

Zou een eenvoudige <textarea> levensvatbaar zijn in plaats van plaintext contenteditable? Kort gezegd: ja. Het blijkt dat een <textarea> veel performanter is voor langere teksten.

In de laatste demo heb ik ook syntax-highlighting toegevoegd. Mijn oorspronkelijke plan was om custom ::highlight te gebruiken op het contenteditable-element. <textarea> kan geen CSS-highlights gebruiken, dus was er een derde laag nodig. Voor de demo heb ik wat "div soup" toegevoegd voor de zichtbare regels om MicroLighter toe te passen.

Update: Er is me verteld dat de nieuwe OpaqueRange API aangepaste highlights mogelijk maakt voor <textarea> — handig!

Update 2: En de EditContext API verbetert de <canvas>-input.

Te veel CSS-highlights vormen opnieuw een knelpunt voor de prestaties. Een robuustere oplossing zou zijn om Tree-sitter te gebruiken om een syntaxisboom te genereren en deze te doorlopen om highlights te genereren voor alleen de zichtbare regels. Ik hoopte virtualised scrolling volledig te vermijden, maar ik zou het kunnen verbeteren met de "inverse sticky" techniek. Of ik kan teruggaan naar contenteditable, omdat de bestandsgroottes die ik zou bewerken de prestatie-muur niet raken.

Conclusie

Hoe dan ook, ziet er goed uit, toch?

Het lijkt op 90% van een teksteditor met 1% van de functies. Vanaf hier is het vrij eenvoudig om "de rest van de uil te tekenen". Ik ben in verzoeking om door te gaan, maar dan denk ik aan alle kleine dingen zoals tab-indentatie. Op dit moment kap ik gewoon de tab-toets af om twee spaties in te voegen...

Mijn demo's zijn niet geoptimaliseerd en verre van perfect toegankelijk, maar tenminste begin ik niet vanuit een verloren positie. Renderen op <canvas> zou een nachtmerrie zijn.

Ik archiveer dit project voor een regenachtige dag.

JavaScript-strings en tekstbereiken werken met UTF-16 code-units. Het is makkelijk om onbedoeld bugs te introduceren. Ik weet zeker dat mijn demo's er vol mee zitten. Ik sluit af met een codevoorbeeld om de nerds te lokken:

"🍋‍🟩".length; // 5
[..."🍋‍🟩"].length; // 3

const segmenter = new Intl.Segmenter("en", {granularity: "grapheme"});
[...segmenter.segment("🍋‍🟩")].length; // 1