<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Michael Sjoberg</title>
    <description>The latest articles on DEV Community by Michael Sjoberg (@michaelsjoberg).</description>
    <link>https://dev.to/michaelsjoberg</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4130864%2F4f7dacb5-b4a4-4008-8971-7b607455d0c4.png</url>
      <title>DEV Community: Michael Sjoberg</title>
      <link>https://dev.to/michaelsjoberg</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/michaelsjoberg"/>
    <language>en</language>
    <item>
      <title>Lexer design in Koi Editor</title>
      <dc:creator>Michael Sjoberg</dc:creator>
      <pubDate>Sat, 19 Sep 2026 04:03:01 +0000</pubDate>
      <link>https://dev.to/michaelsjoberg/lexer-design-in-koi-editor-4nck</link>
      <guid>https://dev.to/michaelsjoberg/lexer-design-in-koi-editor-4nck</guid>
      <description>&lt;p&gt;Koi uses small handwritten lexers instead of syntax definitions or syntax trees. This provides direct control over highlighting and folding, with very little work at runtime.&lt;/p&gt;




&lt;p&gt;Sublime Text uses declarative regex/context syntax definitions.&lt;/p&gt;

&lt;p&gt;A .sublime-syntax file describes matches, scopes, and state transitions, which Sublime's syntax engine then executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;syntax definition -&amp;gt; generic syntax engine -&amp;gt; tokens and scopes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zed uses Tree-sitter grammars to parse the document into a syntax tree, then runs Tree-sitter queries such as highlights.scm against that tree.&lt;/p&gt;

&lt;p&gt;The same parsed structure can support highlighting, outlines, bracket matching, indentation, text objects, and other features:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Tree-sitter grammar -&amp;gt; Tree-sitter parser -&amp;gt; syntax tree -&amp;gt; queries -&amp;gt; tokens and structure
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Xcode's syntax highlighting pipeline is proprietary, so I don't really know how it works.&lt;/p&gt;

&lt;p&gt;Koi does something much more direct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;language-specific lexer -&amp;gt; tokens and fold levels
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are no generic grammars or regex definitions sitting between the text and the highlighting engine.&lt;/p&gt;

&lt;p&gt;Basically, Koi's lexers are ordinary code. Each language has a small purpose-built lexer that directly decides how text should be styled and folded.&lt;/p&gt;

&lt;h3&gt;
  
  
  Syntax-aware folding
&lt;/h3&gt;

&lt;p&gt;Syntax-aware folding is useful when folding can't be inferred from indentation alone.&lt;/p&gt;

&lt;p&gt;For example, this code should still be foldable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nTitle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="n"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zTitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mem&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nTitle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sublime is interesting because its folding isn't really defined by .sublime-syntax.&lt;/p&gt;

&lt;p&gt;It does correctly fold the flat code block above, although I'm not sure where or how this is implemented internally.&lt;/p&gt;

&lt;p&gt;Zed still appears to use indentation-based folding by default. There has been &lt;a href="https://github.com/zed-industries/zed/discussions/31395" rel="noopener noreferrer"&gt;work&lt;/a&gt; on syntax-aware folding using Tree-sitter folds.scm queries.&lt;/p&gt;

&lt;p&gt;However, as of today, using the current version of Zed, the flat code block above does not fold for me.&lt;/p&gt;

&lt;p&gt;Here's another set of examples to determine the folding logic used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In plain text, Sublime and Zed both folds first, but not second and third.&lt;/p&gt;

&lt;p&gt;In Python, even if this is not valid syntax, Sublime folds first and second, but not third.&lt;/p&gt;

&lt;p&gt;Zed still folds first, but not second and third.&lt;/p&gt;

&lt;p&gt;Koi does not fold any of these examples in plain text, since it's plain text.&lt;/p&gt;

&lt;p&gt;In Python, Koi folds all three since I have enabled folding on sets and do not really care if the surrounding syntax is valid or not.&lt;/p&gt;

&lt;p&gt;Here's another indentation only example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;test&lt;/span&gt;
    &lt;span class="s"&gt;foo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In C, Sublime and Zed both folds this indented code.&lt;br&gt;
Koi does not fold this.&lt;/p&gt;

&lt;p&gt;This makes me think Sublime and Zed folding is based on delimiters and indentation.&lt;/p&gt;

&lt;p&gt;There seems to be some language-specific rules in Sublime on indentation in languages like Python.&lt;/p&gt;
&lt;h3&gt;
  
  
  Syntax rules as code
&lt;/h3&gt;

&lt;p&gt;Implementing syntax rules as code avoids having to figure out how to express an unusual condition in a grammar, regex state system, Tree-sitter query, or generic folding configuration.&lt;/p&gt;

&lt;p&gt;If I want some unusual contextual condition for highlighting or folding, I simply write it.&lt;/p&gt;

&lt;p&gt;In recent benchmarks, Koi continues to apply syntax highlighting to files containing millions of lines, while Sublime and Zed become unusable or fail much earlier.&lt;/p&gt;

&lt;p&gt;I think part of the reason is that Koi deliberately does very little work when lexing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What kind of token is this?&lt;/li&gt;
&lt;li&gt;What state should I carry forward?&lt;/li&gt;
&lt;li&gt;What is the fold level?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sometimes doing less work by default is the performance optimization.&lt;/p&gt;

&lt;p&gt;For example, adding a triple quote near the top of a large Python file, in this case 100,000 lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
def main(): return
def main(): return
def main(): return
def main(): return
def main(): return
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;should immediately change the syntax state of every following line until the next triple quote.&lt;/p&gt;

&lt;p&gt;Koi and Sublime both update the highlighting immediately, even on large files. &lt;/p&gt;

&lt;p&gt;Zed does this correctly on smaller files, but at 100,000 lines I don't see the highlighting update throughout the document.&lt;/p&gt;

&lt;p&gt;I don't know whether this is a Tree-sitter limitation, a Zed performance optimization, or something else in Zed's highlighting implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  The case for handwritten lexers
&lt;/h3&gt;

&lt;p&gt;I like handwritten lexers because they're simple, fast, and give me complete control.&lt;/p&gt;

&lt;p&gt;They also seem to provide great performance, but there is the obvious tradeoff where adding a new language takes more work.&lt;/p&gt;

&lt;p&gt;I like performance, so I think the tradeoff is worth it.&lt;br&gt;
The good thing is that I only need to implement each lexer once.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>texteditors</category>
      <category>performance</category>
    </item>
  </channel>
</rss>
