Abstract
FTX (File Tree eXplorer) is a lightweight, asynchronous file tree explorer built entirely in Vimscript with first-class Git integration. Unlike traditional file browsers that block the editor during operations, FTX leverages Vim's native job API and a custom promise-based async engine to provide non-blocking file tree rendering, real-time Git status updates, and responsive interaction. This article introduces FTX's design philosophy, core features, and the architectural decisions that make it both performant and maintainable—from its Git-inspired cache system to its Go-like concurrency primitives.
The Story Behind FTX
For months, I relied on NERDTree as my file browser. It worked, but something felt heavy. The blocking operations, the occasional lag when navigating large directories—it all added friction to my workflow. So I decided to try Netrw, Vim's built-in file explorer. It was minimal, familiar, and perfectly functional. But after a while, it felt too barebones. I missed having a structured tree view, and more importantly, I desperately missed seeing Git status at a glance.
My workflow is deeply tied to Git. I need to know what's changed, what's staged, what state my branch is in—without running git status in a separate terminal. Even my Bash prompt is heavily configured to show Git info. I wanted that same level of awareness inside my file tree.
One night, while browsing file explorer source code, I stumbled upon ranger.vim. It was strikingly simple—a clean, single-file implementation. That sparked an idea: I could build my own. Something tailored exactly to my needs.
I sketched out ideas, discussed architecture patterns, and made a decision: I was going to build FTX.
What is FTX?
FTX is an asynchronous file tree explorer for Vim 8.0+ and Neovim 0.4+. It's designed around three core principles:
Async by design – All file operations and Git status checks run in the background using Vim's job API. Opening deep directories or refreshing Git status never blocks the editor.
Git-first workflow – Real-time Git status indicators (
+staged,*modified,?untracked) appear directly in the tree. Branch information, commit tracking (ahead/behind), and stash detection are all visible without leaving Vim.Zero dependencies – FTX is pure Vimscript with no external dependencies beyond Vim/Neovim itself.
FTX isn't trying to replace project managers or become a Swiss Army knife. It does one thing well: show you your files and exactly what Git thinks about them—quickly, reliably, and without getting in your way.
Project repository: github.com/m-mdy-m/ftx.vim
Core Features
Async Everything
File tree rendering, directory traversal, and Git status parsing all happen asynchronously. FTX uses Vim's +job and +timers features to schedule work without blocking the UI. You can navigate, edit, and work normally while FTX updates in the background.
Git Integration
Every file can display a Git status symbol:
-
+– Staged for commit -
*– Modified, not staged -
?– Untracked by Git -
-– Deleted -
!– Merge conflict -
→– Renamed -
◌– Ignored (optional)
The status line shows branch info: [main] ↑2 ↓1 $ tells you you're on main, 2 commits ahead, 1 behind, with an active stash. Press gi for detailed branch information, or gb for Git blame (if enabled).
Multi-File Operations
Mark files with m, then batch open them (mo) or stage them to Git (mg). The marking system makes working with multiple files fast and intuitive.
Flexible Display Modes
FTX works as both a split window (like Netrw) and a project drawer (like NERDTree). Use :FTX . -drawer for fixed-width sidebar mode with smart quit handling and auto-focus restoration.
Customizable
Configure icons, colors, keymaps, Git update intervals, and behavior. FTX provides sensible defaults but gets out of your way if you want something different.
Full feature documentation: doc/README.md
Configuration guide: doc/config.md
Keymaps reference: doc/keymaps.md
Git features: doc/git.md
Architecture Overview
FTX's architecture is built around five interconnected systems: the async engine, cache layer, tree management, Git integration, and rendering pipeline. Each is designed to be modular, testable, and performant.
Async Engine
At the heart of FTX is a custom async engine inspired by JavaScript Promises and Go's concurrency model. Instead of blocking Vim while reading directories or running Git commands, FTX schedules work on a task queue and processes it using timer-based workers.
Key components:
Promises (
autoload/ftx/async/promise.vim): An ECMAScript-like Promise implementation based on es6-promise. Promises allow chaining async operations with.then()and.catch(), making complex workflows readable and composable.Job API (
autoload/ftx/async/job.vim): A unified wrapper around Vim'sjob_start()and Neovim'sjobstart()that returns Promises. This lets FTX run shell commands (likegit status) asynchronously and handle their output when ready.Scheduler (
autoload/ftx/async/internal/): A Go-inspired goroutine-style scheduler with channels, wait groups, and a worker pool. Tasks are queued and executed by timer-driven workers, scaling up to handle load and shutting down when idle.
This design ensures that no matter how large your repository or how deep your directory tree, FTX remains responsive.
Cache System
FTX's cache is inspired by Git's tree/blob architecture. Instead of naively re-reading directories on every refresh, FTX uses content-based hashing to detect changes and reuse cached results when possible.
How it works:
- Each file and directory gets a hash based on its modification time and content (for directories, the list of children).
- Before rebuilding a tree, FTX checks if the hash has changed. If not, it returns the cached result.
- This dramatically reduces disk I/O and makes navigation feel instant, especially in large projects.
Implementation: autoload/ftx/tree/cache.vim
Tree Management
The tree module (autoload/ftx/tree/tree.vim) handles directory traversal, node expansion, and state tracking. Directories can be lazily loaded—only expanding when the user requests it—which keeps memory usage low and initial render fast.
Node structure:
{
'path': '/full/path/to/file',
'name': 'filename',
'depth': 2,
'is_dir': 1,
'is_expanded': 0,
'children': [...],
'git_status': '*'
}
Nodes are flattened into a displayable list by tree#flatten(), which recursively collects visible nodes based on expansion state. This flat list is then passed to the renderer.
Git Integration
Git status checking runs asynchronously every few seconds (configurable via g:ftx_git_update_time). FTX spawns a git status --porcelain job, parses the output, and updates an internal cache mapping file paths to status symbols.
Branch tracking (autoload/ftx/git/branch.vim): Parses git status -b to extract branch name, ahead/behind counts, and checks for stashed changes.
Blame support (autoload/ftx/git/blame.vim): If enabled, press gb on a file to see the last 10 commits with author, time, and message in a scrollable popup.
Documentation: doc/git.md
Rendering Pipeline
The renderer (autoload/ftx/renderer/default.vim) transforms tree nodes into display lines with icons, colors, and Git status indicators. It:
- Iterates over flattened nodes
- Builds display strings (indentation + icons + filename)
- Applies syntax highlighting rules
- Updates the buffer content in one atomic operation
Syntax groups (FTXDir, FTXGitModified, FTXIconCollapsed, etc.) are dynamically defined based on configuration. Users can override colors and icons by setting g:ftx_colors and g:ftx_icons.
Drawer Mode
Drawer mode (autoload/ftx/internal/drawer/) provides NERDTree-style behavior:
- Auto-resize: Preserves drawer width across tab switches
- Auto-restore focus: Returns focus to the previous window when closing other windows
- Smart quit: Prevents accidentally closing Vim when the drawer is the last window
These behaviors are implemented via autocommands and buffer-local state tracking.
Getting Started
Installation
vim-plug:
Plug 'm-mdy-m/ftx.vim'
Vim 8 packages:
git clone https://github.com/m-mdy-m/ftx.vim ~/.vim/pack/ftx/start/ftx
Neovim packages:
git clone https://github.com/m-mdy-m/ftx.vim ~/.config/nvim/pack/ftx/start/ftx
Basic Usage
Open FTX in current directory:
:FTX .
Toggle FTX:
:FTXToggle
" or press F2
Open as project drawer:
:FTX . -drawer
Auto-open on directory:
vim ~/projects
Common Keymaps
Inside FTX buffer:
| Key | Action |
|---|---|
o, <Enter>
|
Open file / Toggle directory |
t |
Open in tab |
s, v
|
Open in split/vsplit |
r, R
|
Refresh tree / Git status |
I |
Toggle hidden files |
O, C
|
Expand/Collapse all |
m, M
|
Toggle mark / Clear marks |
mo, mg
|
Open marked / Stage marked |
gi, gb
|
Git branch info / Blame |
yy, yn
|
Yank path / filename |
? |
Show help |
q |
Close |
Full keymap reference: doc/keymaps.md
Configuration
FTX provides extensive configuration options. Here are the essentials:
" Window settings
let g:ftx_width = 30
let g:ftx_position = 'left' " or 'right'
let g:ftx_show_hidden = 1
let g:ftx_auto_sync = 1 " Sync to current file
" Git settings
let g:ftx_enable_git = 1
let g:ftx_git_update_time = 2000 " Update interval (ms)
let g:ftx_git_blame = 1 " Enable blame feature
" Icons and colors
let g:ftx_enable_icons = 1
let g:ftx_icon_expanded = '▾'
let g:ftx_icon_collapsed = '▸'
" Custom file type icons
let g:ftx_icons = {
\ 'vim': '',
\ 'md': '',
\ 'js': '',
\ }
" Custom colors
let g:ftx_colors = {
\ 'vim': 'guifg=#019733 ctermfg=35',
\ 'py': 'guifg=#3572A5 ctermfg=67',
\ }
Complete configuration guide: doc/config.md
Under the Hood
For those curious about the implementation details, here's a glimpse into the technical foundation of FTX.
Promise-Based Async
FTX's Promise implementation follows the ECMAScript Promise spec, based on the es6-promise library. Promises wrap asynchronous operations and allow chaining:
ftx#async#fs#readdir('/path')
.then({entries -> process(entries)})
.then({result -> cache(result)})
.catch({err -> handle_error(err)})
This makes complex async workflows (read directory → filter → sort → render) composable and readable.
Goroutine-Style Concurrency
The internal scheduler (autoload/ftx/async/internal/) uses Go-inspired concurrency primitives:
-
Channels (
channels.vim): Non-blocking message passing between tasks -
Wait groups (
waiter.vim): Synchronize multiple async operations -
Worker pool (
worker.vim): Dynamically scales workers to process queued tasks
Workers run on Vim timers, executing one task per tick. When the queue is empty, workers shut down. This keeps overhead minimal while providing concurrency when needed.
Git-Like Cache
Inspired by Git's object storage model, FTX computes content hashes for files and directories. A directory's hash depends on its children's names, so moving files invalidates the cache correctly.
Cache keys are path:hash pairs. When checking the cache, FTX recomputes the hash and compares—only re-reading if content has changed. This approach is both efficient and correct.
Modular Design
FTX is split into logical modules:
-
autoload/ftx/async/– Async engine (promises, jobs, scheduler) -
autoload/ftx/tree/– Tree management (build, cache, filter, nodes) -
autoload/ftx/git/– Git integration (status, branch, blame) -
autoload/ftx/renderer/– Display rendering and syntax -
autoload/ftx/internal/– Window/buffer management, drawer mode -
autoload/ftx/mapping/– Keymap handlers -
autoload/ftx/helpers/– Utilities (path, platform, logging)
Each module has a clear responsibility and minimal coupling. This makes FTX maintainable and testable.
Source code: github.com/m-mdy-m/ftx.vim
Inspiration and Credits
FTX stands on the shoulders of great explorers that came before:
- netrw: Vim's built-in file browser, the gold standard for simplicity
- NERDTree: The classic tree explorer, beloved by many
- fern.vim: Modern async architecture and clean design
- ranger.vim: Elegant single-file simplicity
FTX aims to carve out its own niche: a fast, Git-aware, no-nonsense file tree that gets out of your way.
Community and Feedback
FTX is designed for real workflows, but it can only improve with feedback from the people who use it.
Repository: github.com/m-mdy-m/ftx.vim
Documentation: :help ftx or doc/ftx.txt
Issues & PRs: github.com/m-mdy-m/ftx.vim/issues
Try it, break it, and let me know what you think.
What's your take on file explorers in Vim?
Top comments (0)