[{"data":1,"prerenderedAt":563},["ShallowReactive",2],{"seite-\u002Fen\u002Fopen-source\u002Feditor-suche\u002F":3},{"id":4,"title":5,"body":6,"description":554,"extension":555,"meta":556,"navigation":557,"path":558,"robots":559,"seo":560,"stem":561,"__hash__":562},"seiten_en\u002Fopen-source\u002Feditor-suche.md","Seven months searching for an editor",{"type":7,"value":8,"toc":544},"minimark",[9,13,17,20,25,28,31,35,38,58,72,76,79,87,90,94,97,297,304,308,314,321,332,336,341,352,358,487,494,497,504,507,511,514,518,521,532,540],[10,11,5],"h1",{"id":12},"seven-months-searching-for-an-editor",[14,15,16],"p",{},"VS Code was my first and only editor. But its performance bothered me:\nsometimes it uses an enormous amount of CPU and memory. So I started looking\nfor something faster.",[14,18,19],{},"My requirements were modest: preview for PDF and images, a terminal, and Git\nin the editor.",[21,22,24],"h2",{"id":23},"terminal-editors-are-fast-but-blind","Terminal editors are fast, but blind",[14,26,27],{},"The first path led into the terminal. Speed is not a problem there. Preview\nis: no image, no PDF, no rendered Markdown.",[14,29,30],{},"So I tried to retrofit it. In Fresh, the plugins are too limited for that. In\nLazyVim it works, but not everywhere. Not every terminal cooperates, and it\nwasn't stable either: LazyVim crashed on image previews in the explorer. Above\nall of it hung the suspicion that sooner or later I would hit a ceiling built\ninto the terminal itself.",[21,32,34],{"id":33},"swap-the-shell-instead-of-patching-the-plugin","Swap the shell instead of patching the plugin",[14,36,37],{},"That turned into my first own approach, and it reverses the task. If the\npreview doesn't fit into the terminal, then the terminal editor has to go\ninto a window.",[14,39,40,41,45,46,49,50,53,54,57],{},"Two attempts went in this direction. ",[42,43,44],"code",{},"neoview"," embeds Neovim via ",[42,47,48],{},"nvim-rs","\nin a UI built with gpui, the framework Zed is built with. The UI talks to\nNeovim over msgpack-rpc. ",[42,51,52],{},"freshview"," does the same with the editor Fresh, only\nmore tightly: Fresh runs as a library in the same process, egui provides the\nwindow, ",[42,55,56],{},"egui_ratatui"," translates in between, and mupdf draws PDF and images\nin floating windows next to it.",[14,59,60,61,64,65,68,69,71],{},"The next attempts, ",[42,62,63],{},"flexed"," and ",[42,66,67],{},"editor-framework",", are no longer wrappers but\nscaffolds on gpui in which nothing is core and everything is a plugin. In\n",[42,70,67],{}," the plugins are written in Lua and can be reloaded without\na rebuild.",[21,73,75],{"id":74},"why-rust-was-dropped","Why Rust was dropped",[14,77,78],{},"Zed is a good editor, but without PDF preview. And Zed is written in Rust.",[14,80,81,82,64,84,86],{},"Rust was too slow to build for me. ",[42,83,44],{},[42,85,52],{}," had their\ndifficulties, sluggish rendering here, a crash when closing the PDF preview\nthere. That might have been fixable, or maybe not. Something else decided it:\nwith these compile times I wasn't making progress fast enough. And gpui is\nRust, so it went too.",[14,88,89],{},"What I was looking for was another fast language that could compete with Zed.\nGo seemed dated, V too little known, and Zig was just breaking through as a\ncompetitor to Rust. So Zig, and there builds are pleasantly faster for me.",[21,91,93],{"id":92},"eleven-attempts-eight-uis","Eleven attempts, eight UIs",[14,95,96],{},"Each row is its own repo, in the order in which they were created. Some lived\nfor a day, some for a week. All eleven fall within five weeks between early\nMarch and early April 2026.",[98,99,100,122],"table",{},[101,102,103],"thead",{},[104,105,106,110,113,116,119],"tr",{},[107,108,109],"th",{},"Started",[107,111,112],{},"Attempt",[107,114,115],{},"Language",[107,117,118],{},"UI",[107,120,121],{},"Reason for stopping",[123,124,125,142,155,168,182,197,214,231,248,265,281],"tbody",{},[104,126,127,131,133,136,139],{},[128,129,130],"td",{},"March 2",[128,132,44],{},[128,134,135],{},"Rust",[128,137,138],{},"gpui",[128,140,141],{},"Build times",[104,143,144,146,148,150,153],{},[128,145,130],{},[128,147,52],{},[128,149,135],{},[128,151,152],{},"egui, egui_ratatui",[128,154,141],{},[104,156,157,160,162,164,166],{},[128,158,159],{},"March 8",[128,161,63],{},[128,163,135],{},[128,165,138],{},[128,167,141],{},[104,169,170,173,175,177,180],{},[128,171,172],{},"March 9",[128,174,67],{},[128,176,135],{},[128,178,179],{},"gpui, mlua",[128,181,141],{},[104,183,184,187,190,192,195],{},[128,185,186],{},"March 14",[128,188,189],{},"slint-rust-editor",[128,191,135],{},[128,193,194],{},"Slint",[128,196,141],{},[104,198,199,202,205,208,211],{},[128,200,201],{},"March 15",[128,203,204],{},"mojo-nuklear-editor",[128,206,207],{},"Mojo, C bridge",[128,209,210],{},"Nuklear",[128,212,213],{},"Mojo unsuitable for writing editors",[104,215,216,219,222,225,228],{},[128,217,218],{},"March 17",[128,220,221],{},"slint-editor",[128,223,224],{},"Mojo",[128,226,227],{},"Slint via Python",[128,229,230],{},"Slint only reachable via Python",[104,232,233,236,239,242,245],{},[128,234,235],{},"March 22",[128,237,238],{},"v-gui-editor",[128,240,241],{},"V",[128,243,244],{},"V's GUI framework",[128,246,247],{},"Scrolling broke after adding explorer and tabs",[104,249,250,253,256,259,262],{},[128,251,252],{},"March 25",[128,254,255],{},"zed-clone",[128,257,258],{},"Zig",[128,260,261],{},"dvui",[128,263,264],{},"Crash when scrolling, large files slow",[104,266,267,270,273,275,278],{},[128,268,269],{},"March 26",[128,271,272],{},"sokol-nanovg-zig-editor",[128,274,258],{},[128,276,277],{},"sokol, NanoVG",[128,279,280],{},"Font gone after switching to Vulkan, sokol's game loop cost CPU",[104,282,283,286,289,291,294],{},[128,284,285],{},"April 2",[128,287,288],{},"qt-ziged",[128,290,258],{},[128,292,293],{},"Qt 6 via libqt6zig",[128,295,296],{},"Remained a technical demonstration",[14,298,299,300,303],{},"On April 3, ",[42,301,302],{},"zid"," began. There has been no new attempt since.",[21,305,307],{"id":306},"what-came-of-it","What came of it",[14,309,310,311,313],{},"The editor is called ",[42,312,302],{}," and is written in Zig. It draws via wgpu, with the\nrenderer locked to the Vulkan backend. Layout is done by Clay. Text rendering\nis my own: glyph atlas and GPU renderer on top of FreeType.",[14,315,316,317,320],{},"The editor itself is my own: input, editing commands, search, line wrapping,\nline numbers, key bindings. Underneath lies third-party code, and deliberately\nso. Text storage is handled by ",[42,318,319],{},"flow-core",", the core library of the editor\nFlow Control, with buffer, cursor and selection types. Syntax highlighting\ncomes from the same house via tree-sitter. The terminal is Ghostty's\nemulation, Markdown is rendered by zigdown, PDF is drawn by mupdf.",[14,322,323,324,64,326,328,329,331],{},"That is a different cut than in ",[42,325,44],{},[42,327,52],{},". There, the whole\neditor was third-party and only the shell was my own. In ",[42,330,302],{}," it is the\nother way around: the editor is my own, and the third-party parts are the\nbuilding blocks underneath that have nothing to do with editing.",[21,333,335],{"id":334},"rules-belong-in-the-code-not-in-the-prompt","Rules belong in the code, not in the prompt",[14,337,338,340],{},[42,339,302],{}," has a built-in agent that works against a local language model,\nllama-server with Qwen3-4B. An agent in the editor needs tools, and tools\nneed limits. The obvious way is to write the limits into the system prompt.\nWith a model of this size, though, a prompt line is only a request, and it\ncosts space in a context of 8192 tokens on every request. That is why the\nlimits live in the code. There, however, they have to be airtight.",[14,342,343,344,347,348,351],{},"An example. Anyone who wants to overwrite an existing file gets a\nconfirmation dialog. The model bypassed it on the first try: instead of\n",[42,345,346],{},"write_file"," it sent a ",[42,349,350],{},"replace_text"," whose search text was the entire file.\nFormally not an overwrite, in practice exactly that.",[14,353,354,355,357],{},"Since then, ",[42,356,350],{}," also counts as an overwrite as soon as it replaces\nhalf the file or more:",[359,360,365],"pre",{"className":361,"code":362,"language":363,"meta":364,"style":364},"language-zig shiki shiki-themes github-light github-dark","\u002F\u002F\u002F replace_text mit `old` = (fast) ganzer Datei ist ein verkapptes Überschreiben und\n\u002F\u002F\u002F braucht dieselbe Bestätigung wie write_file auf eine bestehende Datei.\npub fn replaceCountsAsRewrite(file_len: usize, old_len: usize) bool {\n    if (file_len == 0) return false;\n    return old_len * 2 >= file_len;\n}\n","zig","",[42,366,367,376,382,429,458,481],{"__ignoreMap":364},[368,369,372],"span",{"class":370,"line":371},"line",1,[368,373,375],{"class":374},"sJ8bj","\u002F\u002F\u002F replace_text mit `old` = (fast) ganzer Datei ist ein verkapptes Überschreiben und\n",[368,377,379],{"class":370,"line":378},2,[368,380,381],{"class":374},"\u002F\u002F\u002F braucht dieselbe Bestätigung wie write_file auf eine bestehende Datei.\n",[368,383,385,389,392,396,400,404,407,410,413,416,418,420,423,426],{"class":370,"line":384},3,[368,386,388],{"class":387},"szBVR","pub",[368,390,391],{"class":387}," fn",[368,393,395],{"class":394},"sScJk"," replaceCountsAsRewrite",[368,397,399],{"class":398},"sVt8B","(",[368,401,403],{"class":402},"s4XuR","file_len",[368,405,406],{"class":398},": ",[368,408,409],{"class":387},"usize",[368,411,412],{"class":398},", ",[368,414,415],{"class":402},"old_len",[368,417,406],{"class":398},[368,419,409],{"class":387},[368,421,422],{"class":398},") ",[368,424,425],{"class":387},"bool",[368,427,428],{"class":398}," {\n",[368,430,432,435,438,440,443,447,449,452,455],{"class":370,"line":431},4,[368,433,434],{"class":387},"    if",[368,436,437],{"class":398}," (",[368,439,403],{"class":402},[368,441,442],{"class":387}," ==",[368,444,446],{"class":445},"sj4cs"," 0",[368,448,422],{"class":398},[368,450,451],{"class":387},"return",[368,453,454],{"class":387}," false",[368,456,457],{"class":398},";\n",[368,459,461,464,467,470,473,476,479],{"class":370,"line":460},5,[368,462,463],{"class":387},"    return",[368,465,466],{"class":402}," old_len",[368,468,469],{"class":387}," *",[368,471,472],{"class":445}," 2",[368,474,475],{"class":387}," >=",[368,477,478],{"class":402}," file_len",[368,480,457],{"class":398},[368,482,484],{"class":370,"line":483},6,[368,485,486],{"class":398},"}\n",[14,488,489,490,493],{},"The same applies to paths. Instead of telling the model to stay inside the\nproject, a function resolves the path and returns nothing if it lies outside.\nA path that leads out of the project via ",[42,491,492],{},".."," or as an absolute path then\ncannot produce any file operation at all.",[14,495,496],{},"Rules like these are pure functions, and pure functions can be tested. Both\nhave unit tests. On top of that there is an end-to-end script that runs the\neditor headless against the real model and checks the rejection path: request\nan overwrite, wait for the dialog, reject, file unchanged, and a path outside\nthe project gets refused. A prompt line you can only hope for.",[14,498,499,500,503],{},"Conversely, the same holds for capabilities. The ",[42,501,502],{},"command"," tool gets its\nchoices generated from the editor's command enumeration. Every menu and every\nkeyboard shortcut is thus automatically reachable for the agent, without a\nsecond list having to be maintained anywhere.",[14,505,506],{},"The question of which window a file opened by the agent appears in is also\nnot an instruction to the model, but a function with a unit test. The system\nprompt has shrunk to three sentences: the role, the convention that paths are\nmeant relative to the project, and the request to answer briefly in the\nuser's language after tool results. Everything else lives in the code, where\nit is checked, instead of in the prompt, where it is requested.",[21,508,510],{"id":509},"what-remains-open","What remains open",[14,512,513],{},"There are limits that stay in place deliberately. The editor loads exactly\none font face, which is why the Markdown view shows bold and italic through\ncolors instead of real font faces. Points like these are recorded as\ndecisions in the project documentation, not as open tasks.",[21,515,517],{"id":516},"where-it-stands-today","Where it stands today",[14,519,520],{},"Of the three requirements from the introduction, two are met: PDF and images\nopen in a tab, the terminal is built in. Git is only halfway there. The\nexplorer shows the status of every file, diff and blame are still missing.\nAdded along the way were things that weren't on the list: Markdown preview,\nslides from Markdown with export to PDF, go to definition via a language\nserver, quick open and command palette, and the agent. It is built for Linux\nand Windows.",[14,522,523,524,531],{},"The source code is open: ",[525,526,530],"a",{"href":527,"rel":528},"https:\u002F\u002Fgithub.com\u002Fgstrainovic\u002Fzid",[529],"nofollow","github.com\u002Fgstrainovic\u002Fzid",".\nThe eleven attempts before it are public as well, each under its name from\nthe table.",[14,533,534,535,539],{},"How the agent operates and checks the editor without a window is covered in\nthe next article: ",[525,536,538],{"href":537},"\u002Fen\u002Fopen-source\u002Fki-testet-ohne-fenster\u002F","An AI agent tests my editor without a window",".",[541,542,543],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":364,"searchDepth":378,"depth":378,"links":545},[546,547,548,549,550,551,552,553],{"id":23,"depth":378,"text":24},{"id":33,"depth":378,"text":34},{"id":74,"depth":378,"text":75},{"id":92,"depth":378,"text":93},{"id":306,"depth":378,"text":307},{"id":334,"depth":378,"text":335},{"id":509,"depth":378,"text":510},{"id":516,"depth":378,"text":517},"From VS Code through terminal editors and gpui to Zig. Eleven attempts and why tool limits for an AI agent belong in the code.","md",{},true,"\u002Fopen-source\u002Feditor-suche",null,{"title":5,"description":554},"open-source\u002Feditor-suche","y-3zxsVO9E7gUxDG02vm7RwpI8SxJcXUGHmemL-4vZU",1790517213211]