Home

Awesome

sfm.nvim

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.

Demonstration

Here is a short demonstration of the sfm plugin in action:

https://user-images.githubusercontent.com/17776979/213235911-f2cfc886-5485-413d-8959-bf404ecc8451.mp4

Extensions

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.

Available Extensions

Here is a list of available extensions for the sfm plugin:

Installation

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
}

Configuration

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
}

File nesting

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.

Commands

:SFMToggle Open or close the explorer.

Mappings

To use the functionalities provided by the sfm plugin, you can use the following key bindings:

KeyActionDescription
creditOpen a file or directory
ctr-vvsplitOpen a file in a vertical split window
ctr-hsplitOpen a file in a horizontal split window
ctr-ttabnewOpen a file in a new tab
s-tabclose_entryClose current opened directory or parent
Kfirst_siblingNavigate to the first sibling of current file or directory
Jlast_siblingNavigate to the last sibling of current file or directory
Pparent_entryMove cursor to the parent directory
ctr-]change_root_to_parentChange the root directory to the parent directory of the current root
]change_root_to_entryChange the root directory to the current folder entry or to the parent directory of the current file entry
RreloadReload the explorer
qcloseClose the explorer window
ncreateCreate a new file/directory in the current folder
ccopyCopy current/selected file/s directory/ies
xmoveMove/Rename current/selected file/s directory/ies
ddeleteDelete current/selected file/s directory/ies
atoggle_entryExpand or collapse a entry with children, which may be a directory or a nested file.
spacetoggle_selectionToggle the selection of the current file or directory
c-spaceclear_selectionsClear all selections
trashTrash current/selected file/s directory/ies
system_openOpen 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.

Highlighting

The sfm plugin uses the following highlight values:

In addition to the above highlight values, the sfm plugin also uses the following highlight values:

Customizations

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.

remove_renderer

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.

register_renderer

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:

register_entry_filter

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.

remove_entry_filter

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)

set_entry_sort_method

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)

Events

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:

API

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:

Explorer

Entry

Navigation

Path

Debouncing

Logging

Context

Events

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)

Credits