Skip to content

A mini code editor

Let’s build a code editor in about 150 lines: it opens a real folder, shows the file tree, opens tabs, edits with syntax highlighting, saves with Ctrl+S and has a command palette. The workspace pieces (docks, tabs, palette) are the same ones Pyzed Design is built with; their look is inspired by the Zed editor.

Goal Time Level You need
A working code editor for a folder 45 minutes Advanced · Python Lists and tables, Navigation
  • Turn a folder into a file tree (ui.TreeNode.from_paths) and open files in tabs.
  • Edit with syntax highlighting (ui.editor) and track unsaved changes.
  • Lay out a whole workspace: title bar, docks, status bar.
  • Add a command palette with fuzzy search, and keyboard shortcuts.

The mini editor

  1. Reading the folder. TreeNode.from_paths turns a list of paths into the project panel tree. We skip heavy folders such as .git or node_modules:

    def scan(self):
    files = []
    for folder, dirs, names in os.walk(self.root):
    dirs[:] = [d for d in dirs if d not in IGNORED]
    rel = Path(folder).relative_to(self.root)
    files += [(rel / n).as_posix() for n in names]
    return files
    self.nodes = ui.TreeNode.from_paths(self.scan())
    self.expanded = set() # expanded folders
  2. Opening and closing tabs. The state is the open paths, the active one and the text of each. Each tab’s id is its path:

    def open_file(self, path):
    if path not in self.texts:
    self.texts[path] = (self.root / path).read_text(encoding="utf-8")
    if path not in self.open_tabs:
    self.open_tabs.append(path)
    self.active = path
  3. The editor. We use the path as the editor’s id, so each file keeps its cursor and undo history when switching tabs. on_change marks the file as modified:

    ui.editor(self.active, self.texts[self.active], path=self.active,
    on_change=self.edited,
    on_cursor=lambda line, col: setattr(self, "cursor", (line, col)))
    def edited(self, text):
    self.texts[self.active] = text
    self.dirty.add(self.active) # the tab shows a dot
  4. The workspace. ui.workspace lays out the title bar, the left dock, the center and the status bar. Passing None as left hides the panel:

    ui.workspace(
    self.center(),
    title_bar=ui.title_bar([ui.title_button(self.root.name, chevron=True)]),
    left=project if self.show_left else None,
    status_bar=ui.status_bar([...], [ui.status_item(f"Ln {line}, Col {col}")]),
    )
  5. Shortcuts. We map Cmd to Ctrl so it works the same on macOS:

    def on_key(self, combo):
    combo = combo.replace("cmd-", "ctrl-")
    if combo == "ctrl-s":
    self.save()
    elif combo == "ctrl-b":
    self.toggle_left()
  6. Command palette. It opens with Ctrl+Shift+P, focuses its field and floats over the workspace with ui.layer:

    if combo == "ctrl-shift-p":
    self.palette_open, self.query = True, ""
    return Command.focus("command-palette")
    # at the end of view():
    palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(),
    on_submit=self.run_first_match) if self.palette_open else None
    return ui.layer(layout, palette, on_dismiss=lambda: setattr(self, "palette_open", False))

    The command palette

examples/mini_editor.py
"""A small Zed-like editor in ~150 lines: project tree, tabs, code editor, save, docks and command palette.
python examples/mini_editor.py [folder]
Walkthrough: https://albakore.github.io/pyzed/tutorials/advanced/code-editor/
For a fuller version see examples/zed_editor.py.
"""
import os
import sys
from pathlib import Path
import pyzed as pz
from pyzed import Command, icons, ui
IGNORED = {".git", "target", "node_modules", "__pycache__", ".venv", "dist"}
class MiniEditor(pz.App):
size = (1100, 700)
def __init__(self, root="."):
self.root = Path(root).resolve()
self.nodes = ui.TreeNode.from_paths(self.scan()[:2000])
self.expanded = set()
self.open_tabs = [] # paths, in tab order
self.active = None
self.texts = {} # path -> current text
self.dirty = set()
self.cursor = (1, 1)
self.show_left = True
self.palette_open = False
self.query = ""
def scan(self):
files = []
for folder, dirs, names in os.walk(self.root):
dirs[:] = [d for d in dirs if d not in IGNORED]
rel = Path(folder).relative_to(self.root)
files += [(rel / n).as_posix() for n in names]
return files
def title(self):
name = self.active or "sin archivos"
return f"{name} — {self.root.name}"
# -- files ------------------------------------------------------------------
def open_file(self, path):
if path not in self.texts:
try:
self.texts[path] = (self.root / path).read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
return
if path not in self.open_tabs:
self.open_tabs.append(path)
self.active = path
def close_tab(self, path):
index = self.open_tabs.index(path)
self.open_tabs.remove(path)
if self.active == path:
self.active = self.open_tabs[min(index, len(self.open_tabs) - 1)] if self.open_tabs else None
def edited(self, text):
self.texts[self.active] = text
self.dirty.add(self.active)
def save(self):
if self.active in self.dirty:
(self.root / self.active).write_text(self.texts[self.active], encoding="utf-8")
self.dirty.discard(self.active)
def toggle_folder(self, folder):
self.expanded ^= {folder}
# -- commands & keys ----------------------------------------------------------
def toggle_left(self):
self.show_left = not self.show_left
def toggle_theme(self):
self.theme = "One Light" if self.resolved_theme().is_dark else "One Dark"
def commands(self):
return [
ui.PaletteItem("workspace: save", self.save, keys="ctrl-s", icon="save"),
ui.PaletteItem("workspace: toggle left dock", self.toggle_left, keys="ctrl-b", icon="panel-left"),
ui.PaletteItem("theme: toggle light/dark", self.toggle_theme, icon="sun-moon"),
ui.PaletteItem("pane: close active item", lambda: self.active and self.close_tab(self.active),
keys="ctrl-w", icon="x"),
]
def run_first_match(self):
matches = ui.fuzzy_filter(self.query, self.commands(), key=lambda c: c.label)
self.palette_open = False
if matches:
return matches[0][0].on_press()
def on_key(self, combo):
combo = combo.replace("cmd-", "ctrl-")
if combo == "ctrl-shift-p":
self.palette_open, self.query = True, ""
return Command.focus("command-palette")
if combo == "escape":
self.palette_open = False
elif combo == "ctrl-s":
self.save()
elif combo == "ctrl-b":
self.toggle_left()
elif combo == "ctrl-w" and self.active:
self.close_tab(self.active)
# -- view -----------------------------------------------------------------------
def center(self):
if not self.active:
return pz.center(pz.column([ui.heading("Mini editor", level=2),
ui.muted("Abre un archivo del panel de proyecto."),
ui.kbd("ctrl-shift-p")], spacing=8, align="center"))
tabs = [ui.Tab.for_file(p, dirty=p in self.dirty) for p in self.open_tabs]
return pz.column([
ui.tab_bar(tabs, self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close_tab),
ui.toolbar([ui.breadcrumbs(self.active.split("/"))]),
ui.editor(self.active, self.texts[self.active], path=self.active, on_change=self.edited,
on_cursor=lambda line, col: setattr(self, "cursor", (line, col))),
])
def view(self):
project = pz.column([
ui.panel_header(self.root.name, [ui.icon_button("chevrons-down-up", lambda: self.expanded.clear(),
size="xs", tooltip="Plegar todo")]),
pz.scrollable(ui.tree(self.nodes, expanded=self.expanded, selected=self.active,
on_toggle=self.toggle_folder, on_select=self.open_file), height="fill"),
])
line, col = self.cursor
layout = ui.workspace(
self.center(),
title_bar=ui.title_bar([ui.title_button(self.root.name, chevron=True)],
[ui.icon_button("sun-moon", self.toggle_theme, size="sm", tooltip="Tema")]),
left=project if self.show_left else None,
status_bar=ui.status_bar(
[ui.status_item(icon="panel-left", on_press=self.toggle_left, selected=self.show_left,
tooltip="Panel de proyecto", keys="ctrl-b")],
[ui.status_item(f"Ln {line}, Col {col}") if self.active else None,
ui.status_item(icons.language_for(self.active)) if self.active else None],
),
)
palette = None
if self.palette_open:
palette = ui.command_palette(self.query, lambda q: setattr(self, "query", q), self.commands(),
on_submit=self.run_first_match)
return ui.layer(layout, palette, on_dismiss=lambda: setattr(self, "palette_open", False))
if __name__ == "__main__":
pz.run(MiniEditor(sys.argv[1] if len(sys.argv) > 1 else "."))
Windows, macOS and Linux
uv run --with pyzed-ui python examples/mini_editor.py

For a more complete version (file finder, terminal panel, outline, themes, git markers) see examples/zed_editor.py and its page in the Showcase.

Widget Event Handler gets
ui.tab_bar on_select, on_close The tab’s id (its path).
ui.editor on_change, on_cursor The text; the line and column.
ui.tree on_toggle, on_select The path of the folder or file.
ui.status_item on_press Nothing.
ui.command_palette on_query, on_submit The query; nothing.
ui.layer on_dismiss Nothing (a click outside the palette).
The app on_key The combo (ctrl-shift-p, ctrl-b…).
ui.editor(self.active, self.texts[self.active], path=self.active, font_size=self.font_size,
on_change=self.edited,
on_cursor=lambda line, col: setattr(self, "cursor", (line, col)),
on_zoom=lambda step: setattr(self, "font_size", 14 if step == 0 else self.font_size + step))
ui.tree(self.nodes, expanded=self.expanded, selected=self.active,
on_toggle=self.toggle_folder, on_select=self.open_file,
on_open=self.open_file, # double click
on_context=lambda path: setattr(self, "menu_for", path)) # right click: a menu
ui.tab_bar(tabs, self.active, on_select=lambda p: setattr(self, "active", p), on_close=self.close_tab,
on_new=self.new_file) # double click after the tabs