好吧,我要自己构建一个文本编辑器
2026年9月1日,星期二 "他们不再像Sublime Text那样制作文本编辑器" 引起了很多人的共鸣。现在的软件都是垃圾。这让我思考;我擅长建立垃圾!为什么我不能自己构建一个文本编辑器呢?VS Code是建立在Monaco Editor之上的,那是一个<span>混乱的地狱景象。我迟迟没有使用VS Code,因为多年来我那台Intel处理器的Mac运行得太慢。当我购买了苹果硅后,这个问题就解决了。如果这是标准,我还有很多犯错的空间。<br>我的第一个实验是在一个<canvas>元素上呈现所有内容。你可能看不出来,但你的CPU正在为以60-120帧每秒渲染那幅图像而忙碌。缺乏交互性对于文本编辑器来说显然是个问题。我列出了一份“最小可行”的功能清单并实现了它们:<ul><li>按下鼠标以定位文本光标</li><li>箭头键移动文本光标</li><li>高亮当前行</li><li>输入文本</li><li>花哨的光标动画</li></ul>这个下一个演示是可交互的,点击并输入。可在你@我关于Vim绑定的问题之前:闭嘴,我有更紧迫的问题。<canvas>什么都不免费给我。在许多理想功能中,我缺失了:<ul><li>文本选择</li><li>撤消/重做历史</li><li>多行粘贴</li><li>溢出滚动</li></ul>最后一个是至关重要的。生活太短暂,无法实现自定义的弹性滚动条。我决定作弊,使用隐藏元素的原生浏览器溢出。一个<div>的大小与canvas文本相匹配,滚动位置被用来计算canvas上的渲染偏移。我对进展感到满意,但也很沮丧,因为<canvas>是完全不可访问的。我可以继续添加文本选择和其他功能,但我并没有解决根本的可访问性问题。我有了一个更好的主意。<br>内容可编辑<br>与其在<canvas>上渲染文本,不如直接在溢出<div>中原生渲染,并通过contenteditable属性使其可编辑。该属性具有完美适用于代码的纯文本值。所有内容保留在一个文本节点内。<div contenteditable="plaintext-only" autocapitalize="off" autocorrect="off" spellcheck="false" translate="no"> <!-- 文本在这里 --> </div>像spellcheck这样的属性必须被禁用,以避免输入延迟峰值。想猜猜我发现这个修复花了我多少天吗?天!使用contenteditable可以获得原生文本选择和撤销历史等。因此,浏览器免费提供了如此多的可访问性好处。选择API提供了我用来继续渲染自定义文本光标的度量。::selection是可用的,所以我也可以给它设置样式。我将原生光标颜色设置为不可见,可能这是个禁忌。contenteditable技术是很有前途的,但我注意到在某些字符计数超过后会出现奇怪的性能问题。Chromium浏览器的表现比WebKit和现在的Firefox更差,但这是不可预测的。<br>文本区域<br>简单的<textarea>代替plaintext contenteditable是否可行?简而言之:是的。事实证明,<textarea>对于较长文本要高效得多。在这个最终演示中,我还添加了语法高亮。我最初的计划是在contenteditable元素上使用自定义::highlight。<textarea>无法使用CSS高亮,因此需要第三层。出于演示目的,我为可见行添加了一些<div>混合,以应用MicroLighter。编辑:有人告诉我新的OpaqueRange API为<textarea>解锁了自定义highlight——真不错!编辑2:EditContext API改进了<canvas>输入。过多的CSS高亮是另一个性能瓶颈。更稳健的解决方案是使用Tree-sitter生成语法树并遍历它,仅为可见行生成高亮。我希望完全避免虚拟化滚动,但我可以通过使用反向粘性技术来改善。如果我回到contenteditable,原因是我编辑的文件大小并没有碰到性能瓶颈。无论如何,感觉不错,对吧?看起来像是90%的文本编辑器,只有1%的功能。从这里开始,继续绘制其他部分是相当简单的。我很想继续绘制,但又想到了所有的小东西,比如制表符缩进。现在我只是劫持制表键以插入两个空格…我上面的演示是未经优化的,离完美的可访问性还远,但至少我不是从一个失败的位置开始。渲染在<canvas>上将是噩梦。我将这个项目暂时搁置以待时日。JavaScript字符串和文本范围使用UTF-16代码单元。随便引入错误是很容易的。我确信我的演示满是错误。我将以一个代码示例为结束。 " 🍋🟩 ".length; // 5 [ ... " 🍋🟩 " ].length; // 3 const segmenter = new Intl.Segmenter("en", {granularity: "grapheme"}); [ ... segmenter.segment(" 🍋🟩 ")].length; // 1
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡