Architecture

April 12, 2026 · View on GitHub

This document records the architecture, design philosophy, and trade-offs behind eda.nvim. It serves as a reference for developers and AI agents to understand the reasoning behind key design choices.

Design Principles

Buffer-Native File Operations

Edit the explorer buffer like any Vim buffer — rename by changing text, delete by removing lines, create by adding new lines — then :w to apply all changes to the real filesystem.

This extends oil.nvim's buffer-editing paradigm to a full tree view. Each line carries a concealed node ID for reliable tracking. Invalidated extmarks (e.g. from external formatters mangling lines) are skipped during parsing, and the computed operations are then validated for structural errors — missing rename targets, duplicate destinations — before execution.

Tree View with Hierarchy

Directories are displayed as a collapsible tree, not a flat per-directory listing. This is the core differentiation from oil.nvim — eda.nvim combines hierarchical tree navigation with buffer-native editing.

Collapsed (hidden) nodes are excluded from the diff on :w, preventing accidental deletion of files that aren't visible in the buffer.

Progressive Async Rendering

All filesystem scanning is fully asynchronous via vim.uv. The target file's ancestor chain is scanned first, so the relevant portion of the tree appears immediately. Remaining directories load progressively in the background.

Directory expand/collapse — the hot path for re-render — is applied incrementally: only the affected line range is replaced via nvim_buf_set_lines. Other changes fall back to a full repaint.

Stable Startup Experience

A target_node_id tracks which node the cursor should land on. As async scanning progresses and new nodes become paintable, the cursor is placed on the target as soon as its line is rendered — no jitter, no reset.

This directly solves the cursor instability seen in fyler.nvim, where async scan completion would reset the cursor position. The target is held until the user manually moves the cursor.

Flexible Appearance

60+ highlight groups organized into six categories — structure (EdaNormal, EdaBorder, EdaIndentMarker), filesystem (EdaDirectoryName, EdaFileName, EdaSymlink, EdaBrokenSymlink), git status (EdaGitModified, EdaGitAdded, ...), operation confirmation (EdaOpCreate, EdaOpDelete, ...), confirm UI, and help / full-name popup.

A decorator chain (flatten → decorate → paint) allows icon providers, git status indicators, and custom decorators to be composed, replaced, or extended independently.

Extensible Action System

All operations (navigation, file manipulation, UI toggles) are registered in a named action registry and dispatched by string name. This replaces hardcoded keybinding functions with a discoverable, composable system.

Every action receives a unified ActionContext containing the store, buffer, window, scanner, config, and explorer instance. Cursor-position and marked-node state are reached through the buffer field, making custom actions first-class citizens with the same capabilities as built-in ones.

Multiple Window Layouts

Four layout kinds serve different workflows:

  • float — Centered overlay for quick file picking
  • split_left / split_right — Persistent sidebar for ongoing navigation
  • replace — Inline buffer replacement (oil.nvim style)

Git Integration

Asynchronous git status detection via vim.system(), surfaced through the decorator chain as file-level status icons. Runs as a standard decorator, maintaining rendering consistency with other display elements.

netrw Replacement

With hijack_netrw enabled, :edit <directory> opens eda.nvim instead of netrw, providing a seamless default file browsing experience.

Event Hooks

User autocommands (EdaTreeOpen, EdaTreeClose, EdaMutationPre, EdaMutationPost) enable integration with external plugins such as nvim-lsp-file-operations for automatic LSP workspace updates on file rename/move.

Architecture Decisions

DecisionRationale
Flat store (id → node)No path-segment splitting needed. path_index (path → id) provides O(1) lookup. Simpler than a Trie + EntryManager dual structure
Two-phase renderAncestor chain scanned first → cursor placed immediately → remaining dirs load async. Eliminates startup cursor jitter
Action registryString-name registration + dispatch. Easy to extend, easy to discover (:Eda actions / which-key integration)
Decorator chainRendering split into flatten → decorate → paint. Each decorator is independently replaceable. Icon, git, and custom decorators compose via last-wins override
Smart root resolutionDefault: cwd. With update_focused_file.update_root, root follows the active buffer's project (via vim.fs.root() markers). Solves the "file outside cwd" problem
Lazy child materializationUnexpanded directories hold no children in memory (children_state = "unloaded"). Keeps memory usage bounded for 100k+ file repositories
Render snapshot for diffBuffer :w compares against the last painted snapshot, not the full store. Only visible (painted) nodes participate in diff — collapsed subtrees are safe

Comparison with Existing Plugins

Aspectfyler.nvimoil.nvimeda.nvim
ParadigmTree + buffer editingFlat (single dir) + buffer editingTree + buffer editing
Tree storageTrie + EntryManager (dual structure)None (per-directory)Flat store + path_index
Initial renderFully async (cursor bug source)SynchronousAsync, ancestor-first scan
Action systemHardcoded functionsAction string indirectionNamed registry + dispatch
Highlight groups~23 (color-oriented)Few60+ (structure / FS / git / operations / confirm / help)
Root resolutionAlways cwdBuffer's directorycwd + update_root (nvim-tree style)
Rendering pipelineComponent tree (Row/Column/Text)Direct buffer writeflatten → decorate → paint
Extensibilitymappings with selfAdapter abstractionActionContext + Decorator chain + User autocmds
Hierarchy displayYesNo (no nesting)Yes