The simple directory tree viewer for Neovim written in Lua.
I created the sfm
plugin because I wanted to write my own simple and lightweight file management options for NeoVim. I wanted a plugin that would allow me to easily browse and open files in my project without being weighed down by unnecessary features or complexity.
Please note that the sfm
plugin is still in development and may not be fully stable. Use at your own risk.
Here is a short demonstration of the sfm
plugin in action:
sfm-demo.mp4
The sfm
plugin allows users to extend its functionality by installing extensions. Extensions are independent plugins that can add new features or customize the behavior of the sfm
plugin.
The extensions must be written under lua/sfm/extensions/
folder.
Here is a list of available extensions for the sfm
plugin:
- sfm-bookmark: Adds bookmarking functionality to the
sfm
plugin. - sfm-filter: Allows users to filter entries in the
sfm
explorer tree. - sfm-git: Adds git icon support to the
sfm
plugin's file and folder explorer view, indicating the git status of the file or folder. - sfm-telescope: Allows users to search for entries in the
sfm
explorer tree. - sfm-paste: Provides a convenient way to paste text or images into new files within the sfm file explorer.
- sfm-focus: Adds the command
SFMFocus
. This command change current focus to explorer and open it if closed.
Install sfm
on Neovim using your favorite plugin manager. For example, the below example shows how to install sfm
using packer.nvim
use {
'dinhhuy258/sfm.nvim',
config = function()
require("sfm").setup()
end
}
sfm
provides the following configuration options:
local default_config = {
view = {
side = "left", -- side of the tree, can be `left`, `right`. this setting will be ignored if view.float.enable is set to true,
width = 30, -- this setting will be ignored if view.float.enable is set to true,
float = {
enable = false,
config = {
relative = "editor",
border = "rounded",
width = 30, -- int or function
height = 30, -- int or function
row = 1, -- int or function
col = 1 -- int or function
}
},
selection_render_method = "icon" -- render method of selected entries, can be `icon`, `sign`, `highlight`.
},
mappings = {
custom_only = false,
list = {
-- user mappings go here
}
},
renderer = {
icons = {
file = {
default = "",
symlink = "",
},
folder = {
default = "",
open = "",
symlink = "",
symlink_open = "",
},
indicator = {
folder_closed = "",
folder_open = "",
file = " ",
},
selection = "",
}
},
file_nesting = {
enabled = false,
expand = false,
patterns = {},
},
misc = {
trash_cmd = nil,
system_open_cmd = nil,
}
}
You can override the default configuration by calling the setup method and passing in your customizations:
require("sfm").setup {
--- your customization configuration
}
The sfm
now supports nesting related files based on their names. There are several settings to control this behavior:
require("sfm").setup({
file_nesting = {
enabled = true, -- controls whether file nesting is enabled
expand = true, -- controls whether nested files are expanded by default
patterns = {
{ "*.cs", { "$(capture).*.cs" } },
{ "*.ts", { "$(capture).js", "$(capture).d.ts.map", "$(capture).*.ts", "$(capture)_*.js", "$(capture)_*.ts" } },
{ "go.mod", { "go.sum" } },
}, -- controls how files get nested
}
})
The default mapping to expand/collapse nested files is a
.
The idea of how to parse the file nesting pattern is highly inspired by VS Code. That why you can use the patterns that configure from VS Code.
However, Currently I just only support $(capture)
substitute as I find it not unnecessary to support basename
, dirname
, extname
.
:SFMToggle
Open or close the explorer.
To use the functionalities provided by the sfm
plugin, you can use the following key bindings:
Key | Action | Description |
---|---|---|
cr | edit | Open a file or directory |
ctr-v | vsplit | Open a file in a vertical split window |
ctr-h | split | Open a file in a horizontal split window |
ctr-t | tabnew | Open a file in a new tab |
s-tab | close_entry | Close current opened directory or parent |
K | first_sibling | Navigate to the first sibling of current file or directory |
J | last_sibling | Navigate to the last sibling of current file or directory |
P | parent_entry | Move cursor to the parent directory |
ctr-] | change_root_to_parent | Change the root directory to the parent directory of the current root |
] | change_root_to_entry | Change the root directory to the current folder entry or to the parent directory of the current file entry |
R | reload | Reload the explorer |
q | close | Close the explorer window |
n | create | Create a new file/directory in the current folder |
c | copy | Copy current/selected file/s directory/ies |
x | move | Move/Rename current/selected file/s directory/ies |
d | delete | Delete current/selected file/s directory/ies |
a | toggle_entry | Expand or collapse a entry with children, which may be a directory or a nested file. |
space | toggle_selection | Toggle the selection of the current file or directory |
c-space | clear_selections | Clear all selections |
trash | Trash current/selected file/s directory/ies | |
system_open | Open current/selected file/s directory/ies using system default program |
Below is a list of deprecated actions that should not be used anymore and might be removed anytime:
Action |
---|
copy_selections |
move_selections |
delete_selections |
trash_selections |
system_open_selections |
You can customize these key bindings by defining custom functions or action names in the mappings
configuration option. For example, you can assign a custom function to the t
key:
local sfm_explorer = require("sfm").setup {
mappings = {
list = {
{
key = "c",
action = function()
print("Custom function executed")
end,
},
{
key = "x",
action = "close",
},
},
},
}
In this example, when the user presses the c
key in the explorer, the custom function function() print("Custom function executed") end
will be executed. Pressing the x
key will perform the default action close
.
Please note that if the action for a key is set to nil
or an empty string, the default key binding for that key will be disabled. Also, ensure that the action provided is a valid function or action name, as listed in the above table.
The sfm
plugin uses the following highlight values:
SFMRootFolder
: This highlight value is used to highlight the root folder in the file explorer. The default color scheme for this highlight value ispurple
.SFMSymlink
: This highlight value is used to highlight symbolic links in the file explorer. The default color scheme for this highlight value iscyan
.SFMFileIndicator
andSFMFolderIndicator
: These highlight values are used to highlight file and folder indicators in the file explorer. The default color scheme for this highlight value isfg=#3b4261
.SFMFolderName
andSFMFolderIcon
: These highlight values are used to highlight folder names and icons in the file explorer. The default color scheme for this highlight value isDirectory
.SFMFileName
andSFMDefaultFileIcon
: These highlight values are used to highlight file names and icons in the file explorer. The default color scheme for this highlight value isNormal
.
In addition to the above highlight values, the sfm
plugin also uses the following highlight values:
SFMNormal
,SFMNormalNC
,SFMEndOfBuffer
,SFMCursorLine
,SFMCursorLineNr
,SFMLineNr
,SFMWinSeparator
,SFMStatusLine
,SFMStatuslineNC
,SFMSignColumn
these highlight values are used to link to the default Neovim highlight groups.
The sfm
plugin provides several customization mechanisms, including remove_renderer
, register_renderer
, remove_entry_filter
, register_entry_filter
, and set_entry_sort_method
, that allow users to alter the appearance and behavior of the explorer tree.
The remove_renderer
function allows users to remove a renderer components from the list of renderers used to render the entry of the explorer tree. This can be useful if a user wants to disable a specific renderer provided by the sfm plugin or by an extension.
The register_renderer
function allows users to register their own renderers for the explorer tree. This can be useful if a user wants to customize the appearance of the tree or add new features to it.
Here is an example of an extension for the sfm
plugin that adds a custom renderer to display the entry size:
-- define a custom renderer that displays the entry size
local function size_renderer(entry)
local stat = vim.loop.fs_stat(entry.path)
local size = stat.size
local size_text = string.format("[%d bytes]", size)
return {
text = size_text,
highlight = "SFMSize",
}
end
local sfm_explorer = require("sfm").setup {}
-- register the custom renderer
sfm_explorer:register_renderer("custom", 100, size_renderer)
The default entry renderers, in order of rendering priority, are:
- indent (priority 10)
- indicator (priority 20)
- icon (priority 30)
- name (priority 40)
The register_entry_filter
function allows users to register their own filters for the explorer tree. This can be useful if a user wants to filter out certain entries based on certain criteria. For example, a user can filter out files that are larger than a certain size, or files that have a certain file extension.
The remove_entry_filter
function allows users to remove a filter component from the list of filters used to filter the entries of the explorer tree. This can be useful if a user wants to disable a specific filter provided by the sfm plugin or by an extension.
Here is an example of an extension for the sfm
plugin that adds a custom entry filter to hide the big entry size:
local sfm_explorer = require("sfm").setup {}
sfm_explorer:register_entry_filter("big_files", function(entry)
local stat = vim.loop.fs_stat(entry.path)
local size = stat.size
if size > 1000000 then
return false
else
return true
end
end)
This method allows you to customize the sorting of entries in the explorer tree. The function passed as a parameter should take in two entries and return a boolean value indicating whether the first entry should be sorted before the second. For example, you can use the following function to sort entries alphabetically by name:
local sfm_explorer = require("sfm").setup {}
sfm_explorer:set_entry_sort_method(function(entry1, entry2)
return entry1.name < entry2.name
end)
sfm
dispatches events whenever an action is made in the explorer. These events can be subscribed to through handler functions, allowing for even further customization of sfm
.
To subscribe to an event, use the subscribe
function provided by sfm
and specify the event name and the handler function:
local sfm_explorer = require("sfm").setup {}
sfm_explorer:subscribe(event.ExplorerOpened, function(payload)
local bufnr = payload["bufnr"]
local options = {
noremap = true,
expr = false,
}
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"m",
"<CMD>lua require('sfm.extensions.sfm-bookmark').set_mark()<CR>",
options
)
vim.api.nvim_buf_set_keymap(
bufnr,
"n",
"`",
"<CMD>lua require('sfm.extensions.sfm-bookmark').load_mark()<CR>",
options
)
end)
Available events:
ExplorerOpened
: Triggered when the explorer window is opened. The payload of the event is a table with the following keys:winnr
: The number of the window where the explorer is opened.bufnr
: The number of the buffer associated with the explorer window.
ExplorerClosed
: Triggered when the explorer window is closed. It does not provide any payload.ExplorerReloaded
: Triggered when a explorer is reloaded. This event is emitted after the explorer tree has finished reloading, and all the files and folders have been re-read. Listeners can use this event to update or refresh any state or information that is dependent on the explorer tree. It does not provide any payload.ExplorerRendered
: Triggered when a explorer is rendered. This event can be used to perform additional customizations or updates after the explorer has been rendered. The payload of the event is a table with the following keys:winnr
: The number of the window explorer.bufnr
: The number of the buffer associated with the explorer window.
ExplorerRootChanged
: This event is fired when the root of the explorer changes. The payload of the event is a table with the following key:path
: The new root path
FileOpened
: Triggered when a file is opened in the explorer. The payload of the event is a table with the following key:path
: The path of the file that was opened.
FolderOpened
: Triggered when a folder is opened in the explorer. The payload of the event is a table with the following key:path
: The path of the folder that was opened.
FolderClosed
: Triggered when a folder is closed in the explorer. The payload of the event is a table with the following key:path
: The path of the folder that was closed.
EntryCreated
: Dispatched when a new file/directory is created. The payload of the event is a table with the following keys:path
: The entry path of the newly created entry
EntryDeleted
: Dispatched when a new file/directory is created. The payload of the event is a table with the following keys:path
: The entry path of the deleted/trashed entry
EntryWillRename
: Dispatched when a file/directory will be renamed. The payload of the event is a table with the following keys:from_path
: The old pathto_path
: The new path
EntryRenamed
: Dispatched when a file/directory is renamed. The payload of the event is a table with the following keys:from_path
: The old pathto_path
: The new path
The sfm
plugin exposes a number of APIs that can be used to customize the explorer tree and write extensions for the plugin. These functions are located in the lua/sfm/api.lua
file and can be accessed by requiring it. Below are the available functions and their usage:
api.explorer.toggle()
: Toggles the visibility of the explorer window.api.explorer.open()
: Opens the explorer window.api.explorer.close()
: Closes the explorer window.api.explorer.is_open()
: Returnstrue
if the explorer window is currently open,false
otherwise.api.explorer.reload()
: Reloads the explorer tree.api.explorer.refresh()
: Refreshes the current view of the explorer tree.api.explorer.change_root(cwd: string)
: Changes the root directory of the explorer tree to the specified directory. If the directory is not valid, an error message will be displayed.
api.entry.root()
: Returns the root entry of the explorer tree.api.entry.current()
: Returns the current entry in the explorer tree.api.entry.all()
: Returns a table containing all the entries in the explorer tree.
api.navigation.focus(p: string)
: Focuses on the specified file or directory in the explorer tree.
api.path.clean(p: string)
: Cleans up a file path to make it more standard.api.path.split(p: string)
: Splits a file path into a table of its parts.api.path.join(...)
: Joins a list of parts into a file path.api.path.dirname(p: string)
: Returns the directory name of a file path.api.path.basename(p: string)
: Returns the base name of a file path.api.path.remove_trailing(p: string)
: Removes the trailing path separator from a file path.api.path.has_trailing(p: string)
: Returnstrue
if the file path has a trailing path separator,false
otherwise.api.path.add_trailing(p: string)
: Add trailing separator to the given pathapi.path.exists(p: string)
: Check if the given path existsapi.path.isfile(p: string)
: Check if the given path is a fileapi.path.isdir(p: string)
: Check if the given path is a directoryapi.path.islink(p: string)
: Check if the given path is a symbolic linkapi.path.unify(paths: table)
: Unify ancestor for the given pathsapi.path.path_separator
: Get the system path separator
api.debounce(name: string, delay: integer, fn: function)
: Create a debounced version of the given function
api.log.info(message: string)
: Log an informational messageapi.log.warn(message: string)
: Log a warning messageapi.log.error(message: string)
: Log an error message
api.context.is_selected(path: string)
: Check if the given path is selectedapi.context.set_selection(path: string)
: Bookmark the given pathapi.context.remove_selection(path: string)
: Unbookmark the given pathapi.context.clear_selections()
: Clear all bookmarksapi.context.get_selections()
: Get all bookmarks
api.event.dispatch(event_name: string, payload: table?)
: Dispatches an event with the given name and payload to all registered event handlers.
Here's an example of how you might use the API provided by the sfm
plugin in your own extension or configuration file:
local api = require('sfm.api')
-- use the `path.remove_trailing` function to remove trailing slashes from a file path
local cleaned_path = api.path.remove_trailing('/path/to/file/')
-- use the `debounce` function to debounce a function call
api.debounce("debounce-context", 1000, function()
-- your code
end)
- This plugin was developed using NeoVim.
- The file explorer functionality is based on the nvim-tree plugin. I also copied some code from there.
- The icons used in the file explorer are from the nvim-web-devicons.