diff --git a/README.md b/README.md index 59fe876..b471152 100644 --- a/README.md +++ b/README.md @@ -44,49 +44,30 @@ Install on Nightly Neovim (0.8.0+) using your favorite plugin manager: | **[dein.vim](https://github.com/Shougo/dein.vim)** | `call dein#add('SidOfc/carbon.nvim')` | | **[minpac](https://github.com/k-takata/minpac)** | `call minpac#add('SidOfc/carbon.nvim')` | | **[packer.nvim](https://github.com/wbthomason/packer.nvim)** | `use 'SidOfc/carbon.nvim'` | -| **[paq-nvim](https://github.com/savq/paq-nvim)** | `{ 'SidOfc/carbon.nvim', }` | +| **[paq-nvim](https://github.com/savq/paq-nvim)** | `{ 'SidOfc/carbon.nvim' }` | | **[lazy.nvim](https://github.com/folke/lazy.nvim)** | `{ 'SidOfc/carbon.nvim' }` | -**NOTE**: when using `lazy.nvim` please read `:h carbon-lazy-init`. +# Usage and configuration -# Configuration +Depending on whether you use native vim packages or a plugin manager +the way Carbon is set up will be slightly different. The most important +part is that Carbon's `setup` method **must** be called somewhere in your +`init.lua` / `init.vim` to initialize and use Carbon. It can be called like this: -Configuration can be supplied like this: - -**init.vim** - -```viml -lua << EOF - require('carbon').setup({ - setting = 'value', - }) -EOF +```lua +require('carbon').setup() ``` -**init.lua** +Configuration can be supplied like this: ```lua -require('carbon').setup({ - setting = 'value', -}) +require('carbon').setup({ setting = 'value' }) ``` These settings will be deep merged with the default settings. See `:h carbon-settings-table` for a list of available settings. An alternative option of calling this method also exists: -**init.vim** - -```viml -lua << EOF - require('carbon').setup(function(settings) - settings.setting = 'value' - end) -EOF -``` - -**init.lua** - ```lua require('carbon').setup(function(settings) settings.setting = 'value' @@ -99,12 +80,6 @@ You are free to modify them as you wish, no merging will occur. See `:h carbon-setup` for a more detailed explanation on configuration. See `:h carbon-carbon-setup` for documentation about the `.setup` method. -# Usage - -After installation, Carbon will launch automatically and disable NetRW. -These behaviors and many others can be customized, see `:h carbon-settings` for -more information about customization or `:h carbon-toc` for a table of contents. - Carbon comes with a few commands and mappings out of the box, each is described below: ## Commands @@ -115,8 +90,8 @@ customization options. All commands also support bang (`!`) versions which will make Carbon expand the tree to reveal the current buffer path if possible. When successful, the cursor will be moved to the entry and it will be highlighted for a short time as well. -See `:h carbon-buffer-flash-bang` for more information. This behavior can also -be enabled by default by setting: `:h carbon-setting-always-reveal`. +See `:h carbon-view-flash-bang` for more information. This behavior can also +be enabled by default by setting: `:h carbon-setting-auto-reveal`. ### `:Carbon` / `:Explore` @@ -254,7 +229,7 @@ opened with [`Fcarbon`](#fcarbon) or [`Lcarbon` / `Rcarbon`](#lcarbon--lexplore- Enters an interactive mode in which a path can be entered. When done typing, press enter to confirm or escape to cancel. Prepending a `count` to c will select the `count`_nth_ -directory from the left as base. See `:h carbon-buffer-create` for more details. +directory from the left as base. See `:h carbon-view-create` for more details. ### m Moving files and directories @@ -263,7 +238,7 @@ directory from the left as base. See `:h carbon-buffer-create` for more details. Prompts to enter a new destination of the current entry under the cursor. Will throw an error when the new destination already exists. Prepending a `count` to c will select the `count`_nth_ directory from -the left as base. See `:h carbon-buffer-move` for more details. +the left as base. See `:h carbon-view-move` for more details. ### d Deleting files and directories @@ -272,7 +247,7 @@ the left as base. See `:h carbon-buffer-move` for more details. Prompts confirmation to delete the current entry under the cursor. Press enter to confirm the currently highlighted option, D to confirm deletion directly, or escape to cancel. Prepending a `count` to c will select -the `count`_nth_ directory from the left as base. See `:h carbon-buffer-delete` +the `count`_nth_ directory from the left as base. See `:h carbon-view-delete` for more details. # File icons diff --git a/dev/init.lua b/dev/init.lua index 8bd9463..c47c42b 100644 --- a/dev/init.lua +++ b/dev/init.lua @@ -1,3 +1,4 @@ +vim.opt.termguicolors = true vim.opt.packpath:remove({ vim.env.HOME .. '/.local/share/nvim/site' }) vim.opt.runtimepath:remove({ vim.env.HOME .. '/.config/nvim' }) vim.opt.runtimepath:append({ @@ -5,4 +6,4 @@ vim.opt.runtimepath:append({ vim.env.HOME .. '/.local/share/nvim/site/pack/packer/start/nvim-web-devicons', }) -require('carbon').setup({ file_icons = false }) +require('carbon').setup({ file_icons = false, sync_pwd = true }) diff --git a/doc/carbon.txt b/doc/carbon.txt index 16589c9..24c95c7 100644 --- a/doc/carbon.txt +++ b/doc/carbon.txt @@ -1,7 +1,7 @@ *carbon.nvim.txt* The simple directory tree viewer for Neovim written in Lua. *carbon.txt* - `Version: 0.18.3` + `Version: 0.19.0` `Licence: MIT` `Source: https://github.com/SidOfc/carbon.nvim` `Author: Sidney Liebrand ` @@ -18,9 +18,9 @@ TABLE OF CONTENTS *carbon-contents* *carbon- SETUP `................` |carbon-setup| UTIL `.................` |carbon-util| - ENTRY `................` |carbon-entry| PLUGS `................` |carbon-plugs| - BUFFER `...............` |carbon-buffer| + ENTRY `................` |carbon-entry| + VIEW `.................` |carbon-view| CARBON `...............` |carbon-carbon| WATCHER `..............` |carbon-watcher| COMMANDS `.............` |carbon-commands| @@ -30,7 +30,17 @@ TABLE OF CONTENTS *carbon-contents* *carbon- ================================================================================ USAGE *carbon-usage* - Carbon automatically replaces |netrw| and remaps NetRW's |Explore| and |Lexplore| + In your |$MYVIMRC| file, run `require('carbon').setup()` to initialize Carbon. + + Depending on your plugin manager you may need to call this `setup` function + in a specific place or in a specific way. If you use any package manager + please consult its documentation to find out where `setup` functions may be run. + + When using regular Vim |packages| this `setup` function can simply be run + directly anywhere in your |$MYVIMRC| file since such packages are + automatically loaded. + + Carbon replaces |netrw| by default and remaps NetRW's |Explore| and |Lexplore| commands to Carbon's |carbon-command-Carbon| and |carbon-command-Lcarbon| commands respectively. For more specific usage and configuration information, see: @@ -42,7 +52,7 @@ USAGE *carbon-usag ================================================================================ SETUP *carbon-setup* - The behavior of this plugin can be customized by calling + The behavior of this plugin can be customized by providing settings to `require('carbon').setup` in your |$MYVIMRC| like this: init.lua: > @@ -84,7 +94,7 @@ CONSTANTS *carbon-constant Exposes a table containing all the constants which Carbon uses. `------------------------------------------------------------------------------` - hl *carbon-constants-hl* + hl *carbon-constant-hl* Usage: `require('carbon.constants').hl` Value: `carbon` @@ -92,23 +102,31 @@ CONSTANTS *carbon-constant Used for all default highlighting. `------------------------------------------------------------------------------` - hl_tmp *carbon-constants-hl-tmp* + hl_tmp *carbon-constant-hl-tmp* Usage: `require('carbon.constants').hl_tmp` Value: `carbon:tmp` - Used for temporary highlighting. Used only by |carbon-buffer-focus-flash| + Used for temporary highlighting. Used only by |carbon-view-focus-flash| at the moment. `------------------------------------------------------------------------------` - augroup *carbon-constants-augroup* + augroup *carbon-constant-augroup* Usage: `require('carbon.constants').augroup` Value `carbon:tmp` - Used for temporary highlighting. Used only by |carbon-buffer-focus-flash| + Used for temporary highlighting. Used only by |carbon-view-focus-flash| at the moment. + `------------------------------------------------------------------------------` + directions *carbon-constant-directions* + + Usage: `require('carbon.constants').directions` + Value `{ left = 'h', right = 'l', up = 'k', down = 'j' }` + + Helper to map "human" direction names to "vim" directions. + ================================================================================ COMMANDS *carbon-commands* @@ -122,9 +140,9 @@ COMMANDS *carbon-command Alias: `Explore` unless |carbon-setting-keep-netrw| is enabled. Replaces the current buffer with the Carbon buffer. - When called with a (`Carbon!`) or |carbon-setting-always-reveal| enabled, + When called with a (`Carbon!`) or |carbon-setting-auto-reveal| enabled, the tree will expand to show the current buffer path and - |carbon-buffer-flash-bang| it if possible. This also works for its alias. + |carbon-view-flash-bang| it if possible. This also works for its alias. `------------------------------------------------------------------------------` Lcarbon *carbon-command-Lcarbon* @@ -139,9 +157,9 @@ COMMANDS *carbon-command Subsequent calls will reuse a window previously opened via `Lcarbon` if this window still exists and valid. - When called with a (`Carbon!`) or |carbon-setting-always-reveal| enabled, + When called with a (`Carbon!`) or |carbon-setting-auto-reveal| enabled, the tree will expand to show the current buffer path and - |carbon-buffer-flash-bang| it if possible. This also works for its alias. + |carbon-view-flash-bang| it if possible. This also works for its alias. `------------------------------------------------------------------------------` Rcarbon *carbon-command-Rcarbon* @@ -183,9 +201,9 @@ COMMANDS *carbon-command in a vertical split relative to the current window. - When called with a (`Carbon!`) or |carbon-setting-always-reveal| enabled, + When called with a (`Carbon!`) or |carbon-setting-auto-reveal| enabled, the tree will expand to show the current buffer path and - |carbon-buffer-flash-bang| it if possible. + |carbon-view-flash-bang| it if possible. See |carbon-setting-float-settings| for more information on how to configure the floating window spawned by `:Fcarbon`. @@ -196,48 +214,48 @@ AUTOCMDS *carbon-autocmd This section documents the default |autocmd| commands that Carbon uses. `------------------------------------------------------------------------------` - BufEnter *carbon-autocmd-bufenter* + BufWinEnter *carbon-autocmd-bufwinenter* - Group: |carbon-constants-augroup| - Event: `BufEnter` + Group: |carbon-constant-augroup| + Event: `BufWinEnter` Pattern: - Implementation: `require('carbon.buffer').process_enter()` + Implementation: `view:show()` Local to Carbon buffers. - Calls |carbon-buffer-process-enter| when entering a Carbon buffer. + Calls |carbon-view-show| when entering a Carbon buffer. `------------------------------------------------------------------------------` BufHidden *carbon-autocmd-bufhidden* - Group: |carbon-constants-augroup| + Group: |carbon-constant-augroup| Event: `BufHidden` Pattern: - Implementation: `require('carbon.buffer').process_hidden()` + Implementation: `view:hide()` Local to Carbon buffers. - Calls |carbon-buffer-process-hidden| when the Carbon buffer is hidden. + Calls |carbon-view-hide| when the Carbon buffer is hidden. `------------------------------------------------------------------------------` CursorMovedI *carbon-autocmd-cursormovedi* - Group: |carbon-constants-augroup| + Group: |carbon-constant-augroup| Event: `CursorMovedI` Pattern: Implementation: Local to Carbon buffers. Called when moving the cursor in insert mode. - Used during |carbon-buffer-create| to control the line and minimul column + Used during |carbon-view-create| to control the line and minimul column offset of the cursor. `------------------------------------------------------------------------------` DirChanged *carbon-autocmd-dirchanged* - Group: |carbon-constants-augroup| + Group: |carbon-constant-augroup| Event: `DirChanged` Pattern: `global` Implementation: `require('carbon').cd()` - Calls |carbon-buffer-cd| when changing |pwd| using |cd|. + Calls |carbon-view-cd| when changing |pwd| using |cd|. ================================================================================ PLUGS *carbon-plugs* @@ -251,7 +269,7 @@ PLUGS *carbon-plug Implementation: `require('carbon').up()` Mapping: |carbon-setting-actions-up| - Sets |carbon-buffer-data-root| to the parent directory of the current + Sets the root of the current `view` to the parent directory of the current working directory. Accepts a [count], when given, jumps to [count]nth parent directory of the current working directory. @@ -261,10 +279,10 @@ PLUGS *carbon-plug Implementation: `require('carbon').down()` Mapping: |carbon-setting-actions-down| - Sets |carbon-buffer-data-root| to the directory under the cursor. If the entry - under the cursor is a file then the parent directory of the file's path will - be used. Accepts a [count] to allow navigating deeper into compressed paths. - See |carbon-setting-compress| and |carbon-buffer-down| for more information + Sets the root of the current `view` to the directory under the cursor. If the + entry under the cursor is a file then the parent directory of the file's path + will be used. Accepts a [count] to allow navigating deeper into compressed + paths. See |carbon-setting-compress| and |carbon-view-down| for more information about how compressed paths work and how they are handled. `------------------------------------------------------------------------------` @@ -301,7 +319,7 @@ PLUGS *carbon-plug Accepts a [count] to allow left to right selection of the entry to move when on a compressed path (|carbon-setting-compress|). - For more information see |carbon-buffer-move|. + For more information see |carbon-view-move|. `------------------------------------------------------------------------------` (carbon-reset) *carbon-plug-reset* @@ -309,8 +327,7 @@ PLUGS *carbon-plug Implementation: `require('carbon').reset()` Mapping: |carbon-setting-actions-reset| - Sets |carbon-buffer-data-root| to the initial directory that Neovim - was opened with. + Sets root of the current `view` to its initial directory. `------------------------------------------------------------------------------` (carbon-split) *carbon-plug-split* @@ -343,7 +360,7 @@ PLUGS *carbon-plug parent directories will be created using |mkdir|. Pressing without typing anything will not create anything. Press to cancel. - For more information see |carbon-buffer-create|. + For more information see |carbon-view-create|. `------------------------------------------------------------------------------` (carbon-delete) *carbon-plug-delete* @@ -355,7 +372,7 @@ PLUGS *carbon-plug under the cursor. Accepts a [count] to allow deleting directories "from left to right" when the cursor is on a compressed path (|carbon-setting-compress|). - For more information see |carbon-buffer-delete|. + For more information see |carbon-view-delete|. `------------------------------------------------------------------------------` (carbon-close-parent) *carbon-plug-close-parent* @@ -389,39 +406,43 @@ CARBON *carbon-carbo Signature: `require('carbon').setup(`{preferences}`)` + Initializes Carbon. + This method updates Carbon's |carbon-settings| with user {preferences}. The {preferences} argument can be a table which will be deep-merged with |carbon-settings-table| or a callback function which accepts |carbon-settings-table| as argument. The callback can modify the settings freely. See |carbon-setup| for init.vim / init.lua setup call examples. - See |carbon-lazy-init| when experiencing issues related to {preferences} not - being respected. - - `------------------------------------------------------------------------------` - initialize *carbon-carbon-initialize* - - Signature: `require('carbon').initialize()` - - Initializes Carbon. This method creates |carbon-commands|, |carbon-plugs|, - |carbon-autocmds|, and highlights. It hijacks NetRW depending on the value of + This method creates |carbon-commands|, |carbon-plugs|, |carbon-autocmds|, and + highlights. It hijacks NetRW depending on the value of |carbon-setting-keep-netrw| and automatically opens the Carbon buffer if Neovim was opened with a directory path depending on |carbon-setting-auto-open|. It also attaches a callback to the watcher to enable auto-refreshing the buffer when file system events occur. - This method should be called only once per Neovim instance and must be called - before any buffer is created otherwise such buffer will not function properly. + This method must be called within your init.vim / init.lua configuration. - *carbon-lazy-init* + `------------------------------------------------------------------------------` + explore_buf_dir *carbon-carbon-explore-buf-dir* - NOTE: When using a package manager such as https://github.com/folke/lazy.nvim - You may want to set `vim.g.carbon_lazy_init = true` or - `let g:carbon_lazy_init = 1` in your configuration to ensure that - `require('carbon').setup({ ... })` is executed before Carbon executes - `require('carbon').initialize()` (which is executed behind the - scenes). When this is not done, you might notice settings not - being respected. + Signature: `require('carbon').explore_buf_dir(`[{params}]`)` + + Called on |BufWinEnter| if |carbon-setting-open-on-dir| is enabled. + When the buffer being entered is a directory Carbon will show instead. + + The {params} attribute, when given, must be a Lua table with a `file` key + pointing to the absolute path of the buffer. + + If {params} is not passed or does not contain a `file` key, nothing is executed. + + `------------------------------------------------------------------------------` + win_resized *carbon-carbon-win-resized* + + Signature: `require('carbon').win_resized()` + + Resizes the sidebar to |carbon-setting-sidebar-width| after a |WinResized| + event has occurred. When no sidebar is present this method does nothing. `------------------------------------------------------------------------------` session_load_post *carbon-carbon-session-load-post* @@ -471,12 +492,12 @@ CARBON *carbon-carbo Signature: `require('carbon').explore(`[{options}]`)` - Show Carbon in the current window. Calls |carbon-buffer-show| internally. + Show Carbon in the current window. Calls |carbon-view-show| internally. Used by the |carbon-command-Carbon| command. When {options} is given and `options.bang` is `true` or when - |carbon-setting-always-reveal| is `true` then this calls - |carbon-buffer-expand-to-path| to expand the tree to reveal + |carbon-setting-auto-reveal| is `true` then this calls + |carbon-view-expand-to-path| to expand the tree to reveal the current buffer. `------------------------------------------------------------------------------` @@ -496,8 +517,8 @@ CARBON *carbon-carbo 3. `'left'` When {options} is given and `options.bang` is `true` or when - |carbon-setting-always-reveal| is `true` then this calls - |carbon-buffer-expand-to-path| to expand the tree to reveal + |carbon-setting-auto-reveal| is `true` then this calls + |carbon-view-expand-to-path| to expand the tree to reveal the current buffer. Subsequent calls to `:Lcarbon` will attempt to navigate to an existing @@ -540,8 +561,8 @@ CARBON *carbon-carbo and modifies how |carbon-carbon-edit| works for that window. When {options} is given and `options.bang` is `true` or when - |carbon-setting-always-reveal| is `true` then this calls - |carbon-buffer-expand-to-path| to expand the tree to reveal + |carbon-setting-auto-reveal| is `true` then this calls + |carbon-view-expand-to-path| to expand the tree to reveal the current buffer. `------------------------------------------------------------------------------` @@ -549,7 +570,7 @@ CARBON *carbon-carbo Signature: `require('carbon').up()` - Calls |carbon-buffer-up|. When the |buffer-data-root| is updated + Calls |carbon-view-up|. When the root of the current `view` is updated successfully, this method will move the cursor to the top of the buffer and rerender. @@ -558,7 +579,7 @@ CARBON *carbon-carbo Signature: `require('carbon').reset()` - Calls |carbon-buffer-reset|. When the |carbon-buffer-data-root| is updated + Calls |carbon-view-reset|. When the root of the current `view` is updated successfully, this method will move the cursor to the top of the buffer and rerender. @@ -567,7 +588,7 @@ CARBON *carbon-carbo Signature: `require('carbon').down()` - Calls |carbon-buffer-down|. When the |carbon-buffer-data-root| is updated + Calls |carbon-view-down|. When the root of the current `view` is updated successfully, this method will move the cursor to the top of the buffer and rerender. @@ -576,28 +597,28 @@ CARBON *carbon-carbo Signature: `require('carbon').create()` - Calls |carbon-buffer-create|. + Calls |carbon-view-create|. `------------------------------------------------------------------------------` move *carbon-carbon-move* Signature: `require('carbon').move()` - Calls |carbon-buffer-move|. + Calls |carbon-view-move|. `------------------------------------------------------------------------------` delete *carbon-carbon-delete* Signature: `require('carbon').delete()` - Calls |carbon-buffer-delete|. + Calls |carbon-view-delete|. `------------------------------------------------------------------------------` cd *carbon-carbon-cd* Signature: `require('carbon').cd(`[{path}]`)` - Calls |carbon-buffer-cd| to set |carbon-buffer-data-root| to {path}. If {path} + Calls |carbon-view-cd| to set the root of the current `view` to {path}. If {path} is not supplied then |vim.v| variable `vim.v.event.cwd` will be used. When updated successfully, this method will move the cursor to the top of the buffer and rerender. @@ -644,6 +665,24 @@ UTIL *carbon-uti methods must always be considered unstable and should not be used in code external to Carbon. + `------------------------------------------------------------------------------` + explore_path *carbon-util-explore-path* + + Signature: `require('carbon.util').explore_path(`{path}[, {view}]`)` + + Converts {path} to an absolute path. When {view} (|carbon-view-instance|) is + given its `root.path` will be used as a base instead of |uv.cwd()| when {path} is + relative. + + If the current view or current working directory is `/example/directory` + and this function is called with `../` then the result will be `/example`. + For `../other/directory` the result will be `/example/other/directory`. + + When {path} starts with a `/` an absolute path is assumed. In this case no + base path will be prepended. + + When {path} is an empty string, it will default to |uv.cwd()|. + `------------------------------------------------------------------------------` is_excluded *carbon-util-is-excluded* @@ -749,6 +788,54 @@ UTIL *carbon-uti {group} must be a string containing the group name. {properties} must be a table. + `------------------------------------------------------------------------------` + add_highlight *carbon-util-add-highlight* + + Signature: `require('carbon.util').add_highlight(`{buf}, {...}`)` + + Calls |nvim_buf_add_highlight| like this: + + `vim.api.nvim_buf_add_highlight(`{buf}, , {...}`)` + + will be set to |carbon-constant-hl|. + + `------------------------------------------------------------------------------` + window_neighbors *carbon-util-window-neighbors* + + Signature: `require('carbon.util').window_neighbors(`{win}, {sides}`)` + + Get windows on {sides} of {win} if present. The {sides} argument is a + table of directions. Available directions are keys defined in + |carbon-constant-directions|. + + Returns a table of results: + + `{` + `{` + `origin = `{win}`,` + `position = ,` + `target = ,` + `}`, + `... other sides defined in `{sides} + `}` + + `------------------------------------------------------------------------------` + find_buf_by_name *carbon-util-find-buf-by-name* + + Signature: `require('carbon.util').find_buf_by_name(`{name}`)` + + Returns |bufnr| of buffer with name {name} or `nil` when no buffer with + {name} exists. + + `------------------------------------------------------------------------------` + resolve *carbon-util-resolve* + + Signature: `require('carbon.util').resolve(`{path}`)` + + Returns normalized absolute path of given {path}. Uses |vim.fs.normalize| + followed by |fnamemodify| with `:p` as modifier to get the full path. + Trailing slashes are stripped. + `------------------------------------------------------------------------------` autocmd *carbon-util-autocmd* @@ -787,7 +874,7 @@ UTIL *carbon-uti For example: - `util.set_winhl(`..., {Normal = 'CarbonIndicator', FloatBorder = 'Normal'}`)` + `util.set_winhl(`0, {Normal = 'CarbonIndicator', FloatBorder = 'Normal'}`)` Will result in the following command being executed: @@ -1095,7 +1182,7 @@ ENTRY *carbon-entr Any truthy value will be treated as if the `entry` is compressible, any falsy value will be treated as if the `entry` is not compressible. - This method is used by |carbon-buffer-create| to control the rendered + This method is used by |carbon-view-create| to control the rendered structure while a new path is being created. `------------------------------------------------------------------------------` @@ -1164,196 +1251,226 @@ ENTRY *carbon-entr be excluded from the returned table. ================================================================================ -BUFFER *carbon-buffer* +VIEW *carbon-view* - Usage: `require('carbon.buffer')` + Usage: `require('carbon.view')` This module is one of Carbon's core modules. It provides methods and - utilities to show the directory tree, interact with it, and keep it + utilities to show directory trees, interact with them, and keep them synchronized with changes from the file system. - *carbon-buffer-data-root* + `------------------------------------------------------------------------------` + find *carbon-view-find* - The following sections may refer to a `data.root` entry object. - This `data.root` variable is local and private to this module. It is set to - a |carbon-entry-new| created with the value of |getcwd()| as only argument. + Signature: `require('carbon.view').find(`{path}`)` - This variable is the internal representation of the file tree which methods - such as |carbon-buffer-lines| can use as a data source or methods like - |carbon-buffer-up|, |carbon-buffer-down|, |carbon-buffer-reset|, or - |carbon-buffer-cd| can manipulate to enable navigating up from the current - working directory, down into a child, reset back to the original - directory Neovim was opened with, or set to an arbitrary path. + Returns an existing `view` (|carbon-view-instance|) for given {path} or `nil` if no + instance for {path} exists. `------------------------------------------------------------------------------` - sidebar_window_id *carbon-buffer-sidebar-window-id* + get *carbon-view-get* - Signature: `require('carbon.buffer').sidebar_window_id()` + Signature: `require('carbon.view').get(`{path}`)` - Returns the |winid| of the window opened by |carbon-command-Lcarbon|, - |carbon-command-Rcarbon|, or |carbon-command-ToggleSidebarCarbon|. + Returns an existing `view` (|carbon-view-instance|) for given {path}. Otherwise a + new `view` instance is created and returned. - Returns `nil` when no such window exists. + *carbon-view-instance* - `------------------------------------------------------------------------------` - focus_flash *carbon-buffer-focus-flash* + A view instance includes information about a specific root directory. It + holds various state attributes such as which directories have been expanded + and which entries are compressible. - Signature: `require('carbon.buffer').focus_flash(`{duration}, {group}, - {start}, {finish}`)` + Views instances exist to allow multiple Carbon buffers to exist which can + show different directories. They are stored in a list local to this module + to allow them being referenced by index. - Highlights a region from {start} to {finish} with {group} for {duration} - milliseconds. {start} and {finish} must be values accepted by - |vim.highlight.range|. + This is done to prevent confusion when moving the root directory of one view + instance to one which is already present in another view. - See |carbon-buffer-flash-bang| for more information about customizing the - highlighting properties when entries are revealed. + A view instance includes the following data structure: - `------------------------------------------------------------------------------` - expand_to_path *carbon-buffer-expand-to-path* + `{` + `index = ,` + `initial = ,` + `states = >>,` + `root = ` + `}` - Signature: `require('carbon.buffer').expand_to_path(`{path}`)` + `------------------------------------------------------------------------------` + activate *carbon-view-activate* - Expands the Carbon buffer to reveal {path} in the tree. If {path} is not - present within |carbon-buffer-data-root| this will result in a no-op. + Signature: `require('carbon.view').activate(`[{options}]`)` - When successful this sets `data.flash` to the entry identified by {path}. - This causes the next call to |carbon-buffer-render| to move to and highlight - the entry |carbon-buffer-flash-bang|. + Shows a `view` for `path` given in {options} in the current window. If `sidebar` + in {options} is set to `'right'` or `'left'` then a new sidebar explorer is opened + on that side. Otherwise if `float` is truthy then a floating explorer is + opened. If neither is set then an explorer is shown in the current window. - Given {path} will be converted to an absolute path automatically by means of - calling |fnamemodify| with the `:p` flag. You do not need to pass an - absolute path yourself. + `------------------------------------------------------------------------------` + current *carbon-view-current* - This method also works when Carbon is not currently visible. It will fail - only if {path} is not a string. + Signature: `require('carbon.view').current()` - The buffer is NOT rerendered automatically, this must be done manually by - calling |carbon-buffer-render| afterwards. + Returns the |carbon-view-instance| for the current buffer if it is a Carbon + explorer. When the current buffer is not a Carbon explorer `false` is returned. `------------------------------------------------------------------------------` - set_root *carbon-buffer-set-root* + execute *carbon-view-execute* - Signature: `require('carbon.buffer').set_root(`{new_root}`)` + Signature: `require('carbon.view').execute(`{callback}`)` - The {new_root} argument can be an absolute path to a directory or a - |carbon-entry-new| entry. When passed as an absolute path it is converted to - an entry. |carbon-buffer-data-root| is then set to this entry. + Calls {callback} when the current active buffer is a Carbon buffer. Does + nothing otherwise. - When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. + Executes {callback} with a table containing the following keys: - Returns the updated |carbon-buffer-data-root|. + `{` + `cursor =` |carbon-view-cursor|`,` + `view = `|carbon-view-instance|`,` + `}` `------------------------------------------------------------------------------` - launch *carbon-buffer-launch* + resync *carbon-view-resync* - Signature: `require('carbon.buffer').launch(`{new_root}`)` + Signature: `require('carbon.view').resync(`{path}`)` - Called during |carbon-carbon-initialize| when |carbon-setting-auto-open| is set - and Neovim is opened with a directory or no argument. Not called when - Neovim is opened with a regular file. + Stores {path} in `view.resync_paths` and defers synchronization by + |carbon-setting-sync-delay| milliseconds. This is done to batch multiple + modifications to directories which were made in quick succession. - Calls |carbon-buffer-set-root| with {new_root} and then calls |carbon-buffer-show|. - Also set the reset directory used by |carbon-plug-reset| to navigate back to. + Calls |carbon-entry-synchronize| on the root directory of every `view` after + |carbon-setting-sync-delay| amount of time has passed without any modifications. - Returns the updated |carbon-buffer-data-root|. + `------------------------------------------------------------------------------` + close_sidebar *carbon-view-close-sidebar* + + Signature: `require('carbon.view').close_sidebar()` + + Closes a sidebar Carbon explorer window. `------------------------------------------------------------------------------` - process_event *carbon-buffer-process-event* + close_float *carbon-view-close-float* + + Signature: `require('carbon.view').close_float()` + + Closes a floating Carbon explorer window. - Signature: `require('carbon.buffer').defer_resync()` + `------------------------------------------------------------------------------` + handle_sidebar_or_float *carbon-view-handle-sidebar-or-float* - During |carbon-carbon-initialize| this method is attached using - |carbon-watcher-on| to process file change events. + Signature: `require('carbon.view').handle_sidebar_or_float()` - When this method is called, it will start a timer for - |carbon-setting-sync-delay| milliseconds after which |carbon-buffer-synchronize| - is called. When called twice within a single |carbon-setting-sync-delay| - the previous timer will be cancelled and a new one will be started. + Executed before opening new files to handle sidebar/float mechanics i.e. + when |carbon-command-ToggleSidebarCarbon| is used to open a sidebar explorer and + a file is edited from there this function ensures that the buffer to the side + of the explorer is used to show the file. If such buffer does not exist it + creates it. `------------------------------------------------------------------------------` - process_enter *carbon-buffer-process-enter* + expand_to_path *carbon-view-expand-to-path* + + Signature: `view:expand_to_path(`{path}`)` - Signature: `require('carbon.buffer').process_enter()` - Autocmd: |carbon-autocmd-bufenter| + The variable `view` in this section refers to a |carbon-view-instance|. - This method sets |fillchars| `eob` to a space to hide the tilde characters after - the end of the buffer and disables |wrap|. + Expands the Carbon buffer to reveal {path} in the tree. If {path} is not + present within the root of the current `view` this will result in a no-op. + + When successful this sets `data.flash` to the entry identified by {path}. + This causes the next call to |carbon-view-render| to move to and highlight + the entry |carbon-view-flash-bang|. + + Given {path} will be converted to an absolute path via |carbon-util-resolve|. + + This method also works when Carbon is not currently visible. It will fail + only if {path} is not a string. + + The buffer is NOT rerendered automatically, this must be done manually by + calling |carbon-view-render| afterwards. `------------------------------------------------------------------------------` - process_hidden *carbon-buffer-process-hidden* + get_path_attr *carbon-view-get-path-attr* - Signature: `require('carbon.buffer').process_hidden()` - Autocmd: |carbon-autocmd-bufhidden| + Signature: `view:get_path_attr(`{path}, {attr}`)` - This method restores |fillchars| and |wrap| back to their original values - before |carbon-buffer-process-enter| was called. + The variable `view` in this section refers to a |carbon-view-instance|. - It also `nils` out any `vim.w.carbon_*` window-local variables which are used - internally by some of Carbon's commands. + Get {attr} for {path} in the current `view`. When {attr} is `'compressible'` + and its value is `nil` then `true` is returned. `------------------------------------------------------------------------------` - is_loaded *carbon-buffer-is-loaded* + set_path_attr *carbon-view-set-path-attr* + + Signature: `view:set_path_attr(`{path}, {attr}`)` - Signature: `require('carbon.buffer').is_loaded()` + The variable `view` in this section refers to a |carbon-view-instance|. - Returns `true` or `false` depending on whether the current Carbon buffer handle is - valid and loaded. Uses |nvim_buf_is_loaded| internally. + Set {attr} for {path} in the current `view`. `------------------------------------------------------------------------------` - is_hidden *carbon-buffer-is-hidden* + buffers *carbon-view-buffers* - Signature: `require('carbon.buffer').is_hidden()` + Signature: `view:buffers()` - Returns `nil` if there is no Carbon buffer handle, `true` if the current buffer - handle is hidden, and `false` if the current buffer handle is not hidden. - A buffer is deemed to be hidden when |carbon-util-bufwinid| returns `nil`. + The variable `view` in this section refers to a |carbon-view-instance|. + + Returns a list of buffers which belong to the current `view`. This is done using + an internal `view.index` property which is stored in each buffer's `carbon` + local variable which is attached to new Carbon buffers via |carbon-view-buffer|. `------------------------------------------------------------------------------` - handle *carbon-buffer-handle* + buffer *carbon-view-buffer* + + Signature: `view:buffer()` - Signature: `require('carbon.buffer').handle()` + The variable `view` in this section refers to a |carbon-view-instance|. - Checks the current buffer handle and if valid, returns it. Otherwise creates - and configures a new buffer, updates the current buffer handle and returns - the new buffer handle. + Checks if a buffer for the current `view` already exists in + |carbon-view-buffers| and returns it. Otherwise creates and configures a new + buffer, updates the current buffer handle and returns the new buffer handle. - The buffer name will always be `'carbon'` and the following buffer-local - options are always set: + The buffer name is set to the root directory of the current `view` and the + following buffer-local options are always set: |swapfile| => `false` - |filetype| => `'carbon'` - |bufhidden| => `'hide'` + |filetype| => `'carbon.explorer'` + |bufhidden| => `'wipe'` |buftype| => `'nofile'` |modifiable| => `false` |modified| => `false` - Finally, if there are actions to map in |carbon-setting-actions| then they - will be mapped locally to this buffer to a || mapping with the - same suffix from |carbon-plugs|. + Actions in |carbon-setting-actions| will be mapped locally to this buffer to a + || mapping with the same suffix from |carbon-plugs|. + + Additionally, a buffer-local `carbon` variable is set so that Carbon can + figure out which buffers belong to Carbon. `------------------------------------------------------------------------------` - show *carbon-buffer-show* + update *carbon-view-update* + + Signature: `view:update()` - Signature: `require('carbon.buffer').show()` + The variable `view` in this section refers to a |carbon-view-instance|. - Show the Carbon buffer in the current window. Triggers a |carbon-buffer-render|. + Clears lines of the current `view` cached via |carbon-view-current-lines|. + Does not automatically rerender the buffer! `------------------------------------------------------------------------------` - render *carbon-buffer-render* + render *carbon-view-render* - Signature: `require('carbon.buffer').render()` + Signature: `view:render()` - Renders the Carbon buffer. The contents are determined by calling the - |carbon-buffer-lines| method with the `data.root` entry object as only - argument. See |carbon-buffer-data-root| for more information about `data.root`. + The variable `view` in this section refers to a |carbon-view-instance|. - The Carbon buffer is NOT rendered if |carbon-buffer-is-loaded| returns - `false` or if |carbon-buffer-is-hidden| returns `true`. + Renders the Carbon buffer. The contents are determined by calling the + |carbon-view-lines| method with the root entry object of the current `view` as + only argument. - *carbon-buffer-flash-bang* + *carbon-view-flash-bang* After rendering, if `data.flash` is set to an entry which is currently visible, - (it is set by |carbon-buffer-expand-to-path|) then the cursor will move to + (it is set by |carbon-view-expand-to-path|) then the cursor will move to that entry and reset `data.flash` to `nil`. Additionally, when this happens, that entry is also highlighted using @@ -1361,46 +1478,11 @@ BUFFER *carbon-buffe highlight are controlled by |carbon-setting-flash|. `------------------------------------------------------------------------------` - cursor *carbon-buffer-cursor* - - Signature: `require('carbon.buffer').cursor(`[{opts}]`)` - - Returns a table with the following shape: -> - { - line = , - target = , - target_line = , - } -< - Property: `line` + lines *carbon-view-lines* - An item at index `line('.')` from |carbon-buffer-lines|. + Signature: `view:lines(`{target}[, {lines}[, {depth}]]`)` - Property: `target` - - Equal to `line.entry` by default but when called during a mapping, will respect - |v:count| where possible. This is used for example to start deleting from - "left to right" on compressed paths by prefixing a mapping with a [count]. - - When {opts}`.target_directory_only` is truthy and `target.is_directory` is `false` - then `target` will be set to `line.path[#line.path] or target.parent` BEFORE - any [count] logic is applied. - - One way to describe the difference between `line.entry` and `target` is that - `line.entry` will always point to the last path component entry on the current - line whereas `target` points to the entry targetted by the user. - - Property: `target_line` - - Like `line` but `target_line` refers to the line which `target` is located on. - - For more information about compressed paths, see: |carbon-setting-compress|. - - `------------------------------------------------------------------------------` - lines *carbon-buffer-lines* - - Signature: `require('carbon.buffer').lines(`{entry}[, {lines}[, {depth}]]`)` + The variable `view` in this section refers to a |carbon-view-instance|. The {entry} argument must be an entry object as returned by |carbon-entry-new|. The {lines} argument is optional. Defaults to an empty table `{}`. When {lines} @@ -1431,17 +1513,17 @@ BUFFER *carbon-buffe Property: `lnum` - The line number of the entry. Used by |carbon-buffer-create| to determine + The line number of the entry. Used by |carbon-view-create| to determine the line number to move to for editing. Property: `depth` - The indent depth of the entry. Used by |carbon-buffer-create| to determine + The indent depth of the entry. Used by |carbon-view-create| to determine the start column to move to for editing. Property: `icon_width` - Width of file icons shown before a path. Used by |carbon-buffer-create| to + Width of file icons shown before a path. Used by |carbon-view-create| to determine the start column to move to for editing. Property: `highlights` @@ -1455,176 +1537,108 @@ BUFFER *carbon-buffe , } < - |carbon-buffer-render| uses |nvim_buf_add_highlight| to set the highlights. + |carbon-view-render| uses |nvim_buf_add_highlight| to set the highlights. It is called the following way: -> - vim.api.nvim_buf_add_highlight( - buffer.handle(), - require('util.constants').hl, - highlight[1], - lnum - 1, - highlight[2], - highlight[3], - ) -< + + `vim.api.nvim_buf_add_highlight(` + |carbon-view-buffer|, + |carbon-constant-hl|, + `highlight[1],` + `lnum - 1,` + `highlight[2],` + `highlight[3],` + `)` + Property: `path` When |carbon-setting-compress| is enabled, this table is filled with the compressed parent entry objects of `entry`. `------------------------------------------------------------------------------` - synchronize *carbon-buffer-synchronize* - - Signature: `require('carbon.buffer').synchronize()` - - Calls |carbon-entry-synchronize| on |carbon-buffer-data-root| and calls - |carbon-buffer-render| afterwards. - - `------------------------------------------------------------------------------` - up *carbon-buffer-up* + current_lines *carbon-view-current-lines* - Signature: `require('carbon.buffer').up(`[{count}]`)` + Signature: `view:current_lines()` - Set |carbon-buffer-data-root| to its {count}th parent directory. When {count} - is not supplied, it will use `vim.v.count1` which defaults to `1` or [count] if - specified while executing this method in a mapping. + The variable `view` in this section refers to a |carbon-view-instance|. - This method moves the root up one level at a time. It queries the children - of the parent and replaces the child with the same path as the current - `data.root` with `data.root`. It also sets `data.root` to an open state so - when "zooming out" you still see what you had open. - - After doing all this, the `data.root` is finally set to the actual parent. - This routine then is repeated {count} `- 1` more times. - - When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. - - Returns `true` if the root is replaced at least once. `nil` otherwise. This - can happen when you are at your OS root directory and try to go up more. + Returns cached lines of the current `view` generated from |carbon-view-lines| if + present. Otherwise |carbon-view-lines| is called and its results are cached. `------------------------------------------------------------------------------` - down *carbon-buffer-down* + cursor *carbon-view-cursor* - Signature: `require('carbon.buffer').down(`[{count}]`)` + Signature: `view:cursor(`[{opts}]`)` - Set |carbon-buffer-data-root| to its {count}th child directory below the - cursor. When {count} is not supplied, it will use `vim.v.count1` which defaults - to `1` or [count] if specified while executing this method in a mapping. + The variable `view` in this section refers to a |carbon-view-instance|. - Specifying a {count} is only useful when the cursor is on a compressed path. - The |carbon-setting-compress| section explains how compressed paths work. - - With the cursor on this path `a/b/c/d/e.txt`, calling this method without - specifying a {count} will move the root to `/a`, you will now see the path - `b/c/d/e.txt`. If you call the method again, but this time you supply `2` as - {count} you will navigate "past" `c/` directly into `d/`. You will now only - see `e.txt`. - - When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. - - Returns `true` when the root is replaced, `nil` when the `new_root` path is the - same as the current `data.root` path in which case the root is not replaced. - - `------------------------------------------------------------------------------` - cd *carbon-buffer-cd* - - Signature: `require('carbon.buffer').cd(`{path}`)` - - Set |carbon-buffer-data-root| to a |carbon-entry-new| created using {path}. - This method remembers the state of all child directories when navigating up - directories. When navigating to a child of the current working directory - Carbon tries look for any children in its own state to jump to without - having to perform additional file system reads. In any other case, Carbon - navigates to the {path} entry object. - - When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. - - `------------------------------------------------------------------------------` - reset *carbon-buffer-reset* + Returns a table with the following shape: +> + { + line = , + target = , + target_line = , + } +< + Property: `line` - Signature: `require('carbon.buffer').reset()` + An item at index `line('.')` from |carbon-view-lines|. - Set |carbon-buffer-data-root| to the original |pwd| using |carbon-buffer-cd|. - When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. + Property: `target` - `------------------------------------------------------------------------------` - set_lines *carbon-buffer-set-lines* + Equal to `line.entry` by default but when called during a mapping, will respect + |v:count| where possible. This is used for example to start deleting from + "left to right" on compressed paths by prefixing a mapping with a [count]. - Signature: `require('carbon.buffer').set_lines(`{from}, {to}, {lines}`)` + When {opts}`.target_directory_only` is truthy and `target.is_directory` is `false` + then `target` will be set to `line.path[#line.path] or target.parent` BEFORE + any [count] logic is applied. - Makes the Carbon buffer |modifiable| and calls |nvim_buf_set_lines| like this: + One way to describe the difference between `line.entry` and `target` is that + `line.entry` will always point to the last path component entry on the current + line whereas `target` points to the entry targetted by the user. - `vim.api.nvim_buf_set_lines(`, {from}, {to}, 1, {lines}`)` + Property: `target_line` - After lines have been inserted Carbon sets |modified| to `false`. - Will also set |nomodifiable| to `true` except when called in |insert-mode|. + Like `line` but `target_line` refers to the line which `target` is located on. - will be set to the |bufnr| of the Carbon buffer. + For more information about compressed paths, see: |carbon-setting-compress|. `------------------------------------------------------------------------------` - add_highlight *carbon-buffer-add-highlight* - - Signature: `require('carbon.buffer').add_highlight(`{hl}, {lnum}, {from}, {to}`)` - - Calls |nvim_buf_add_highlight| like this: - - `vim.api.nvim_buf_add_highlight(`, , {hl}, {lnum}, {from}, {to}`)` + show *carbon-view-show* - will be set to the |bufnr| of the Carbon buffer. - will be set to the namespace stored internally by Carbon. + Signature: `view:show()` - `------------------------------------------------------------------------------` - clear_namespace *carbon-buffer-clear-namespace* + The variable `view` in this section refers to a |carbon-view-instance|. - Signature: `require('carbon.buffer').clear_namespace(`{from}, {to}`)` + Sets the following local options: - Calls |nvim_buf_clear_namespace| like this: + - |wrap| to `false` + - |spell| to `false` + - |fillchars| to `{ eob = ' ' }` - `vim.api.nvim_buf_clear_namespace(`, , {from}, {to}`)` - - will be set to the |bufnr| of the Carbon buffer. - will be set to the namespace stored internally by Carbon. + Show the Carbon buffer in the current window. Triggers a |carbon-view-render|. `------------------------------------------------------------------------------` - clear_extmarks *carbon-buffer-clear-extmarks* - - Signature: `require('carbon.buffer').clear_extmarks(`{from}, {to}, {opts}`)` + hide *carbon-view-hide* - Wraps |nvim_buf_get_extmarks| and |nvim_buf_del_extmark|. This function - first calls |nvim_buf_get_extmarks| like this: + Signature: `view:hide()` - `vim.api.nvim_buf_get_extmarks(`, , {from}, {to}, {opts}`)` + The variable `view` in this section refers to a |carbon-view-instance|. - The resulting list of |extmarks| will all be cleared by calling - |nvim_buf_del_extmark| like this: + Resets the following local options to their global defaults: - `vim.api.nvim_buf_del_extmark(`, , `)` + - |wrap| + - |spell| + - |fillchars| - will be set to the |bufnr| of the Carbon buffer. - will be set to the namespace stored internally by Carbon. + Clears sidebar and float flags. `------------------------------------------------------------------------------` - entry_line *carbon-buffer-entry-line* - - Signature: `require('carbon.buffer').entry_line(`{entry}`)` - - Returns line information for given {entry}. The format is the same as a - single line result from |carbon-buffer-lines|. When {entry} is not visible - in the buffer this function returns `nil`. - - An entry is visible when it is shown in the buffer. This is also true for - compressed path entries. While they are not displayed on their "own" line - they are visible "somewhere" in the buffer. - - In those cases, Carbon returns the line info of the rendered entry which - will then have {entry} in its `line.path`. + create *carbon-view-create* - See |carbon-setting-compress| for more information about compressed paths. + Signature: `view:create()` - `------------------------------------------------------------------------------` - create *carbon-buffer-create* - - Signature: `require('carbon.buffer').create()` + The variable `view` in this section refers to a |carbon-view-instance|. When called, puts the Carbon buffer in a context-aware insert mode which allows a path to be typed. Pressing will confirm creation and pressing @@ -1735,9 +1749,11 @@ BUFFER *carbon-buffe behavior as if no count was supplied will be executed. `------------------------------------------------------------------------------` - delete *carbon-buffer-delete* + delete *carbon-view-delete* + + Signature: `view:delete()` - Signature: `require('carbon.buffer').delete()` + The variable `view` in this section refers to a |carbon-view-instance|. Prompts confirmation to delete the current entry under the cursor. When the current entry is a regular file or executable the file itself will be @@ -1796,9 +1812,11 @@ BUFFER *carbon-buffe then it will select the last component (the actual entry of that line). `------------------------------------------------------------------------------` - move *carbon-buffer-move* + move *carbon-view-move* - Signature: `require('carbon.buffer').move()` + Signature: `view:move()` + + The variable `view` in this section refers to a |carbon-view-instance|. Prompts the user for a new destination for the entry below the cursor. A [count] can be supplied to move starting from left to right on compressed @@ -1807,6 +1825,159 @@ BUFFER *carbon-buffe Intermediate directories will be created if they do not exist. + `------------------------------------------------------------------------------` + focus_flash *carbon-view-focus-flash* + + Signature: `view:focus_flash(`{duration}, {group}, {start}, {finish}`)` + + The variable `view` in this section refers to a |carbon-view-instance|. + + Highlights a region from {start} to {finish} with {group} for {duration} + milliseconds. {start} and {finish} must be values accepted by + |vim.highlight.range|. + + See |carbon-view-flash-bang| for more information about customizing the + highlighting properties when entries are revealed. + + `------------------------------------------------------------------------------` + set_root *carbon-view-set-root* + + Signature: `view:set_root(`{target}[, {opts}]`)` + + The variable `view` in this section refers to a |carbon-view-instance|. + + The {target} argument can be an absolute path to a directory or a + |carbon-entry-new| entry. When passed as an absolute path it is converted to + an entry. The root of the current `view` is then set to this entry. + + When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. + This is only done when the original root directory matches the current + working directory. + + The |carbon-view-buffer| name will be set to the new root of the `view` + unless {opts} is given and `opts.rename` is set to `false`. + + `------------------------------------------------------------------------------` + cd *carbon-view-cd* + + Signature: `view:cd(`{path}`)` + + The variable `view` in this section refers to a |carbon-view-instance|. + + Set the root of the current `view` to specified {path}. + When {path} is a parent of the current root directory of `view` then + |carbon-view-up| is used. Otherwise the root directory is set directly via + |carbon-view-set-root|. + + `------------------------------------------------------------------------------` + reset *carbon-view-reset* + + Signature: `view:reset()` + + The variable `view` in this section refers to a |carbon-view-instance|. + + Reset the root of the current `view` to the directory it originally showed. + + `------------------------------------------------------------------------------` + up *carbon-view-up* + + Signature: `view:up(`[{count}]`)` + + The variable `view` in this section refers to a |carbon-view-instance|. + + Set the root of the current `view` to its {count}th parent directory. When + {count} is not supplied, it will use `vim.v.count1` which defaults to `1` or + [count] if specified while executing this method in a mapping. + + When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. + This is only done when the original root directory matches the current + working directory. + + Returns `true` if the root is replaced at least once. `nil` otherwise. This + can happen when you are at your OS root directory and try to go up more. + + This method also tries to open the intermediate parent directories while + navigating to the desired location. + + This is not always possible to enfore because Carbon can have multiple views + representing different directories and the name of Carbon `view` buffers will + be the absolute path to these directories. + + This causes issues when trying to navigate to a path for which a `view` + already exists, because Carbon will rename the buffer to match its current + root path which will yield |E95|. + + To solve this Carbon checks if a `view` for the desired destination exists. + If this is the case, then Carbon skips traversing and opening parent + directories and will instead show the existing view directly. + + In doing this, Carbon will no longer expand intermediate parents between the + original `view` root and the destination `view` root. + + `------------------------------------------------------------------------------` + down *carbon-view-down* + + Signature: `view:down(`[{count}]`)` + + The variable `view` in this section refers to a |carbon-view-instance|. + + Set the root of the current `view` to its {count}th child directory below the + cursor. When {count} is not supplied, it will use `vim.v.count1` which defaults + to `1` or [count] if specified while executing this method in a mapping. + + Specifying a {count} is only useful when the cursor is on a compressed path. + The |carbon-setting-compress| section explains how compressed paths work. + + With the cursor on this path `a/b/c/d/e.txt`, calling this method without + specifying a {count} will move the root to `/a`, you will now see the path + `b/c/d/e.txt`. If you call the method again, but this time you supply `2` as + {count} you will navigate "past" `c/` directly into `d/`. You will now only + see `e.txt`. + + When |carbon-setting-sync-pwd| is enabled Neovim's |pwd| is updated automatically. + This is only done when the original root directory matches the current + working directory. + + Returns `true` when the root is replaced, `nil` when the `new_root` path is the + same as the current `data.root` path in which case the root is not replaced. + + `------------------------------------------------------------------------------` + parents *carbon-view-parents* + + Signature: `view:parents(`[{count}]`)` + + The variable `view` in this section refers to a |carbon-view-instance|. + + Returns a table of parent entries (|carbon-entry-new|) sorted from nearest to + farthest i.e. given a view whose root is `/a/b/c/d` and {count} set to `4` this + function returns: + + `{` + `,` + `,` + `,` + `,` + `}` + + An empty table is returned when there are no parents. If {count} is not + supplied it will default to `1` and result in either a table with a single + parent entry, or an empty table if there are no parents (i.e. the root + directory of the operating system). + + `------------------------------------------------------------------------------` + switch_to_existing_view *carbon-view-switch-to-existing-view* + + Signature: `view:switch_to_existing_view(`{path}`)` + + The variable `view` in this section refers to a |carbon-view-instance|. + + If an existing `view` instance exists which has its root set to {path} the + current window will have its current buffer replaced for the buffer + belonging to that existing view. + + Additionally, if the current `view` (before switching) root equals |uv.cwd()| and + |carbon-setting-sync-pwd| is set then the cwd of Neovim is updated as well. + ================================================================================ WATCHER *carbon-watcher* @@ -1969,7 +2140,8 @@ SETTINGS *carbon-setting ` sidebar_width = `|carbon-setting-sidebar-width|`,` ` sidebar_toggle_focus = `|carbon-setting-sidebar-toggle-focus|`,` ` sidebar_position = `|carbon-setting-sidebar-position|`,` - ` always_reveal = `|carbon-setting-always-reveal|`,` + ` auto_reveal = `|carbon-setting-auto-reveal|`,` + ` open_on_dir = `|carbon-setting-open-on-dir|`,` ` exclude = `|carbon-setting-exclude|`,` ` indicators = {` @@ -2109,7 +2281,8 @@ SETTINGS *carbon-setting Disabled by default when |autochdir| is set to `1`, enabled by default when |autochdir| set to `0`. When enabled, Carbon listens for `:cd` commands. Once a `:cd` command is executed Carbon will automatically update - |carbon-buffer-data-root| to that path. + the root of a `view` pointing to the original current working directory + to the `:cd` path. `------------------------------------------------------------------------------` sync_delay *carbon-setting-sync-delay* @@ -2142,7 +2315,7 @@ SETTINGS *carbon-setting Default sidebar position, see |carbon-carbon-explore-sidebar| for context. `------------------------------------------------------------------------------` - always_reveal *carbon-setting-always-reveal* + auto_reveal *carbon-setting-auto-reveal* Default: `false` @@ -2150,6 +2323,13 @@ SETTINGS *carbon-setting the current buffer. Enabling this will cause all versions of these commands to reveal the current buffer. + `------------------------------------------------------------------------------` + open_on_dir *carbon-setting-open-on-dir* + + Default: `true` + + Shows a Carbon buffer when opening directories. + `------------------------------------------------------------------------------` exclude *carbon-setting-exclude* @@ -2198,7 +2378,7 @@ SETTINGS *carbon-setting `}` Controls highlight {delay} and {duration} for entries revealed by - |carbon-buffer-expand-to-path|. Setting it to `nil` will disable + |carbon-view-expand-to-path|. Setting it to `nil` will disable highlighting of revealed entries. Uses |carbon-setting-highlights-CarbonFlash| to highlight the revealed entry. @@ -2210,13 +2390,13 @@ SETTINGS *carbon-setting function() local columns = vim.opt.columns:get() local rows = vim.opt.lines:get() - local width = math.min(50, columns * 0.8) - local height = math.min(20, rows * 0.8) + local width = math.min(40, columns * 0.9) + local height = math.min(20, rows * 0.9) return { relative = 'editor', style = 'minimal', - border = 'single', + border = 'rounded', width = width, height = height, col = math.floor(columns / 2 - width / 2), @@ -2307,6 +2487,10 @@ SETTINGS *carbon-setting ` ctermfg = 'DarkGray',` ` bold = true,` ` },` + ` CarbonFloat = {` *carbon-setting-highlights-CarbonFloat* + ` bg = '#111111',` + ` ctermbg = 'Black',` + ` },` ` CarbonDanger = {` *carbon-setting-highlights-CarbonDanger* ` link = 'Error',` ` },` diff --git a/lua/carbon.lua b/lua/carbon.lua deleted file mode 100644 index 67a2df2..0000000 --- a/lua/carbon.lua +++ /dev/null @@ -1,382 +0,0 @@ -local util = require('carbon.util') -local buffer = require('carbon.buffer') -local watcher = require('carbon.watcher') -local settings = require('carbon.settings') -local carbon = {} -local data = { initialized = false } - -function carbon.setup(user_settings) - if data.initialized then - return - end - - if type(user_settings) == 'function' then - user_settings(settings) - else - local next = vim.tbl_deep_extend('force', settings, user_settings) - - for setting, value in pairs(next) do - settings[setting] = value - end - - if type(user_settings.highlights) == 'table' then - settings.highlights = - vim.tbl_extend('force', settings.highlights, user_settings.highlights) - end - end - - if vim.g.carbon_lazy_init ~= nil then - carbon.initialize() - end - - return settings -end - -function carbon.initialize() - if data.initialized then - return - else - data.initialized = true - end - - watcher.on('carbon:synchronize', buffer.defer_resync) - - util.command('Carbon', carbon.explore, { bang = true }) - util.command('Lcarbon', carbon.explore_left, { bang = true }) - util.command('Rcarbon', carbon.explore_right, { bang = true }) - util.command('Fcarbon', carbon.explore_float, { bang = true }) - util.command('ToggleSidebarCarbon', carbon.toggle_sidebar, { bang = true }) - - for action in pairs(settings.defaults.actions) do - vim.keymap.set('', util.plug(action), carbon[action]) - end - - if settings.sync_on_cd then - util.autocmd('DirChanged', carbon.cd, { pattern = 'global' }) - end - - util.autocmd('SessionLoadPost', carbon.session_load_post, { pattern = '*' }) - - if type(settings.highlights) == 'table' then - for group, properties in pairs(settings.highlights) do - util.highlight(group, properties) - end - end - - if not settings.keep_netrw then - vim.g.loaded_netrw = 1 - vim.g.loaded_netrwPlugin = 1 - - pcall(vim.api.nvim_del_augroup_by_name, 'FileExplorer') - pcall(vim.api.nvim_del_augroup_by_name, 'Network') - - util.command('Explore', carbon.explore, { bang = true }) - util.command('Lexplore', carbon.explore_left, { bang = true }) - util.command('Rexplore', carbon.explore_right, { bang = true }) - util.command('ToggleSidebarExplore', carbon.toggle_sidebar, { bang = true }) - end - - local argv = vim.fn.argv() - local open = argv[1] and vim.fn.fnamemodify(argv[1], ':p') or vim.loop.cwd() - - if - vim.fn.has('vim_starting') - and settings.auto_open - and util.is_directory(open) - then - local current_buffer = vim.api.nvim_win_get_buf(0) - - buffer.launch(open) - - if vim.api.nvim_buf_is_valid(current_buffer) then - vim.api.nvim_buf_delete(current_buffer, { force = true }) - end - end - - return carbon -end - -function carbon.session_load_post(event) - local buffer_name = vim.api.nvim_buf_get_name(event.buf) - - if string.match(buffer_name, 'carbon$') then - vim.api.nvim_buf_set_name(event.buf, '_carbon') - buffer.show() - vim.api.nvim_buf_delete(event.buf, { force = true }) - - if vim.api.nvim_win_get_width(0) == settings.sidebar_width then - vim.w.carbon_sidebar_window = vim.api.nvim_get_current_win() - end - end -end - -function carbon.toggle_recursive() - local line = buffer.cursor().line - - if line.entry.is_directory then - line.entry:set_open(not line.entry:is_open(), true) - - buffer.render() - end -end - -function carbon.edit() - local line = buffer.cursor().line - local keepalt = #vim.fn.getreg('#') ~= 0 - - if line.entry.is_directory then - line.entry:set_open(not line.entry:is_open()) - - buffer.render() - elseif vim.w.carbon_sidebar_window then - local split_right = vim.w.carbon_sidebar_split == 'botright' - local split = split_right and 'topleft' or 'botright' - local check_position = split_right and 'h' or 'l' - - vim.cmd({ cmd = 'wincmd', args = { check_position } }) - - if vim.w.carbon_sidebar_window == vim.api.nvim_get_current_win() then - vim.cmd({ - cmd = 'split', - args = { line.entry.path }, - mods = { vertical = true, split = split }, - }) - - vim.api.nvim_win_set_width( - util.bufwinid(buffer.handle()), - settings.sidebar_width - ) - else - vim.cmd({ - cmd = 'edit', - args = { line.entry.path }, - mods = { keepalt = keepalt }, - }) - end - else - if vim.w.carbon_fexplore_window then - vim.api.nvim_win_close(0, 1) - end - - vim.cmd({ - cmd = 'edit', - args = { line.entry.path }, - mods = { keepalt = keepalt }, - }) - end -end - -function carbon.split() - local line = buffer.cursor().line - - if not line.entry.is_directory then - if vim.w.carbon_fexplore_window then - vim.api.nvim_win_close(0, 1) - end - - vim.cmd({ cmd = 'split', args = { line.entry.path } }) - end -end - -function carbon.vsplit() - local line = buffer.cursor().line - - if not line.entry.is_directory then - if vim.w.carbon_fexplore_window then - vim.api.nvim_win_close(0, 1) - end - - vim.cmd({ cmd = 'vsplit', args = { line.entry.path } }) - end -end - -function carbon.explore(options) - if options and options.bang or settings.always_reveal then - buffer.expand_to_path(vim.fn.expand('%')) - end - - buffer.show() -end - -function carbon.toggle_sidebar(options) - local current_win = vim.api.nvim_get_current_win() - local existing_win = buffer.sidebar_window_id() - - if existing_win then - vim.api.nvim_win_close(existing_win, 1) - else - local explore_options = vim.tbl_extend( - 'force', - options or {}, - { position = settings.sidebar_position } - ) - - carbon.explore_sidebar(explore_options) - - if not settings.sidebar_toggle_focus then - vim.api.nvim_set_current_win(current_win) - end - end -end - -function carbon.explore_sidebar(options) - if type(options) ~= 'table' then - options = {} - end - - if options.bang or settings.always_reveal then - buffer.expand_to_path(vim.fn.expand('%')) - end - - local existing_win = buffer.sidebar_window_id() - local position = options.position or settings.sidebar_position - local split = position == 'right' and 'botright' or 'topleft' - - if existing_win then - vim.api.nvim_set_current_win(existing_win) - buffer.show() - else - vim.cmd({ cmd = 'split', mods = { vertical = true, split = split } }) - buffer.show() - vim.api.nvim_win_set_width(0, settings.sidebar_width) - - vim.w.carbon_sidebar_window = vim.api.nvim_get_current_win() - vim.w.carbon_sidebar_split = split - end -end - -function carbon.explore_left(options) - local existing_win = buffer.sidebar_window_id() - - if existing_win and buffer.sidebar_window_split() ~= 'topleft' then - vim.api.nvim_win_close(existing_win, 1) - end - - carbon.explore_sidebar( - vim.tbl_extend('force', options or {}, { position = 'left' }) - ) -end - -function carbon.explore_right(options) - local existing_win = buffer.sidebar_window_id() - - if existing_win and buffer.sidebar_window_split() ~= 'botright' then - vim.api.nvim_win_close(existing_win, 1) - end - - carbon.explore_sidebar( - vim.tbl_extend('force', options or {}, { position = 'right' }) - ) -end - -function carbon.explore_float(options) - if options and options.bang or settings.always_reveal then - buffer.expand_to_path(vim.fn.expand('%')) - end - - local window_settings = settings.float_settings - - if type(window_settings) == 'function' then - window_settings = window_settings() - end - - window_settings = vim.deepcopy(window_settings) - - local carbon_fexplore_window = vim.api.nvim_get_current_win() - local window = vim.api.nvim_open_win(buffer.handle(), 1, window_settings) - - buffer.render() - - vim.api.nvim_win_set_option( - window, - 'winhl', - 'FloatBorder:Normal,Normal:Normal' - ) - - vim.w.carbon_fexplore_window = carbon_fexplore_window -end - -function carbon.up() - if buffer.up() then - util.cursor(1, 1) - buffer.render() - end -end - -function carbon.reset() - if buffer.reset() then - util.cursor(1, 1) - buffer.render() - end -end - -function carbon.down() - if buffer.down() then - util.cursor(1, 1) - buffer.render() - end -end - -function carbon.cd(path) - local destination = path and path.file or path or vim.v.event.cwd - - if buffer.cd(destination) then - util.cursor(1, 1) - buffer.render() - end -end - -function carbon.quit() - if #vim.api.nvim_list_wins() > 1 then - vim.api.nvim_win_close(0, 1) - elseif #vim.api.nvim_list_bufs() > 1 then - vim.cmd({ cmd = 'bprevious' }) - end -end - -function carbon.create() - buffer.create() -end - -function carbon.delete() - buffer.delete() -end - -function carbon.move() - buffer.move() -end - -function carbon.close_parent() - local count = 0 - local lines = { unpack(buffer.lines(), 2) } - local entry = buffer.cursor().line.entry - local line - - while count < vim.v.count1 do - line = util.tbl_find(lines, function(current) - return current.entry == entry.parent - or vim.tbl_contains(current.path, entry.parent) - end) - - if line then - count = count + 1 - entry = line.path[1] and line.path[1].parent or line.entry - - entry:set_open(false) - else - break - end - end - - line = util.tbl_find(lines, function(current) - return current.entry == entry or vim.tbl_contains(current.path, entry) - end) - - if line then - vim.fn.cursor(line.lnum, (line.depth + 1) * 2 + 1) - end - - buffer.render() -end - -return carbon diff --git a/lua/carbon/buffer.lua b/lua/carbon/buffer.lua deleted file mode 100644 index cab9d50..0000000 --- a/lua/carbon/buffer.lua +++ /dev/null @@ -1,737 +0,0 @@ -local util = require('carbon.util') -local entry = require('carbon.entry') -local watcher = require('carbon.watcher') -local settings = require('carbon.settings') -local constants = require('carbon.constants') -local buffer = {} -local internal = {} -local open_cwd = vim.loop.cwd() -local data = { root = entry.new(open_cwd), resync_paths = {}, handle = -1 } -local file_icons - -if settings.file_icons then - local ok, module = pcall(require, 'nvim-web-devicons') - - if ok then - file_icons = module - end -end - -function buffer.launch(target) - buffer.set_root(target) - buffer.show() - - open_cwd = data.root.path - - return data.root -end - -function buffer.is_loaded() - return vim.api.nvim_buf_is_loaded(data.handle) -end - -function buffer.is_hidden() - return not util.bufwinid(data.handle) -end - -function buffer.handle() - if buffer.is_loaded() then - return data.handle - end - - local mappings = - { { 'n', 'i', '' }, { 'n', 'o', '' }, { 'n', 'O', '' } } - - for action, mapping in pairs(settings.actions or {}) do - mapping = type(mapping) == 'string' and { mapping } or mapping or {} - - for _, key in ipairs(mapping) do - mappings[#mappings + 1] = - { 'n', key, util.plug(action), { nowait = true } } - end - end - - data.handle = util.create_scratch_buf({ - name = 'carbon', - filetype = 'carbon.explorer', - modifiable = false, - modified = false, - bufhidden = 'hide', - mappings = mappings, - autocmds = { - BufHidden = function() - buffer.process_hidden() - end, - BufWinEnter = function() - buffer.process_enter() - end, - }, - }) - - return data.handle -end - -function buffer.sidebar_window_id() - local existing_win - - for _, win in ipairs(vim.api.nvim_list_wins()) do - if pcall(vim.api.nvim_win_get_var, win, 'carbon_sidebar_window') then - if vim.api.nvim_win_is_valid(win) then - existing_win = win - end - - break - end - end - - return existing_win -end - -function buffer.sidebar_window_split() - local existing_win = buffer.sidebar_window_id() - - if existing_win then - return vim.api.nvim_win_get_var(existing_win, 'carbon_sidebar_split') - end -end - -function buffer.show() - vim.api.nvim_win_set_buf(0, buffer.handle()) - buffer.render() -end - -function buffer.render() - if not buffer.is_loaded() or buffer.is_hidden() then - return - end - - local cursor = nil - local lines = {} - local hls = {} - - for lnum, line_data in ipairs(buffer.lines()) do - lines[#lines + 1] = line_data.line - - if data.flash and data.flash.path == line_data.entry.path then - cursor = { lnum = lnum, col = 1 + (line_data.depth + 1) * 2 } - end - - for _, hl in ipairs(line_data.highlights) do - hls[#hls + 1] = { hl[1], lnum - 1, hl[2], hl[3] } - end - end - - buffer.clear_namespace(0, -1) - buffer.set_lines(0, -1, lines) - - for _, hl in ipairs(hls) do - buffer.add_highlight(unpack(hl)) - end - - if cursor then - util.cursor(cursor.lnum, cursor.col) - - if settings.flash then - vim.defer_fn(function() - buffer.focus_flash( - settings.flash.duration, - 'CarbonFlash', - { cursor.lnum - 1, cursor.col - 1 }, - { cursor.lnum - 1, -1 } - ) - end, settings.flash.delay) - end - end - - data.flash = nil -end - -function buffer.expand_to_path(input_path) - local path = vim.fn.fnamemodify(input_path, ':p') - - if vim.startswith(path, data.root.path) then - local dirs = vim.split(string.sub(path, #data.root.path + 2), '/') - local current = data.root - - for _, dir in ipairs(dirs) do - current:children() - - current = entry.find(string.format('%s/%s', current.path, dir)) - - if current then - current:set_open(true) - else - break - end - end - - if current and current.path == path then - data.flash = current - - return true - end - - return false - end -end - -function buffer.cursor(opts) - local options = opts or {} - local lines = buffer.lines() - local line = lines[vim.fn.line('.')] - local target = line.entry - local target_line - - if options.target_directory_only and not target.is_directory then - target = target.parent - end - - target = line.path[vim.v.count] or target - target_line = util.tbl_find(lines, function(current) - if current.entry.path == target.path then - return true - end - - return util.tbl_find(current.path, function(parent) - if parent.path == target.path then - return true - end - end) - end) - - return { line = line, target = target, target_line = target_line } -end - -function buffer.lines(input_target, lines, depth) - lines = lines or {} - depth = depth or 0 - local target = input_target or data.root - local expand_indicator = ' ' - local collapse_indicator = ' ' - - if type(settings.indicators) == 'table' then - expand_indicator = settings.indicators.expand or expand_indicator - collapse_indicator = settings.indicators.collapse or collapse_indicator - end - - if not input_target and #lines == 0 then - lines[#lines + 1] = { - lnum = 1, - depth = -1, - entry = data.root, - line = data.root.name .. '/', - highlights = { { 'CarbonDir', 0, -1 } }, - path = {}, - } - - watcher.register(data.root.path) - end - - for _, child in ipairs(target:children()) do - local tmp = child - local hls = {} - local path = {} - local lnum = 1 + #lines - local indent = string.rep(' ', depth) - local is_empty = true - local indicator = '' - local path_suffix = '' - - if settings.compress then - while - tmp.is_directory - and #tmp:children() == 1 - and tmp:is_compressible() - do - watcher.register(tmp.path) - - path[#path + 1] = tmp - tmp = tmp:children()[1] - end - end - - if tmp.is_directory then - watcher.register(tmp.path) - - is_empty = #tmp:children() == 0 - path_suffix = '/' - - if not is_empty and tmp:is_open() then - indicator = collapse_indicator - elseif not is_empty then - indicator = expand_indicator - end - end - - if is_empty then - indent = indent .. ' ' - end - - local icon = '' - local icon_highlight - - if file_icons and settings.file_icons and not tmp.is_directory then - local info = { - file_icons.get_icon( - tmp.name .. path_suffix, - vim.fn.fnamemodify(tmp.name, ':e'), - { default = true } - ), - } - - icon = info[1] or ' ' - icon_highlight = info[2] - end - - local link_group - local full_path = tmp.name .. path_suffix - local indent_end = #indent - local icon_width = #icon ~= 0 and #icon + 1 or 0 - local indicator_width = #indicator ~= 0 and #indicator + 1 or 0 - local path_start = indent_end + icon_width + indicator_width - local dir_path = table.concat( - vim.tbl_map(function(parent) - return parent.name - end, path), - '/' - ) - - if path[1] then - full_path = dir_path .. '/' .. full_path - end - - if tmp.is_symlink == 1 then - link_group = 'CarbonSymlink' - elseif tmp.is_symlink == 2 then - link_group = 'CarbonBrokenSymlink' - elseif tmp.is_executable then - link_group = 'CarbonExe' - end - - if indicator_width ~= 0 and not is_empty then - hls[#hls + 1] = - { 'CarbonIndicator', indent_end, indent_end + indicator_width } - end - - if icon and icon_highlight then - hls[#hls + 1] = - { icon_highlight, indent_end + indicator_width, path_start - 1 } - end - - if tmp.is_directory then - hls[#hls + 1] = { link_group or 'CarbonDir', path_start, -1 } - elseif path[1] then - local dir_end = path_start + #dir_path + 1 - - hls[#hls + 1] = { link_group or 'CarbonDir', path_start, dir_end } - hls[#hls + 1] = { link_group or 'CarbonFile', dir_end, -1 } - else - hls[#hls + 1] = { link_group or 'CarbonFile', path_start, -1 } - end - - local line_prefix = indent - - if indicator_width ~= 0 then - line_prefix = line_prefix .. indicator .. ' ' - end - - if icon_width ~= 0 then - line_prefix = line_prefix .. icon .. ' ' - end - - lines[#lines + 1] = { - lnum = lnum, - depth = depth, - entry = tmp, - line = line_prefix .. full_path, - icon_width = icon_width, - highlights = hls, - path = path, - } - - if tmp.is_directory and tmp:is_open() then - buffer.lines(tmp, lines, depth + 1) - end - end - - return lines -end - -function buffer.synchronize() - data.root:synchronize(data.resync_paths) - buffer.render() - - data.resync_paths = {} -end - -function buffer.up(count) - local rerender = false - local remaining = count or vim.v.count1 - - while remaining > 0 do - remaining = remaining - 1 - local new_root = entry.new(vim.fn.fnamemodify(data.root.path, ':h')) - - if new_root.path ~= data.root.path then - rerender = true - - new_root:set_children(vim.tbl_map(function(child) - if child.path == data.root.path then - child:set_open(true) - child:set_children(data.root:children()) - end - - return child - end, new_root:get_children())) - - buffer.set_root(new_root) - end - end - - return rerender -end - -function buffer.down(count) - local line = buffer.cursor().line - local new_root = line.path[count or vim.v.count1] or line.entry - - if not new_root.is_directory then - new_root = new_root.parent - end - - if new_root.path ~= data.root.path then - data.root:set_open(true) - buffer.set_root(new_root) - - return true - end -end - -function buffer.set_root(target) - if type(target) == 'string' then - target = entry.new(target) - end - - data.root = target - - watcher.keep(function(path) - return vim.startswith(path, data.root.path) - end) - - if settings.sync_pwd then - vim.api.nvim_set_current_dir(data.root.path) - end - - return data.root -end - -function buffer.reset() - local rerender = buffer.cd(open_cwd) - - if rerender and not settings.sync_pwd then - vim.api.nvim_set_current_dir(open_cwd) - end - - return rerender -end - -function buffer.cd(path) - local new_root = entry.new(path) - - if new_root.path == data.root.path then - return false - elseif vim.startswith(data.root.path, new_root.path) then - local new_depth = select(2, string.gsub(new_root.path, '/', '')) - local current_depth = select(2, string.gsub(data.root.path, '/', '')) - - if current_depth - new_depth > 0 then - buffer.up(current_depth - new_depth) - - return true - end - else - buffer.set_root(entry.find(new_root.path) or new_root) - - return true - end -end - -function buffer.delete() - local line = buffer.cursor().line - local targets = vim.list_extend({ unpack(line.path) }, { line.entry }) - - local lnum_idx = line.lnum - 1 - local count = vim.v.count == 0 and #targets or vim.v.count1 - local path_idx = math.min(count, #targets) - local target = targets[path_idx] - local highlight = - { 'CarbonFile', line.depth * 2 + 2 + (line.icon_width or 0), lnum_idx } - - if targets[path_idx].path == data.root.path then - return - end - - if target.is_directory then - highlight[1] = 'CarbonDir' - end - - for idx = 1, path_idx - 1 do - highlight[2] = highlight[2] + #line.path[idx].name + 1 - end - - buffer.clear_extmarks({ lnum_idx, highlight[2] }, { lnum_idx, -1 }, {}) - buffer.add_highlight('CarbonDanger', lnum_idx, highlight[2], -1) - util.confirm({ - row = line.lnum, - col = highlight[2], - highlight = 'CarbonDanger', - actions = { - { - label = 'delete', - shortcut = 'D', - callback = function() - local result = - vim.fn.delete(target.path, target.is_directory and 'rf' or '') - - if result == -1 then - vim.api.nvim_echo({ - { 'Failed to delete: ', 'CarbonDanger' }, - { vim.fn.fnamemodify(target.path, ':.'), 'CarbonIndicator' }, - }, false, {}) - else - buffer.defer_resync(nil, vim.fn.fnamemodify(target.path, ':h')) - end - end, - }, - { - label = 'cancel', - shortcut = 'q', - callback = function() - buffer.clear_extmarks({ lnum_idx, 0 }, { lnum_idx, -1 }, {}) - - for _, lhl in ipairs(line.highlights) do - buffer.add_highlight(lhl[1], lnum_idx, lhl[2], lhl[3]) - end - - buffer.render() - end, - }, - }, - }) -end - -function buffer.move() - local ctx = buffer.cursor() - local target_line = ctx.target_line - local targets = vim.list_extend( - { unpack(target_line.path) }, - { target_line.entry } - ) - local target_names = vim.tbl_map(function(part) - return part.name - end, targets) - - if ctx.target.path == data.root.path then - return - end - - local path_start = target_line.depth * 2 + 2 + target_line.icon_width - local lnum_idx = target_line.lnum - 1 - local target_idx = util.tbl_key(targets, ctx.target) - local clamped_names = { unpack(target_names, 1, target_idx - 1) } - local start_hl = path_start + #table.concat(clamped_names, '/') - - if target_idx > 1 then - start_hl = start_hl + 1 - end - - buffer.clear_extmarks({ lnum_idx, start_hl }, { lnum_idx, -1 }, {}) - buffer.add_highlight('CarbonPending', lnum_idx, start_hl, -1) - - vim.cmd({ cmd = 'redraw', bang = true }) - vim.cmd({ cmd = 'echohl', args = { 'CarbonPending' } }) - - local updated_path = string.gsub( - vim.fn.input({ - prompt = 'destination: ', - default = ctx.target.path, - cancelreturn = ctx.target.path, - }), - '/+$', - '' - ) - - vim.cmd({ cmd = 'echohl', args = { 'None' } }) - vim.api.nvim_echo({ { ' ' } }, false, {}) - - if updated_path == ctx.target.path then - buffer.render() - elseif vim.loop.fs_stat(updated_path) then - buffer.render() - vim.api.nvim_echo({ - { 'Failed to move: ', 'CarbonDanger' }, - { vim.fn.fnamemodify(ctx.target.path, ':.'), 'CarbonIndicator' }, - { ' => ' }, - { vim.fn.fnamemodify(updated_path, ':.'), 'CarbonIndicator' }, - { ' (destination exists)', 'CarbonPending' }, - }, false, {}) - else - local directory = vim.fn.fnamemodify(updated_path, ':h') - local tmp_path = ctx.target.path - - if vim.startswith(updated_path, tmp_path) then - tmp_path = vim.fn.tempname() - - vim.fn.rename(ctx.target.path, tmp_path) - end - - vim.fn.mkdir(directory, 'p') - vim.fn.rename(tmp_path, updated_path) - buffer.defer_resync(nil, vim.fn.fnamemodify(ctx.target.path, ':h')) - end -end - -function buffer.create() - local ctx = buffer.cursor({ target_directory_only = true }) - - ctx.compact = ctx.target.is_directory and #ctx.target:children() == 0 - ctx.prev_open = ctx.target:is_open() - ctx.prev_compressible = ctx.target:is_compressible() - - ctx.target:set_open(true) - ctx.target:set_compressible(false) - - if ctx.compact then - ctx.edit_prefix = ctx.line.line - ctx.edit_lnum = ctx.line.lnum - 1 - ctx.edit_col = #ctx.edit_prefix + 1 - ctx.init_end_lnum = ctx.edit_lnum + 1 - else - ctx.edit_prefix = string.rep(' ', ctx.target_line.depth + 2) - ctx.edit_lnum = ctx.target_line.lnum + #buffer.lines(ctx.target) - ctx.edit_col = #ctx.edit_prefix - ctx.init_end_lnum = ctx.edit_lnum - end - - buffer.render() - buffer.set_lines(ctx.edit_lnum, ctx.init_end_lnum, { ctx.edit_prefix }) - util.autocmd('CursorMovedI', internal.create_insert_move(ctx), { buffer = 0 }) - vim.keymap.set('i', '', internal.create_confirm(ctx), { buffer = 0 }) - vim.keymap.set('i', '', internal.create_cancel(ctx), { buffer = 0 }) - util.cursor(ctx.edit_lnum + 1, ctx.edit_col) - vim.api.nvim_buf_set_option(data.handle, 'modifiable', true) - vim.cmd({ cmd = 'startinsert', bang = true }) -end - -function buffer.clear_extmarks(...) - local extmarks = vim.api.nvim_buf_get_extmarks(data.handle, constants.hl, ...) - - for _, extmark in ipairs(extmarks) do - vim.api.nvim_buf_del_extmark(data.handle, constants.hl, extmark[1]) - end -end - -function buffer.clear_namespace(...) - vim.api.nvim_buf_clear_namespace(data.handle, constants.hl, ...) -end - -function buffer.add_highlight(...) - vim.api.nvim_buf_add_highlight(data.handle, constants.hl, ...) -end - -function buffer.set_lines(start_lnum, end_lnum, lines) - local current_mode = string.lower(vim.api.nvim_get_mode().mode) - - vim.api.nvim_buf_set_option(data.handle, 'modifiable', true) - vim.api.nvim_buf_set_lines(data.handle, start_lnum, end_lnum, 1, lines) - vim.api.nvim_buf_set_option(data.handle, 'modified', false) - - if not string.find(current_mode, 'i') then - vim.api.nvim_buf_set_option(data.handle, 'modifiable', false) - end -end - -function buffer.defer_resync(_, path) - if data.resync_timer then - data.resync_timer:stop() - end - - data.resync_paths[path] = true - data.resync_timer = vim.defer_fn(buffer.synchronize, settings.sync_delay) -end - -function buffer.process_enter() - vim.opt_local.wrap = false - vim.opt_local.spell = false - vim.opt_local.fillchars = { eob = ' ' } -end - -function buffer.process_hidden() - vim.opt_local.wrap = vim.opt_global.wrap:get() - vim.opt_local.spell = vim.opt_global.spell:get() - vim.opt_local.fillchars = vim.opt_global.fillchars:get() - vim.w.carbon_sidebar_split = nil - vim.w.carbon_sidebar_window = nil - vim.w.carbon_fexplore_window = nil -end - -function internal.create_confirm(ctx) - return function() - local text = vim.trim(string.sub(vim.fn.getline('.'), ctx.edit_col)) - local name = vim.fn.fnamemodify(text, ':t') - local parent_directory = ctx.target.path - .. '/' - .. vim.trim(vim.fn.fnamemodify(text, ':h')) - - vim.fn.mkdir(parent_directory, 'p') - - if name ~= '' then - vim.fn.writefile({}, parent_directory .. '/' .. name) - end - - internal.create_leave(ctx) - buffer.defer_resync(nil, vim.fn.fnamemodify(parent_directory, ':h')) - end -end - -function internal.create_cancel(ctx) - return function() - ctx.target:set_open(ctx.prev_open) - internal.create_leave(ctx) - buffer.render() - end -end - -function internal.create_leave(ctx) - vim.cmd({ cmd = 'stopinsert' }) - ctx.target:set_compressible(ctx.prev_compressible) - util.cursor(ctx.target_line.lnum, 1) - vim.keymap.del('i', '', { buffer = 0 }) - vim.keymap.del('i', '', { buffer = 0 }) - util.clear_autocmd('CursorMovedI', { buffer = 0 }) -end - -function internal.create_insert_move(ctx) - return function() - local text = ctx.edit_prefix - .. vim.trim(string.sub(vim.fn.getline('.'), ctx.edit_col)) - local last_slash_col = vim.fn.strridx(text, '/') + 1 - - buffer.set_lines(ctx.edit_lnum, ctx.edit_lnum + 1, { text }) - buffer.clear_extmarks({ ctx.edit_lnum, 0 }, { ctx.edit_lnum, -1 }, {}) - buffer.add_highlight('CarbonDir', ctx.edit_lnum, 0, last_slash_col) - buffer.add_highlight('CarbonFile', ctx.edit_lnum, last_slash_col, -1) - util.cursor(ctx.edit_lnum + 1, math.max(ctx.edit_col, vim.fn.col('.'))) - end -end - -function buffer.focus_flash(duration, group, start, finish) - vim.highlight.range(data.handle, constants.hl_tmp, group, start, finish, {}) - vim.defer_fn(function() - if buffer.is_loaded() then - vim.api.nvim_buf_clear_namespace(data.handle, constants.hl_tmp, 0, -1) - end - end, duration) -end - -return buffer diff --git a/lua/carbon/constants.lua b/lua/carbon/constants.lua index 5aaf6bf..be25df8 100644 --- a/lua/carbon/constants.lua +++ b/lua/carbon/constants.lua @@ -2,4 +2,5 @@ return { hl = vim.api.nvim_create_namespace('carbon'), hl_tmp = vim.api.nvim_create_namespace('carbon:tmp'), augroup = vim.api.nvim_create_augroup('carbon', { clear = false }), + directions = { left = 'h', right = 'l', up = 'k', down = 'j' }, } diff --git a/lua/carbon/entry.lua b/lua/carbon/entry.lua index 06386d0..b7a6b10 100644 --- a/lua/carbon/entry.lua +++ b/lua/carbon/entry.lua @@ -1,8 +1,8 @@ local util = require('carbon.util') local watcher = require('carbon.watcher') local entry = {} -local data = { children = {}, open = {}, compressible = {} } +entry.items = {} entry.__index = entry entry.__lt = function(a, b) if a.is_directory and b.is_directory then @@ -17,14 +17,15 @@ entry.__lt = function(a, b) end function entry.new(path, parent) - local clean = string.gsub(path, '/+$', '') - local lstat = select(2, pcall(vim.loop.fs_lstat, clean)) or {} + local raw_path = path == '' and '/' or path + local clean = string.gsub(raw_path, '/+$', '') + local lstat = select(2, pcall(vim.loop.fs_lstat, raw_path)) or {} local is_executable = lstat.mode == 33261 local is_directory = lstat.type == 'directory' local is_symlink = lstat.type == 'link' and 1 if is_symlink then - local stat = select(2, pcall(vim.loop.fs_stat, clean)) + local stat = select(2, pcall(vim.loop.fs_stat, raw_path)) if stat then is_executable = lstat.mode == 33261 @@ -36,6 +37,7 @@ function entry.new(path, parent) end return setmetatable({ + raw_path = raw_path, path = clean, name = vim.fn.fnamemodify(clean, ':t'), parent = parent, @@ -46,7 +48,7 @@ function entry.new(path, parent) end function entry.find(path) - for _, children in pairs(data.children) do + for _, children in pairs(entry.items) do for _, child in ipairs(children) do if child.path == path then return child @@ -63,10 +65,12 @@ function entry:synchronize(paths) paths = paths or {} if paths[self.path] then + paths[self.path] = nil + local all_paths = {} local current_paths = {} local previous_paths = {} - local previous_children = data.children[self.path] or {} + local previous_children = entry.items[self.path] or {} self:set_children(nil) @@ -86,7 +90,6 @@ function entry:synchronize(paths) if previous and current then if current.is_directory then - current:set_open(previous:is_open()) current:synchronize(paths) end elseif previous then @@ -116,54 +119,29 @@ function entry:terminate() if self.parent and self.parent:has_children() then self.parent:set_children(vim.tbl_filter(function(sibling) return sibling.path ~= self.path - end, data.children[self.parent.path])) - end -end - -function entry:set_compressible(value) - data.compressible[self.path] = value -end - -function entry:is_compressible() - return data.compressible[self.path] == nil and true - or data.compressible[self.path] -end - -function entry:set_open(value, recursive) - if self.is_directory then - data.open[self.path] = value - - if recursive and self:has_children() then - for _, child in ipairs(self:children()) do - child:set_open(value, recursive) - end - end + end, entry.items[self.parent.path])) end end -function entry:is_open() - return data.open[self.path] and true or false -end - function entry:children() if self.is_directory and not self:has_children() then self:set_children(self:get_children()) end - return data.children[self.path] or {} + return entry.items[self.path] or {} end function entry:has_children() - return data.children[self.path] and true or false + return entry.items[self.path] and true or false end function entry:set_children(children) - data.children[self.path] = children + entry.items[self.path] = children end function entry:get_children() local entries = {} - local handle = vim.loop.fs_scandir(self.path) + local handle = vim.loop.fs_scandir(self.raw_path) if type(handle) == 'userdata' then local function iterator() @@ -185,4 +163,18 @@ function entry:get_children() return entries end +function entry:highlight_group() + if self.is_symlink == 1 then + return 'CarbonSymlink' + elseif self.is_symlink == 2 then + return 'CarbonBrokenSymlink' + elseif self.is_directory then + return 'CarbonDir' + elseif self.is_executable then + return 'CarbonExe' + else + return 'CarbonFile' + end +end + return entry diff --git a/lua/carbon/health.lua b/lua/carbon/health.lua new file mode 100644 index 0000000..8977d5a --- /dev/null +++ b/lua/carbon/health.lua @@ -0,0 +1,79 @@ +local util = require('carbon.util') +local view = require('carbon.view') +local watcher = require('carbon.watcher') +local health = {} + +local function sort_names(a, b) + return string.lower(a) < string.lower(b) +end + +local function sort_paths(a, b) + local a_is_directory = util.is_directory(a) + local b_is_directory = util.is_directory(b) + + if a_is_directory and b_is_directory then + return sort_names(a, b) + elseif a_is_directory then + return true + elseif b_is_directory then + return false + end + + return sort_names(a, b) +end + +function health.check() + health.report_views() + health.report_listeners() + health.report_events() +end + +function health.report_views() + vim.health.report_start('view::active') + + local view_roots = vim.tbl_map(function(item) + return item.root + end, view.items) + + table.sort(view_roots) + + for _, root in ipairs(view_roots) do + vim.health.report_info(root.path) + end +end + +function health.report_events() + vim.health.report_start('watcher::events') + + local names = vim.tbl_keys(watcher.events) + + table.sort(names, sort_names) + + for _, name in ipairs(names) do + local callback_count = #vim.tbl_keys(watcher.events[name] or {}) + local reporter = callback_count == 0 and 'report_warn' or 'report_info' + + vim.health[reporter]( + string.format( + '%d %s attached to %s', + callback_count, + callback_count == 1 and 'handler' or 'handlers', + name + ) + ) + end +end + +function health.report_listeners() + vim.health.report_start('watcher::listeners') + + local paths = vim.tbl_keys(watcher.listeners) + + table.sort(paths, sort_paths) + + for _, path in ipairs(paths) do + vim.health.report_info(path) + end +end + +return health diff --git a/lua/carbon/init.lua b/lua/carbon/init.lua new file mode 100644 index 0000000..04f0bc9 --- /dev/null +++ b/lua/carbon/init.lua @@ -0,0 +1,374 @@ +local util = require('carbon.util') +local watcher = require('carbon.watcher') +local settings = require('carbon.settings') +local view = require('carbon.view') +local carbon = {} + +function carbon.setup(user_settings) + if type(user_settings) ~= 'table' then + user_settings = {} + end + + if not vim.g.carbon_initialized then + if type(user_settings) == 'function' then + user_settings(settings) + elseif type(user_settings) == 'table' then + local next = vim.tbl_deep_extend('force', settings, user_settings) + + for setting, value in pairs(next) do + settings[setting] = value + end + end + + if type(user_settings.highlights) == 'table' then + settings.highlights = + vim.tbl_extend('force', settings.highlights, user_settings.highlights) + end + + local argv = vim.fn.argv() + local open = argv[1] and vim.fn.fnamemodify(argv[1], ':p') or vim.loop.cwd() + local command_opts = { bang = true, nargs = '?', complete = 'dir' } + + watcher.on('carbon:synchronize', function(_, path) + view.resync(path) + end) + + util.command('Carbon', carbon.explore, command_opts) + util.command('Rcarbon', carbon.explore_right, command_opts) + util.command('Lcarbon', carbon.explore_left, command_opts) + util.command('Fcarbon', carbon.explore_float, command_opts) + util.command('ToggleSidebarCarbon', carbon.toggle_sidebar, command_opts) + + util.autocmd('SessionLoadPost', carbon.session_load_post, { pattern = '*' }) + util.autocmd('WinResized', carbon.win_resized, { pattern = '*' }) + + if settings.open_on_dir then + util.autocmd('BufWinEnter', carbon.explore_buf_dir, { pattern = '*' }) + end + + if settings.sync_on_cd then + util.autocmd('DirChanged', carbon.cd, { pattern = 'global' }) + end + + if not settings.keep_netrw then + vim.g.loaded_netrw = 1 + vim.g.loaded_netrwPlugin = 1 + + pcall(vim.api.nvim_del_augroup_by_name, 'FileExplorer') + pcall(vim.api.nvim_del_augroup_by_name, 'Network') + + util.command('Explore', carbon.explore, command_opts) + util.command('Rexplore', carbon.explore_right, command_opts) + util.command('Lexplore', carbon.explore_left, command_opts) + util.command('ToggleSidebarExplore', carbon.toggle_sidebar, command_opts) + end + + for action in pairs(settings.defaults.actions) do + vim.keymap.set('', util.plug(action), carbon[action]) + end + + if type(settings.highlights) == 'table' then + for group, properties in pairs(settings.highlights) do + util.highlight(group, properties) + end + end + + if + vim.fn.has('vim_starting') + and settings.auto_open + and util.is_directory(open) + then + view.activate({ path = open }) + end + + vim.g.carbon_initialized = true + end +end + +function carbon.win_resized() + if vim.api.nvim_win_is_valid(view.sidebar.origin) then + local window_width = vim.api.nvim_win_get_width(view.sidebar.origin) + + if window_width ~= settings.sidebar_width then + vim.api.nvim_win_set_width(view.sidebar.origin, settings.sidebar_width) + end + end +end + +function carbon.session_load_post(event) + if util.is_directory(event.file) then + local window_id = util.bufwinid(event.buf) + local window_width = vim.api.nvim_win_get_width(window_id) + local is_sidebar = window_width == settings.sidebar_width + + view.activate({ path = event.file }) + view.execute(function(ctx) + ctx.view:show() + end) + + if is_sidebar then + local neighbor = util.tbl_find( + util.window_neighbors(window_id, { 'left', 'right' }), + function(neighbor) + return neighbor.target + end + ) + + if neighbor then + view.sidebar = neighbor + end + end + end +end + +function carbon.toggle_recursive() + view.execute(function(ctx) + if ctx.cursor.line.entry.is_directory then + local function toggle_recursive(target, value) + if target.is_directory then + ctx.view:set_path_attr(target.path, 'open', value) + + if target:has_children() then + for _, child in ipairs(target:children()) do + toggle_recursive(child, value) + end + end + end + end + + toggle_recursive( + ctx.cursor.line.entry, + not ctx.view:get_path_attr(ctx.cursor.line.entry.path, 'open') + ) + + ctx.view:update() + ctx.view:render() + end + end) +end + +function carbon.edit() + view.execute(function(ctx) + if ctx.cursor.line.entry.is_directory then + local open = ctx.view:get_path_attr(ctx.cursor.line.entry.path, 'open') + + ctx.view:set_path_attr(ctx.cursor.line.entry.path, 'open', not open) + ctx.view:update() + ctx.view:render() + else + view.handle_sidebar_or_float() + vim.cmd.edit({ + ctx.cursor.line.entry.path, + mods = { keepalt = #vim.fn.getreg('#') ~= 0 }, + }) + end + end) +end + +function carbon.split() + view.execute(function(ctx) + if not ctx.cursor.line.entry.is_directory then + if vim.w.carbon_fexplore_window then + vim.api.nvim_win_close(0, 1) + end + + vim.cmd.split(ctx.cursor.line.entry.path) + end + end) +end + +function carbon.vsplit() + view.execute(function(ctx) + if not ctx.cursor.line.entry.is_directory then + if vim.w.carbon_fexplore_window then + vim.api.nvim_win_close(0, 1) + end + + vim.cmd.vsplit(ctx.cursor.line.entry.path) + end + end) +end + +function carbon.up() + view.execute(function(ctx) + if ctx.view:up() then + ctx.view:update() + ctx.view:render() + util.cursor(1, 1) + end + end) +end + +function carbon.reset() + view.execute(function(ctx) + if ctx.view:reset() then + ctx.view:update() + ctx.view:render() + util.cursor(1, 1) + end + end) +end + +function carbon.down() + view.execute(function(ctx) + if ctx.view:down() then + ctx.view:update() + ctx.view:render() + util.cursor(1, 1) + end + end) +end + +function carbon.cd(path) + view.execute(function(ctx) + local destination = path and path.file or path or vim.v.event.cwd + + if ctx.view:cd(destination) then + ctx.view:update() + ctx.view:render() + util.cursor(1, 1) + end + end) +end + +function carbon.explore(options_param) + local options = options_param or {} + local path = + util.explore_path(options.fargs and options.fargs[1] or '', view.current()) + + view.activate({ path = path, reveal = options.bang }) +end + +function carbon.toggle_sidebar(options) + local current_win = vim.api.nvim_get_current_win() + + if vim.api.nvim_win_is_valid(view.sidebar.origin) then + vim.api.nvim_win_close(view.sidebar.origin, 1) + else + local explore_options = vim.tbl_extend( + 'force', + options or {}, + { sidebar = settings.sidebar_position } + ) + + carbon.explore_sidebar(explore_options) + + if not settings.sidebar_toggle_focus then + vim.api.nvim_set_current_win(current_win) + end + end +end + +function carbon.explore_sidebar(options_param) + local options = options_param or {} + local sidebar = options.sidebar or settings.sidebar_position + local path = + util.explore_path(options.fargs and options.fargs[1] or '', view.current()) + + view.activate({ path = path, reveal = options.bang, sidebar = sidebar }) +end + +function carbon.explore_left(options_param) + if view.sidebar.position ~= 'left' then + view.close_sidebar() + end + + carbon.explore_sidebar( + vim.tbl_extend('force', options_param or {}, { sidebar = 'left' }) + ) +end + +function carbon.explore_right(options_param) + if view.sidebar.position ~= 'right' then + view.close_sidebar() + end + + carbon.explore_sidebar( + vim.tbl_extend('force', options_param or {}, { sidebar = 'right' }) + ) +end + +function carbon.explore_float(options_param) + local options = options_param or {} + local path = + util.explore_path(options.fargs and options.fargs[1] or '', view.current()) + + view.activate({ path = path, reveal = options.bang, float = true }) +end + +function carbon.explore_buf_dir(params) + if vim.bo.filetype == 'carbon.explorer' then + return + end + + if params and params.file and util.is_directory(params.file) then + view.activate({ path = params.file }) + view.execute(function(ctx) + ctx.view:show() + end) + end +end + +function carbon.quit() + if #vim.api.nvim_list_wins() > 1 then + vim.api.nvim_win_close(0, 1) + elseif #vim.api.nvim_list_bufs() > 1 then + pcall(vim.cmd.bprevious) + end +end + +function carbon.create() + view.execute(function(ctx) + ctx.view:create() + end) +end + +function carbon.delete() + view.execute(function(ctx) + ctx.view:delete() + end) +end + +function carbon.move() + view.execute(function(ctx) + ctx.view:move() + end) +end + +function carbon.close_parent() + view.execute(function(ctx) + local count = 0 + local lines = { unpack(ctx.view:current_lines(), 2) } + local entry = ctx.cursor.line.entry + local line + + while count < vim.v.count1 do + line = util.tbl_find(lines, function(current) + return current.entry == entry.parent + or vim.tbl_contains(current.path, entry.parent) + end) + + if line then + count = count + 1 + entry = line.path[1] and line.path[1].parent or line.entry + + ctx.view:set_path_attr(entry.path, 'open', false) + else + break + end + end + + line = util.tbl_find(lines, function(current) + return current.entry == entry or vim.tbl_contains(current.path, entry) + end) + + if line then + vim.fn.cursor(line.lnum, (line.depth + 1) * 2 + 1) + end + + ctx.view:update() + ctx.view:render() + end) +end + +return carbon diff --git a/lua/carbon/settings.lua b/lua/carbon/settings.lua index bb710a9..10d37ac 100644 --- a/lua/carbon/settings.lua +++ b/lua/carbon/settings.lua @@ -6,10 +6,11 @@ local defaults = { file_icons = pcall(require, 'nvim-web-devicons'), sync_on_cd = not vim.opt.autochdir:get(), sync_delay = 20, + open_on_dir = true, + auto_reveal = false, sidebar_width = 30, sidebar_toggle_focus = true, sidebar_position = 'left', - always_reveal = false, exclude = { '~$', '#$', @@ -36,13 +37,13 @@ local defaults = { float_settings = function() local columns = vim.opt.columns:get() local rows = vim.opt.lines:get() - local width = math.min(50, math.floor(columns * 0.8)) - local height = math.min(20, math.floor(rows * 0.8)) + local width = math.min(40, math.floor(columns * 0.9)) + local height = math.min(20, math.floor(rows * 0.9)) return { relative = 'editor', style = 'minimal', - border = 'single', + border = 'rounded', width = width, height = height, col = math.floor(columns / 2 - width / 2), @@ -66,10 +67,11 @@ local defaults = { highlights = { CarbonDir = { link = 'Directory' }, CarbonFile = { link = 'Text' }, - CarbonExe = { link = 'NetrwExe' }, - CarbonSymlink = { link = 'NetrwSymLink' }, - CarbonBrokenSymlink = { link = 'ErrorMsg' }, + CarbonExe = { link = '@function.builtin' }, + CarbonSymlink = { link = '@include' }, + CarbonBrokenSymlink = { link = 'DiagnosticError' }, CarbonIndicator = { fg = 'Gray', ctermfg = 'DarkGray', bold = true }, + CarbonFloat = { bg = '#111111', ctermbg = 'black' }, CarbonDanger = { link = 'Error' }, CarbonPending = { link = 'Search' }, CarbonFlash = { link = 'Visual' }, diff --git a/lua/carbon/util.lua b/lua/carbon/util.lua index 1fc67c5..a04ac38 100644 --- a/lua/carbon/util.lua +++ b/lua/carbon/util.lua @@ -2,6 +2,30 @@ local constants = require('carbon.constants') local settings = require('carbon.settings') local util = {} +function util.explore_path(path, current_view) + path = string.gsub(path, '%s', '') + + if path == '' then + path = vim.loop.cwd() + end + + if not vim.startswith(path, '/') then + local base_path = current_view and current_view.root.path or vim.loop.cwd() + + path = string.format('%s/%s', base_path, path) + end + + return string.gsub(vim.fn.simplify(path), '/+$', '') +end + +function util.resolve(path) + return string.gsub( + vim.fn.fnamemodify(vim.fs.normalize(path), ':p'), + '/+$', + '' + ) +end + function util.is_excluded(path) if settings.exclude then for _, pattern in ipairs(settings.exclude) do @@ -34,6 +58,16 @@ function util.tbl_key(tbl, item) end end +function util.tbl_some(tbl, callback) + for key, value in pairs(tbl) do + if callback(value, key) then + return true + end + end + + return false +end + function util.tbl_find(tbl, callback) for key, value in pairs(tbl) do if callback(value, key) then @@ -100,7 +134,7 @@ function util.confirm(options) callback() end - vim.cmd({ cmd = 'close' }) + vim.cmd.close() end if not immediate then @@ -153,7 +187,7 @@ function util.confirm(options) }) local win = vim.api.nvim_open_win(buf, true, { - relative = 'editor', + relative = 'win', anchor = 'NW', border = 'single', style = 'minimal', @@ -182,9 +216,16 @@ function util.bufwinid(buf) end end +function util.find_buf_by_name(name) + return util.tbl_find(vim.api.nvim_list_bufs(), function(bufnr) + return name == vim.api.nvim_buf_get_name(bufnr) + end) +end + function util.create_scratch_buf(options) options = options or {} - local buf = vim.api.nvim_create_buf(false, true) + local found = util.find_buf_by_name(options.name) + local buf = found or vim.api.nvim_create_buf(false, true) local buffer_options = vim.tbl_extend('force', { bufhidden = 'wipe', buftype = 'nofile', @@ -192,7 +233,7 @@ function util.create_scratch_buf(options) }, util.tbl_except(options, { 'name', 'lines', 'mappings', 'autocmds' })) if options.name then - vim.api.nvim_buf_set_name(buf, options.name) + vim.api.nvim_buf_set_name(buf, options.name == '' and '/' or options.name) end if options.lines then @@ -242,4 +283,41 @@ function util.set_winhl(win, highlights) vim.api.nvim_win_set_option(win, 'winhl', table.concat(winhls, ',')) end +function util.clear_extmarks(buf, ...) + local extmarks = vim.api.nvim_buf_get_extmarks(buf, constants.hl, ...) + + for _, extmark in ipairs(extmarks) do + vim.api.nvim_buf_del_extmark(buf, constants.hl, extmark[1]) + end +end + +function util.add_highlight(buf, ...) + vim.api.nvim_buf_add_highlight(buf, constants.hl, ...) +end + +function util.window_neighbors(window_id, sides) + local original_window = vim.api.nvim_get_current_win() + local result = {} + + for _, side in ipairs(sides or {}) do + vim.api.nvim_set_current_win(window_id) + vim.cmd.wincmd(constants.directions[side]) + + local side_id = vim.api.nvim_get_current_win() + local result_id = window_id ~= side_id and side_id or nil + + if result_id then + result[#result + 1] = { + origin = window_id, + position = side, + target = result_id, + } + end + end + + vim.api.nvim_set_current_win(original_window) + + return result +end + return util diff --git a/lua/carbon/view.lua b/lua/carbon/view.lua new file mode 100644 index 0000000..cb30582 --- /dev/null +++ b/lua/carbon/view.lua @@ -0,0 +1,945 @@ +local util = require('carbon.util') +local entry = require('carbon.entry') +local watcher = require('carbon.watcher') +local settings = require('carbon.settings') +local constants = require('carbon.constants') +local view = {} + +view.__index = view +view.sidebar = { origin = -1, target = -1 } +view.float = { origin = -1, target = -1 } +view.items = {} +view.resync_paths = {} + +local function create_leave(ctx) + vim.cmd.stopinsert() + ctx.view:set_path_attr(ctx.target.path, 'compressible', ctx.prev_compressible) + util.cursor(ctx.target_line.lnum, 1) + vim.keymap.del('i', '', { buffer = 0 }) + vim.keymap.del('i', '', { buffer = 0 }) + util.clear_autocmd('CursorMovedI', { buffer = 0 }) + ctx.view:update() + ctx.view:render() +end + +local function create_confirm(ctx) + return function() + local text = vim.trim(string.sub(vim.fn.getline('.'), ctx.edit_col)) + local name = vim.fn.fnamemodify(text, ':t') + local parent_directory = ctx.target.path + .. '/' + .. vim.trim(vim.fn.fnamemodify(text, ':h')) + + vim.fn.mkdir(parent_directory, 'p') + + if name ~= '' then + vim.fn.writefile({}, parent_directory .. '/' .. name) + end + + create_leave(ctx) + view.resync(vim.fn.fnamemodify(parent_directory, ':h')) + end +end + +local function create_cancel(ctx) + return function() + ctx.view:set_path_attr(ctx.target.path, 'open', ctx.prev_open) + create_leave(ctx) + end +end + +local function create_insert_move(ctx) + return function() + local text = ctx.edit_prefix + .. vim.trim(string.sub(vim.fn.getline('.'), ctx.edit_col)) + local last_slash_col = vim.fn.strridx(text, '/') + 1 + + vim.api.nvim_buf_set_lines(0, ctx.edit_lnum, ctx.edit_lnum + 1, 1, { text }) + util.clear_extmarks(0, { ctx.edit_lnum, 0 }, { ctx.edit_lnum, -1 }, {}) + util.add_highlight(0, 'CarbonDir', ctx.edit_lnum, 0, last_slash_col) + util.add_highlight(0, 'CarbonFile', ctx.edit_lnum, last_slash_col, -1) + util.cursor(ctx.edit_lnum + 1, math.max(ctx.edit_col, vim.fn.col('.'))) + end +end + +function view.file_icons() + if settings.file_icons then + local ok, module = pcall(require, 'nvim-web-devicons') + + if ok then + return module + end + end +end + +function view.find(path) + local resolved = util.resolve(path) + + return util.tbl_find(view.items, function(target_view) + return target_view.root.path == resolved + end) +end + +function view.get(path) + local found_view = view.find(path) + + if found_view then + return found_view + end + + local index = #view.items + 1 + local resolved = util.resolve(path) + local instance = setmetatable({ + index = index, + initial = resolved, + states = {}, + root = entry.new(resolved), + }, view) + + view.items[index] = instance + + return instance +end + +function view.activate(options_param) + local options = options_param or {} + local original_window = vim.api.nvim_get_current_win() + local current_view = (options.path and view.get(options.path)) + or view.current() + or view.get(vim.loop.cwd()) + + if options.reveal or settings.auto_reveal then + current_view:expand_to_path(vim.fn.expand('%')) + end + + if options.sidebar then + if vim.api.nvim_win_is_valid(view.sidebar.origin) then + vim.api.nvim_set_current_win(view.sidebar.origin) + else + local split = options.sidebar == 'right' and 'botright' or 'topleft' + local target_side = options.sidebar == 'right' and 'left' or 'right' + + vim.cmd.split({ mods = { vertical = true, split = split } }) + + local origin_id = vim.api.nvim_get_current_win() + local neighbor = util.window_neighbors(origin_id, { target_side })[1] + local target = neighbor and neighbor.target or original_window + + view.sidebar = { + position = options.sidebar, + origin = origin_id, + target = target, + } + end + + vim.api.nvim_win_set_width(view.sidebar.origin, settings.sidebar_width) + vim.api.nvim_win_set_buf(view.sidebar.origin, current_view:buffer()) + elseif options.float then + local float_settings = settings.float_settings + or settings.defaults.float_settings + + float_settings = type(float_settings) == 'function' and float_settings() + or vim.deepcopy(float_settings) + + view.float = { + origin = vim.api.nvim_open_win(current_view:buffer(), 1, float_settings), + target = original_window, + } + + vim.api.nvim_win_set_option( + view.float.origin, + 'winhl', + 'FloatBorder:CarbonFloat,Normal:CarbonFloat' + ) + else + vim.api.nvim_win_set_buf(0, current_view:buffer()) + end +end + +function view.close_sidebar() + if vim.api.nvim_win_is_valid(view.sidebar.origin) then + vim.api.nvim_win_close(view.sidebar.origin, true) + end + + view.sidebar = { origin = -1, target = -1 } +end + +function view.close_float() + if vim.api.nvim_win_is_valid(view.float.origin) then + vim.api.nvim_win_close(view.float.origin, true) + end + + view.float = { origin = -1, target = -1 } +end + +function view.handle_sidebar_or_float() + local current_window = vim.api.nvim_get_current_win() + + if current_window == view.sidebar.origin then + if vim.api.nvim_win_is_valid(view.sidebar.target) then + vim.api.nvim_set_current_win(view.sidebar.target) + else + local split = view.sidebar.position == 'right' and 'topleft' or 'botright' + local target_side = view.sidebar.position == 'right' and 'left' or 'right' + local neighbor = + util.window_neighbors(view.sidebar.origin, { target_side })[1] + + if neighbor then + view.sidebar.target = neighbor.target + vim.api.nvim_set_current_win(neighbor.target) + else + vim.cmd.split({ mods = { vertical = true, split = split } }) + + view.sidebar.target = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_width(view.sidebar.origin, settings.sidebar_width) + end + end + elseif current_window == view.float.origin then + view.close_float() + end +end + +function view.current() + local bufnr = vim.api.nvim_get_current_buf() + local ref = select(2, pcall(vim.api.nvim_buf_get_var, bufnr, 'carbon')) + + return ref and view.items[ref.index] or false +end + +function view.execute(callback) + local current_view = view.current() + + if current_view then + return callback({ cursor = current_view:cursor(), view = current_view }) + end +end + +function view.resync(path) + view.resync_paths[path] = true + + if view.resync_timer and not view.resync_timer:is_closing() then + view.resync_timer:close() + end + + view.resync_timer = vim.defer_fn(function() + for _, current_view in ipairs(view.items) do + current_view.root:synchronize(view.resync_paths) + current_view:update() + current_view:render() + end + + if not view.resync_timer:is_closing() then + view.resync_timer:close() + end + + view.resync_timer = nil + view.resync_paths = {} + end, settings.sync_delay) +end + +function view:expand_to_path(path) + local resolved = util.resolve(path) + + if vim.startswith(resolved, self.root.path) then + local dirs = vim.split(string.sub(resolved, #self.root.path + 2), '/') + local current = self.root + + for _, dir in ipairs(dirs) do + current:children() + + current = entry.find(string.format('%s/%s', current.path, dir)) + + if current then + self:set_path_attr(current.path, 'open', true) + else + break + end + end + + if current and current.path == resolved then + self.flash = current + + self:update() + + return true + end + + return false + end +end + +function view:get_path_attr(path, attr) + local state = self.states[path] + local value = state and state[attr] + + if attr == 'compressible' and value == nil then + return true + end + + return value +end + +function view:set_path_attr(path, attr, value) + if not self.states[path] then + self.states[path] = {} + end + + self.states[path][attr] = value + + return value +end + +function view:buffers() + return vim.tbl_filter(function(bufnr) + local ref = select(2, pcall(vim.api.nvim_buf_get_var, bufnr, 'carbon')) + + return ref and ref.index == self.index + end, vim.api.nvim_list_bufs()) +end + +function view:update() + self.cached_lines = nil +end + +function view:render() + local cursor + local lines = {} + local hls = {} + + for lnum, line_data in ipairs(self:current_lines()) do + lines[#lines + 1] = line_data.line + + if self.flash and self.flash.path == line_data.entry.path then + cursor = { lnum = lnum, col = 1 + (line_data.depth + 1) * 2 } + end + + for _, hl in ipairs(line_data.highlights) do + hls[#hls + 1] = { hl[1], lnum - 1, hl[2], hl[3] } + end + end + + local buf = self:buffer() + local current_mode = string.lower(vim.api.nvim_get_mode().mode) + + vim.api.nvim_buf_clear_namespace(buf, constants.hl, 0, -1) + vim.api.nvim_buf_set_option(buf, 'modifiable', true) + vim.api.nvim_buf_set_lines(buf, 0, -1, 1, lines) + vim.api.nvim_buf_set_option(buf, 'modified', false) + + if not string.find(current_mode, 'i') then + vim.api.nvim_buf_set_option(buf, 'modifiable', false) + end + + for _, hl in ipairs(hls) do + vim.api.nvim_buf_add_highlight( + buf, + constants.hl, + hl[1], + hl[2], + hl[3], + hl[4] + ) + end + + if cursor then + util.cursor(cursor.lnum, cursor.col) + + if settings.flash then + vim.defer_fn(function() + self:focus_flash( + settings.flash.duration, + 'CarbonFlash', + { cursor.lnum - 1, cursor.col - 1 }, + { cursor.lnum - 1, -1 } + ) + end, settings.flash.delay) + end + end + + self.flash = nil +end + +function view:focus_flash(duration, group, start, finish) + local buf = self:buffer() + + vim.highlight.range(buf, constants.hl_tmp, group, start, finish, {}) + + vim.defer_fn(function() + if vim.api.nvim_buf_is_valid(buf) then + vim.api.nvim_buf_clear_namespace(buf, constants.hl_tmp, 0, -1) + end + end, duration) +end + +function view:buffer() + local buffers = self:buffers() + + if buffers[1] then + return buffers[1] + end + + local mappings = { + { 'n', 'i', '' }, + { 'n', 'I', '' }, + { 'n', 'o', '' }, + { 'n', 'O', '' }, + } + + for action, mapping in pairs(settings.actions or {}) do + if type(mapping) == 'string' then + mapping = { mapping } + end + + if type(mapping) == 'table' then + for _, key in ipairs(mapping) do + mappings[#mappings + 1] = + { 'n', key, util.plug(action), { nowait = true } } + end + end + end + + local buffer = util.create_scratch_buf({ + name = self.root.path, + filetype = 'carbon.explorer', + modifiable = false, + modified = false, + bufhidden = 'wipe', + mappings = mappings, + autocmds = { + BufHidden = function() + self:hide() + end, + BufWinEnter = function() + self:show() + end, + }, + }) + + vim.api.nvim_buf_set_var( + buffer, + 'carbon', + { index = self.index, path = self.root.path } + ) + + return buffer +end + +function view:hide() -- luacheck:ignore unused argument self + vim.opt_local.wrap = vim.opt_global.wrap:get() + vim.opt_local.spell = vim.opt_global.spell:get() + vim.opt_local.fillchars = vim.opt_global.fillchars:get() + + view.sidebar = { origin = -1, target = -1 } + view.float = { origin = -1, target = -1 } +end + +function view:show() + vim.opt_local.wrap = false + vim.opt_local.spell = false + vim.opt_local.fillchars = { eob = ' ' } + + self:render() +end + +function view:up(count) + local parents = self:parents(count) + local destination = parents[#parents] + + if destination and self:switch_to_existing_view(destination.path) then + return true + end + + for idx, parent_entry in ipairs(parents) do + self:set_path_attr(self.root.path, 'open', true) + self:set_root(parent_entry, { rename = idx == #parents }) + end + + return #parents ~= 0 +end + +function view:reset() + return self:cd(self.initial) +end + +function view:cd(path) + if path == self.root.path then + return false + elseif vim.startswith(self.root.path, path) then + local new_depth = select(2, string.gsub(path, '/', '')) + local current_depth = select(2, string.gsub(self.root.path, '/', '')) + + if current_depth - new_depth > 0 then + return self:up(current_depth - new_depth) + end + elseif self:switch_to_existing_view(path) then + return true + else + return self:set_root(entry.find(path) or entry.new(path)) + end +end + +function view:down(count) + local cursor = self:cursor() + local new_root = cursor.line.path[count or vim.v.count1] or cursor.line.entry + + if not new_root.is_directory then + new_root = new_root.parent + end + + if not new_root or new_root.path == self.root.path then + return false + end + + if self:switch_to_existing_view(new_root.path) then + return true + else + self:set_path_attr(self.root.path, 'open', true) + + return self:set_root(new_root) + end +end + +function view:set_root(target, options_param) + local options = options_param or {} + local is_cwd = self.root.path == vim.loop.cwd() + + if type(target) == 'string' then + target = entry.new(target) + end + + if target.path == self.root.path then + return false + end + + self.root = target + + if options.rename ~= false then + vim.api.nvim_buf_set_name(self:buffer(), self.root.raw_path) + end + + vim.api.nvim_buf_set_var( + self:buffer(), + 'carbon', + { index = self.index, path = self.root.path } + ) + + watcher.keep(function(path) + return util.tbl_some(view.items, function(current_view) + return vim.startswith(path, current_view.root.path) + end) + end) + + if settings.sync_pwd and is_cwd then + vim.api.nvim_set_current_dir(self.root.path) + end + + return true +end + +function view:current_lines() + if not self.cached_lines then + self.cached_lines = self:lines() + end + + return self.cached_lines +end + +function view:lines(input_target, lines, depth) + lines = lines or {} + depth = depth or 0 + local target = input_target or self.root + local expand_indicator = ' ' + local collapse_indicator = ' ' + local file_icons = view.file_icons() + + if type(settings.indicators) == 'table' then + expand_indicator = settings.indicators.expand or expand_indicator + collapse_indicator = settings.indicators.collapse or collapse_indicator + end + + if not input_target and #lines == 0 then + lines[#lines + 1] = { + lnum = 1, + depth = -1, + entry = self.root, + line = self.root.name .. '/', + highlights = { { 'CarbonDir', 0, -1 } }, + icon_width = 0, + path = {}, + } + + watcher.register(self.root.path) + end + + for _, child in ipairs(target:children()) do + local tmp = child + local hls = {} + local path = {} + local lnum = 1 + #lines + local indent = string.rep(' ', depth) + local is_empty = true + local indicator = '' + local path_suffix = '' + + if settings.compress then + while + tmp.is_directory + and #tmp:children() == 1 + and self:get_path_attr(tmp.path, 'compressible') + do + watcher.register(tmp.path) + + path[#path + 1] = tmp + tmp = tmp:children()[1] + end + end + + if tmp.is_directory then + watcher.register(tmp.path) + + is_empty = #tmp:children() == 0 + path_suffix = '/' + + if not is_empty and self:get_path_attr(tmp.path, 'open') then + indicator = collapse_indicator + elseif not is_empty then + indicator = expand_indicator + else + indent = indent .. ' ' + end + else + indent = indent .. ' ' + end + + local icon = '' + local icon_highlight + + if file_icons and settings.file_icons and not tmp.is_directory then + local info = { + file_icons.get_icon( + tmp.name .. path_suffix, + vim.fn.fnamemodify(tmp.name, ':e'), + { default = true } + ), + } + + icon = info[1] or ' ' + icon_highlight = info[2] + end + + local full_path = tmp.name .. path_suffix + local indent_end = #indent + local icon_width = #icon ~= 0 and #icon + 1 or 0 + local indicator_width = #indicator ~= 0 and #indicator + 1 or 0 + local path_start = indent_end + icon_width + indicator_width + local dir_path = table.concat( + vim.tbl_map(function(parent) + return parent.name + end, path), + '/' + ) + + if path[1] then + full_path = dir_path .. '/' .. full_path + end + + if indicator_width ~= 0 and not is_empty then + hls[#hls + 1] = + { 'CarbonIndicator', indent_end, indent_end + indicator_width } + end + + if icon and icon_highlight then + hls[#hls + 1] = + { icon_highlight, indent_end + indicator_width, path_start - 1 } + end + + local entries = { unpack(path) } + entries[#entries + 1] = tmp + + for _, current_entry in ipairs(entries) do + local part = current_entry.name .. '/' + local path_end = path_start + #part + local highlight_group = 'CarbonFile' + + if current_entry.is_symlink == 1 then + highlight_group = 'CarbonSymlink' + elseif current_entry.is_symlink == 2 then + highlight_group = 'CarbonBrokenSymlink' + elseif current_entry.is_directory then + highlight_group = 'CarbonDir' + elseif current_entry.is_executable then + highlight_group = 'CarbonExe' + end + + hls[#hls + 1] = { highlight_group, path_start, path_end } + path_start = path_end + end + + local line_prefix = indent + + if indicator_width ~= 0 then + line_prefix = line_prefix .. indicator .. ' ' + end + + if icon_width ~= 0 then + line_prefix = line_prefix .. icon .. ' ' + end + + lines[#lines + 1] = { + lnum = lnum, + depth = depth, + entry = tmp, + line = line_prefix .. full_path, + icon_width = icon_width, + highlights = hls, + path = path, + } + + if tmp.is_directory and self:get_path_attr(tmp.path, 'open') then + self:lines(tmp, lines, depth + 1) + end + end + + return lines +end + +function view:cursor(opts) + local options = opts or {} + local lines = self:current_lines() + local line = lines[vim.fn.line('.')] + local target = line.entry + local target_line + + if options.target_directory_only and not target.is_directory then + target = target.parent + end + + target = line.path[vim.v.count] or target + target_line = util.tbl_find(lines, function(current) + if current.entry.path == target.path then + return true + end + + return util.tbl_find(current.path, function(parent) + if parent.path == target.path then + return true + end + end) + end) + + return { line = line, target = target, target_line = target_line } +end + +function view:create() + local ctx = self:cursor({ target_directory_only = true }) + + ctx.view = self + ctx.compact = ctx.target.is_directory and #ctx.target:children() == 0 + ctx.prev_open = self:get_path_attr(ctx.target.path, 'open') + ctx.prev_compressible = self:get_path_attr(ctx.target.path, 'compressible') + + self:set_path_attr(ctx.target.path, 'open', true) + self:set_path_attr(ctx.target.path, 'compressible', false) + + if ctx.compact then + ctx.edit_prefix = ctx.line.line + ctx.edit_lnum = ctx.line.lnum - 1 + ctx.edit_col = #ctx.edit_prefix + 1 + ctx.init_end_lnum = ctx.edit_lnum + 1 + else + ctx.edit_prefix = string.rep(' ', ctx.target_line.depth + 2) + ctx.edit_lnum = ctx.target_line.lnum + #self:lines(ctx.target) + ctx.edit_col = #ctx.edit_prefix + 1 + ctx.init_end_lnum = ctx.edit_lnum + end + + self:update() + self:render() + util.autocmd('CursorMovedI', create_insert_move(ctx), { buffer = 0 }) + vim.keymap.set('i', '', create_confirm(ctx), { buffer = 0 }) + vim.keymap.set('i', '', create_cancel(ctx), { buffer = 0 }) + vim.cmd.startinsert({ bang = true }) + vim.api.nvim_buf_set_option(0, 'modifiable', true) + vim.api.nvim_buf_set_lines( + 0, + ctx.edit_lnum, + ctx.init_end_lnum, + 1, + { ctx.edit_prefix } + ) + util.cursor(ctx.edit_lnum + 1, ctx.edit_col) +end + +function view:delete() + local cursor = self:cursor() + local targets = vim.list_extend( + { unpack(cursor.line.path) }, + { cursor.line.entry } + ) + + local lnum_idx = cursor.line.lnum - 1 + local count = vim.v.count == 0 and #targets or vim.v.count1 + local path_idx = math.min(count, #targets) + local target = targets[path_idx] + local highlight = { + 'CarbonFile', + cursor.line.depth * 2 + 2 + cursor.line.icon_width, + lnum_idx, + } + + if targets[path_idx].path == self.root.path then + return + end + + if target.is_directory then + highlight[1] = 'CarbonDir' + end + + for idx = 1, path_idx - 1 do + highlight[2] = highlight[2] + #cursor.line.path[idx].name + 1 + end + + util.clear_extmarks(0, { lnum_idx, highlight[2] }, { lnum_idx, -1 }, {}) + util.add_highlight(0, 'CarbonDanger', lnum_idx, highlight[2], -1) + util.confirm({ + row = cursor.line.lnum, + col = highlight[2], + highlight = 'CarbonDanger', + actions = { + { + label = 'delete', + shortcut = 'D', + callback = function() + local result = + vim.fn.delete(target.path, target.is_directory and 'rf' or '') + + if result == -1 then + vim.api.nvim_echo({ + { 'Failed to delete: ', 'CarbonDanger' }, + { vim.fn.fnamemodify(target.path, ':.'), 'CarbonIndicator' }, + }, false, {}) + else + view.resync(vim.fn.fnamemodify(target.path, ':h')) + end + end, + }, + { + label = 'cancel', + shortcut = 'q', + callback = function() + util.clear_extmarks(0, { lnum_idx, 0 }, { lnum_idx, -1 }, {}) + + for _, lhl in ipairs(cursor.line.highlights) do + util.add_highlight(0, lhl[1], lnum_idx, lhl[2], lhl[3]) + end + + self:render() + end, + }, + }, + }) +end + +function view:move() + local ctx = self:cursor() + local target_line = ctx.target_line + local targets = vim.list_extend( + { unpack(target_line.path) }, + { target_line.entry } + ) + local target_names = vim.tbl_map(function(part) + return part.name + end, targets) + + if ctx.target.path == self.root.path then + return + end + + local path_start = target_line.depth * 2 + 2 + target_line.icon_width + local lnum_idx = target_line.lnum - 1 + local target_idx = util.tbl_key(targets, ctx.target) + local clamped_names = { unpack(target_names, 1, target_idx - 1) } + local start_hl = path_start + #table.concat(clamped_names, '/') + + if target_idx > 1 then + start_hl = start_hl + 1 + end + + util.clear_extmarks(0, { lnum_idx, start_hl }, { lnum_idx, -1 }, {}) + util.add_highlight(0, 'CarbonPending', lnum_idx, start_hl, -1) + vim.cmd.redraw({ bang = true }) + vim.cmd.echohl('CarbonPending') + + local updated_path = string.gsub( + vim.fn.input({ + prompt = 'destination: ', + default = ctx.target.path, + cancelreturn = ctx.target.path, + }), + '/+$', + '' + ) + + vim.cmd.echohl('None') + vim.api.nvim_echo({ { ' ' } }, false, {}) + + if updated_path == ctx.target.path then + self:render() + elseif vim.loop.fs_stat(updated_path) then + self:render() + vim.api.nvim_echo({ + { 'Failed to move: ', 'CarbonDanger' }, + { vim.fn.fnamemodify(ctx.target.path, ':.'), 'CarbonIndicator' }, + { ' => ' }, + { vim.fn.fnamemodify(updated_path, ':.'), 'CarbonIndicator' }, + { ' (destination exists)', 'CarbonPending' }, + }, false, {}) + else + local directory = vim.fn.fnamemodify(updated_path, ':h') + local tmp_path = ctx.target.path + + if vim.startswith(updated_path, tmp_path) then + tmp_path = vim.fn.tempname() + + vim.fn.rename(ctx.target.path, tmp_path) + end + + vim.fn.mkdir(directory, 'p') + vim.fn.rename(tmp_path, updated_path) + view.resync(vim.fn.fnamemodify(ctx.target.path, ':h')) + end +end + +function view:parents(count) + local path = self.root.path + local parents = {} + + if path ~= '' then + for _ = count or vim.v.count1, 1, -1 do + path = vim.fn.fnamemodify(path, ':h') + parents[#parents + 1] = entry.new(path) + + if path == '/' then + break + end + end + end + + return parents +end + +function view:switch_to_existing_view(path) + local destination_view = view.find(path) + + if destination_view then + vim.api.nvim_win_set_buf(0, destination_view:buffer()) + + if settings.sync_pwd and self.root.path == vim.loop.cwd() then + vim.api.nvim_set_current_dir(destination_view.root.path) + end + + return true + end +end + +return view diff --git a/lua/carbon/watcher.lua b/lua/carbon/watcher.lua index 9a96c0d..c46c73c 100644 --- a/lua/carbon/watcher.lua +++ b/lua/carbon/watcher.lua @@ -1,9 +1,11 @@ local util = require('carbon.util') local watcher = {} -local data = { listeners = {}, events = {} } + +watcher.listeners = {} +watcher.events = {} function watcher.keep(callback) - for path in pairs(data.listeners) do + for path in pairs(watcher.listeners) do if not callback(path) then watcher.release(path) end @@ -12,21 +14,21 @@ end function watcher.release(path) if not path then - for listener_path in pairs(data.listeners) do + for listener_path in pairs(watcher.listeners) do watcher.release(listener_path) end - elseif data.listeners[path] then - data.listeners[path]:stop() + elseif watcher.listeners[path] then + watcher.listeners[path]:stop() - data.listeners[path] = nil + watcher.listeners[path] = nil end end function watcher.register(path) - if not data.listeners[path] and not util.is_excluded(path) then - data.listeners[path] = vim.loop.new_fs_event() + if not watcher.listeners[path] and not util.is_excluded(path) then + watcher.listeners[path] = vim.loop.new_fs_event() - data.listeners[path]:start( + watcher.listeners[path]:start( path, {}, vim.schedule_wrap(function(error, filename) @@ -37,11 +39,11 @@ function watcher.register(path) end function watcher.emit(event, ...) - for callback in pairs(data.events[event] or {}) do + for callback in pairs(watcher.events[event] or {}) do callback(event, ...) end - for callback in pairs(data.events['*'] or {}) do + for callback in pairs(watcher.events['*'] or {}) do callback(event, ...) end end @@ -52,33 +54,34 @@ function watcher.on(event, callback) watcher.on(key, callback) end elseif event then - data.events[event] = data.events[event] or {} - data.events[event][callback] = callback + watcher.events[event] = watcher.events[event] or {} + watcher.events[event][callback] = callback end end function watcher.off(event, callback) if not event then - data.events = {} + watcher.events = {} elseif type(event) == 'table' then for _, key in ipairs(event) do watcher.off(key, callback) end - elseif data.events[event] and callback then - data.events[event][callback] = nil - elseif event then - data.events[event] = {} + elseif watcher.events[event] and callback then + watcher.events[event][callback] = nil + elseif watcher.events[event] then + watcher.events[event] = {} else - data.events[event] = nil + watcher.events[event] = nil end end function watcher.has(event, callback) - return data.events[event] and data.events[event][callback] and true or false + return watcher.events[event] and watcher.events[event][callback] and true + or false end function watcher.registered() - return vim.tbl_keys(data.listeners) + return vim.tbl_keys(watcher.listeners) end return watcher diff --git a/plugin/carbon.vim b/plugin/carbon.vim deleted file mode 100644 index 1ae7070..0000000 --- a/plugin/carbon.vim +++ /dev/null @@ -1,3 +0,0 @@ -if !exists('g:carbon_lazy_init') - lua require('carbon').initialize() -endif diff --git a/test/config/helpers.lua b/test/config/helpers.lua index 3d9b434..5a63556 100644 --- a/test/config/helpers.lua +++ b/test/config/helpers.lua @@ -1,3 +1,5 @@ +local util = require('carbon.util') +local view = require('carbon.view') local entry = require('carbon.entry') local constants = require('carbon.constants') local helpers = {} @@ -79,6 +81,20 @@ function helpers.entry(relative_path) return entry.find(string.format('%s/%s', vim.loop.cwd(), relative_path)) end +function helpers.is_open(path) + return view.execute(function(ctx) + return ctx.view:get_path_attr(path, 'open') + end) +end + +function helpers.line_with_file() + return view.execute(function(ctx) + return util.tbl_find(ctx.view:current_lines(), function(line) + return not line.entry.is_directory + end) + end) +end + function helpers.markdown_info(absolute_path) local result = { tags = {}, refs = {}, header_tags = {}, header_refs = {} } local lines = vim.fn.readfile(absolute_path) diff --git a/test/config/init.lua b/test/config/init.lua index a5cecba..95f5820 100644 --- a/test/config/init.lua +++ b/test/config/init.lua @@ -6,7 +6,7 @@ vim.opt.runtimepath:prepend(repo_root) vim.fn.system(string.format('cp -R %s %s', repo_root, tmp_dir)) vim.fn.chdir(tmp_dir) -require('carbon').initialize() +require('carbon').setup() vim.api.nvim_create_autocmd('VimLeavePre', { pattern = '*', diff --git a/test/specs/buffer_spec.lua b/test/specs/buffer_spec.lua deleted file mode 100644 index a9d8f05..0000000 --- a/test/specs/buffer_spec.lua +++ /dev/null @@ -1,456 +0,0 @@ -require('test.config.assertions') - -local spy = require('luassert.spy') -local util = require('carbon.util') -local entry = require('carbon.entry') -local carbon = require('carbon') -local buffer = require('carbon.buffer') -local watcher = require('carbon.watcher') -local settings = require('carbon.settings') -local helpers = require('test.config.helpers') - -describe('carbon.buffer', function() - before_each(function() - carbon.explore() - util.cursor(1, 1) - end) - - describe('options', function() - it('buffer has name "carbon"', function() - assert.equal('carbon', vim.fn.bufname()) - end) - - it('buffer has filetype "carbon.explorer"', function() - assert.equal('carbon.explorer', vim.o.filetype) - assert.equal('carbon.explorer', vim.bo.filetype) - end) - - it('is not modifiable', function() - assert.is_false(vim.bo.modifiable) - end) - - it('is not modified', function() - assert.is_false(vim.bo.modified) - end) - end) - - describe('autocommands', function() - it('calls buffer.process_enter on BufWinEnter', function() - local callback = spy.on(buffer, 'process_enter') - - vim.api.nvim_exec_autocmds('BufWinEnter', { buffer = 0 }) - - assert.spy(callback).is_called() - end) - - it('calls buffer.process_hidden on BufHidden', function() - local callback = spy.on(buffer, 'process_hidden') - - vim.api.nvim_exec_autocmds('BufHidden', { buffer = 0 }) - - assert.spy(callback).is_called() - end) - end) - - describe('keymaps', function() - for action, maps in pairs(settings.actions) do - local plug = util.plug(action) - local keys = type(maps) == 'string' and { maps } or maps - - for _, key in ipairs(keys) do - it(string.format('binds %s to %s', key, plug), function() - assert.same(string.lower(vim.fn.maparg(key, 'n')), plug) - end) - end - end - end) - - describe('display', function() - it('shows current directory', function() - assert.same({ - vim.fn.fnamemodify(vim.loop.cwd(), ':t') .. '/', - ' .github/workflows/ci.yml', - ' dev/init.lua', - '+ doc/', - '+ lua/', - ' plugin/carbon.vim', - '+ test/', - ' .gitignore', - ' .luacheckrc', - ' LICENSE.md', - ' Makefile', - ' README.md', - ' stylua.toml', - }, vim.api.nvim_buf_get_lines(0, 0, -1, true)) - end) - end) - - describe('is_loaded', function() - it('returns true when buffer is loaded', function() - assert.is_true(buffer.is_loaded()) - end) - - it('returns false when buffer is not loaded', function() - local bufnr = vim.api.nvim_get_current_buf() - - vim.cmd.edit('README.md') - vim.api.nvim_buf_delete(bufnr, { force = true }) - - assert.is_false(buffer.is_loaded()) - end) - end) - - describe('is_hidden', function() - it('returns false when buffer is not hidden', function() - assert.is_false(buffer.is_hidden()) - end) - - it('returns true when buffer is hidden', function() - vim.cmd.edit('README.md') - - assert.is_true(buffer.is_hidden()) - end) - end) - - describe('handle', function() - it('returns current buffer handle if loaded', function() - assert.equal(vim.api.nvim_get_current_buf(), buffer.handle()) - end) - - it('creates and returns new buffer handle if not loaded', function() - local bufnr = vim.api.nvim_get_current_buf() - - vim.cmd.edit('README.md') - vim.api.nvim_buf_delete(bufnr, { force = true }) - - assert.not_equal(bufnr, buffer.handle()) - end) - end) - - describe('show', function() - it('replaces current buffer with carbon buffer', function() - vim.cmd.edit('README.md') - - local bufnr = vim.api.nvim_get_current_buf() - - buffer.show() - - assert.equal('carbon', vim.fn.bufname()) - assert.not_equal(bufnr, vim.api.nvim_get_current_buf()) - end) - - it('rerenders the buffer', function() - local render = spy.on(buffer, 'render') - - buffer.show() - - assert.spy(render).is_called() - end) - end) - - describe('render', function() - it('does nothing when buffer is not loaded', function() - local bufnr = vim.api.nvim_get_current_buf() - - vim.cmd.edit('README.md') - vim.api.nvim_buf_delete(bufnr, { force = true }) - - assert.is_nil(buffer.render()) - end) - - it('does nothing when buffer is hidden', function() - vim.cmd.edit('README.md') - - assert.is_nil(buffer.render()) - end) - - describe('always_reveal', function() - it('calls focus_flash when enabled', function() - local focus_flash = spy.on(buffer, 'focus_flash') - local lua_entry = entry.find(helpers.resolve('lua')) - local lua_carbon_entry = entry.find(helpers.resolve('lua')) - local target_path = helpers.resolve('lua/carbon/util.lua') - - settings.always_reveal = true - - buffer.expand_to_path(target_path) - buffer.render() - vim.wait(settings.flash.delay * 3) - - assert.spy(focus_flash).is_called() - assert.equal(target_path, buffer.cursor().line.entry.path) - - lua_entry:set_open(false) - lua_carbon_entry:set_open(false) - settings.always_reveal = settings.defaults.always_reveal - end) - end) - end) - - describe('expand_to_path', function() - it('does nothing when target outside of cwd', function() - assert.is_nil(buffer.expand_to_path('/')) - end) - - it('opens parent directories', function() - local lua_entry = entry.find(helpers.resolve('lua')) - local lua_carbon_entry = entry.find(helpers.resolve('lua')) - - assert.is_false(lua_entry:is_open()) - assert.is_false(lua_carbon_entry:is_open()) - - buffer.expand_to_path(helpers.resolve('lua/carbon/util.lua')) - - assert.is_true(lua_entry:is_open()) - assert.is_true(lua_carbon_entry:is_open()) - end) - - it('moves the cursor to the revealed entry', function() - local target_path = helpers.resolve('lua/carbon/util.lua') - - buffer.expand_to_path(target_path) - buffer.render() - - assert.equal(target_path, buffer.cursor().line.entry.path) - end) - end) - - describe('cursor', function() - it('returns line information of current line', function() - util.cursor(3, 1) - - assert.equal(3, buffer.cursor().line.lnum) - end) - - it('returns target entry with count', function() - local result - - vim.keymap.set('n', '_', function() - result = buffer.cursor() - end, { buffer = 0 }) - - util.cursor(3, 1) - helpers.type_keys('1_') - - vim.keymap.del('n', '_', { buffer = 0 }) - - assert.equal(helpers.resolve('dev'), result.target.path) - end) - end) - - describe('lines', function() - it('returns a table of line info objects', function() - for _, line_info in ipairs(buffer.lines()) do - assert.is_table(line_info) - assert.is_number(line_info.lnum) - assert.is_number(line_info.depth) - assert.is_entry(line_info.entry) - assert.is_string(line_info.line) - assert.is_table(line_info.highlights) - assert.is_table(line_info.path) - end - end) - end) - - describe('set_root', function() - it('accepts string path', function() - local target_path = helpers.resolve('lua') - - assert.equal(target_path, buffer.set_root(target_path).path) - assert.is_true(buffer.reset()) - end) - - it('accepts carbon.entry.new instance', function() - local target_entry = entry.new(helpers.resolve('lua')) - - assert.equal(target_entry.path, buffer.set_root(target_entry).path) - assert.is_true(buffer.reset()) - end) - - it('filters watchers', function() - local watchers = watcher.registered() - - buffer.set_root(helpers.resolve('lua')) - - assert.not_same(watchers, watcher.registered()) - assert.is_true(buffer.reset()) - end) - - describe('sync_pwd', function() - it("sets Neovim's cwd when enabled", function() - local cwd = vim.loop.cwd() - - settings.sync_pwd = true - - buffer.set_root(helpers.resolve('lua')) - - settings.sync_pwd = settings.defaults.sync_pwd - - assert.not_same(cwd, vim.loop.cwd()) - assert.is_true(buffer.reset()) - end) - - it("does not set Neovim's cwd when disabled", function() - local cwd = vim.loop.cwd() - - buffer.set_root(helpers.resolve('lua')) - - assert.same(cwd, vim.loop.cwd()) - assert.is_true(buffer.reset()) - end) - end) - end) - - describe('reset', function() - describe('sync_pwd', function() - it("resets Neovim's cwd when enabled", function() - local cwd = vim.loop.cwd() - - settings.sync_pwd = true - - buffer.set_root(helpers.resolve('lua')) - - settings.sync_pwd = settings.defaults.sync_pwd - - assert.not_same(cwd, vim.loop.cwd()) - assert.is_true(buffer.reset()) - assert.same(cwd, vim.loop.cwd()) - end) - - it("resets Neovim's cwd when disabled", function() - local cwd = vim.loop.cwd() - - settings.sync_pwd = true - - buffer.set_root(helpers.resolve('lua')) - - settings.sync_pwd = false - - assert.not_same(cwd, vim.loop.cwd()) - assert.is_true(buffer.reset()) - assert.same(cwd, vim.loop.cwd()) - - settings.sync_pwd = settings.defaults.sync_pwd - end) - end) - end) - - describe('set_lines', function() - it('overwrites buffer content', function() - buffer.set_lines(0, 0, { 'a', 'b', 'c' }) - - assert.same({ 'a', 'b', 'c' }, vim.api.nvim_buf_get_lines(0, 0, 3, true)) - end) - - it('works when modifiable is false', function() - assert.is_false(vim.bo.modifiable) - buffer.set_lines(0, 0, { 'a', 'b', 'c' }) - - assert.same({ 'a', 'b', 'c' }, vim.api.nvim_buf_get_lines(0, 0, 3, true)) - end) - - it('resets modifiable when not in insert', function() - buffer.set_lines(0, 0, { 'a', 'b', 'c' }) - assert.is_false(vim.bo.modifiable) - end) - - it('does not set modified', function() - buffer.set_lines(0, 0, { 'a', 'b', 'c' }) - assert.is_false(vim.bo.modified) - end) - end) - - -- FIXME: fs events have wildly inconsistent timing, causing - -- tests below to fail often while actually working well - -- in isolated scenarios. Because of this, all tests - -- below have been marked "pending" until a solution is found. - - describe('create', function() - pending('can create file', function() - helpers.type_keys( - string.format('%shello.txt', settings.actions.create) - ) - - assert.is_true(helpers.has_path('hello.txt')) - end) - - pending('can create directory', function() - helpers.type_keys(string.format('%shello/', settings.actions.create)) - - assert.is_true(helpers.has_path('hello/')) - assert.is_true(helpers.is_directory('hello/')) - end) - - pending('can create deeply nested path', function() - helpers.type_keys( - string.format('%shello/world/test.txt', settings.actions.create) - ) - - assert.is_true(helpers.has_path('hello/world/test.txt')) - end) - end) - - describe('delete', function() - pending('can delete file', function() - helpers.ensure_path('.a/.a.txt') - - util.cursor(2, 1) - helpers.type_keys(string.format('%sD', settings.actions.delete)) - - assert.is_true(helpers.has_path('.a/')) - assert.is_false(helpers.has_path('.a/.a.txt')) - end) - - pending('can delete directory', function() - helpers.ensure_path('.a/') - - util.cursor(2, 1) - helpers.type_keys(string.format('%sD', settings.actions.delete)) - - assert.is_false(helpers.has_path('.a/')) - end) - - pending('can partially delete deeply nested path using count', function() - helpers.ensure_path('.a/.a/a.txt') - util.cursor(2, 1) - helpers.type_keys(string.format('2%sD', settings.actions.delete)) - - assert.is_true(helpers.has_path('.a/')) - assert.is_false(helpers.has_path('.a/.a/')) - end) - - pending('can completely delete deeply nested path using count', function() - helpers.ensure_path('.a/.a/.a.txt') - util.cursor(2, 1) - helpers.type_keys(string.format('1%sD', settings.actions.delete)) - - assert.is_false(helpers.has_path('.a/')) - end) - end) - - describe('move', function() - pending('can rename path', function() - helpers.ensure_path('.a/.a.txt') - - util.cursor(2, 1) - helpers.type_keys(string.format('%s1', settings.actions.move)) - - assert.is_false(helpers.has_path('.a/.a.txt')) - assert.is_true(helpers.has_path('.a/.a.txt1')) - - helpers.delete_path('.a/') - end) - - pending('creates intermediate directories', function() - helpers.ensure_path('.a/.a.txt') - - util.cursor(2, 1) - helpers.type_keys( - string.format('%s/b/c', settings.actions.move) - ) - - assert.is_false(helpers.has_path('.a/.a/.a.txt')) - assert.is_true(helpers.has_path('.a/.a/b/c')) - end) - end) -end) diff --git a/test/specs/carbon_spec.lua b/test/specs/carbon_spec.lua index 4b8ab97..dbe74a9 100644 --- a/test/specs/carbon_spec.lua +++ b/test/specs/carbon_spec.lua @@ -3,7 +3,7 @@ require('test.config.assertions') local spy = require('luassert.spy') local util = require('carbon.util') local carbon = require('carbon') -local buffer = require('carbon.buffer') +local view = require('carbon.view') local watcher = require('carbon.watcher') local settings = require('carbon.settings') local helpers = require('test.config.helpers') @@ -12,17 +12,11 @@ describe('carbon', function() before_each(function() carbon.explore() util.cursor(1, 1) - vim.cmd.only() + vim.cmd.only({ mods = { silent = true } }) end) describe('autocommands', function() describe('DirChanged', function() - it('exists', function() - local autocmd = helpers.autocmd('DirChanged') - - assert.is_number(autocmd.id) - end) - it('is not buffer local', function() local autocmd = helpers.autocmd('DirChanged') @@ -37,26 +31,23 @@ describe('carbon', function() end) describe('BufWinEnter', function() - it('exists', function() - local autocmd = helpers.autocmd('BufWinEnter') + it('has buffer local event', function() + local autocmd = helpers.autocmd( + 'BufWinEnter', + { buffer = vim.api.nvim_get_current_buf() } + ) - assert.is_number(autocmd.id) + assert.is_true(autocmd.buflocal) end) - it('is buffer local', function() + it('has a global event', function() local autocmd = helpers.autocmd('BufWinEnter') - assert.is_true(autocmd.buflocal) + assert.is_false(autocmd.buflocal) end) end) describe('BufHidden', function() - it('exists', function() - local autocmd = helpers.autocmd('BufHidden') - - assert.is_number(autocmd.id) - end) - it('is buffer local', function() local autocmd = helpers.autocmd('BufHidden') @@ -100,49 +91,61 @@ describe('carbon', function() util.cursor(4, 1) carbon.edit() - assert.is_true(doc_entry:is_open()) + view.execute(function(ctx) + assert.is_true(ctx.view:get_path_attr(doc_entry.path, 'open')) + end) carbon.edit() - assert.is_false(doc_entry:is_open()) + view.execute(function(ctx) + assert.is_false(ctx.view:get_path_attr(doc_entry.path, 'open')) + end) end) it('edits file when on file', function() - assert.equal('carbon', vim.fn.bufname()) + assert.equal('carbon.explorer', vim.bo.filetype) util.cursor(12, 1) carbon.edit() - assert.not_equal('carbon', vim.fn.bufname()) + assert.not_equal('carbon.explorer', vim.bo.filetype) end) end) describe('split', function() it('open file in horizontal split', function() - assert.equal('carbon', vim.fn.bufname()) + assert.equal('carbon.explorer', vim.bo.filetype) - util.cursor(3, 1) + local file_line = helpers.line_with_file() + + assert.not_nil(file_line) + + util.cursor(file_line.lnum, 1) carbon.split() - assert.not_equal('carbon', vim.fn.bufname()) + assert.not_equal('carbon.explorer', vim.bo.filetype) vim.cmd.wincmd('j') - assert.equal('carbon', vim.fn.bufname()) + assert.equal('carbon.explorer', vim.bo.filetype) end) end) describe('vsplit', function() it('open file in vertical split', function() - assert.equal('carbon', vim.fn.bufname()) + assert.equal('carbon.explorer', vim.bo.filetype) + + local file_line = helpers.line_with_file() - util.cursor(3, 1) + assert.not_nil(file_line) + + util.cursor(file_line.lnum, 1) carbon.vsplit() - assert.not_equal('carbon', vim.fn.bufname()) + assert.not_equal('carbon.explorer', vim.bo.filetype) vim.cmd.wincmd('l') - assert.equal('carbon', vim.fn.bufname()) + assert.equal('carbon.explorer', vim.bo.filetype) end) end) @@ -154,16 +157,16 @@ describe('carbon', function() assert.not_nil(assets_entry) carbon.toggle_recursive() - assert.is_true(assets_entry:is_open()) + assert.is_true(helpers.is_open(assets_entry.path)) assert.same( { '- doc/', ' - assets/' }, vim.api.nvim_buf_get_lines(0, 3, 5, true) ) carbon.toggle_recursive() - assert.is_false(assets_entry:is_open()) + assert.is_false(helpers.is_open(assets_entry.path)) assert.same( - { '+ doc/', '+ lua/' }, + { '+ doc/', '+ lua/carbon/' }, vim.api.nvim_buf_get_lines(0, 3, 5, true) ) end) @@ -171,23 +174,27 @@ describe('carbon', function() describe('close_parent', function() it('closes parent of cursor entry and moves cursor', function() - util.cursor(7, 1) + util.cursor(6, 1) carbon.edit() - util.cursor(9, 1) + util.cursor(8, 1) carbon.close_parent() - assert.equal(7, vim.fn.line('.')) + assert.equal(6, vim.fn.line('.')) assert.equal(3, vim.fn.col('.')) end) end) describe('explore', function() it('shows the buffer', function() - util.cursor(12, 1) + local file_line = helpers.line_with_file() + + assert.not_nil(file_line) + + util.cursor(file_line.lnum, 1) carbon.edit() carbon.explore() - assert.equal('carbon', vim.fn.bufname()) + assert.equal('carbon.explorer', vim.bo.filetype) end) end) @@ -274,20 +281,6 @@ describe('carbon', function() assert.not_same(original_listeners, watcher.registered()) end) - - it('automatically opens previous cwd', function() - util.cursor(1, 1) - - assert.is_equal('carbon', vim.fn.bufname()) - - local root_entry = buffer.cursor().line.entry - - carbon.up() - - assert.is_true(root_entry:is_open()) - - carbon.reset() - end) end) describe('reset', function() @@ -314,19 +307,9 @@ describe('carbon', function() assert.equal(vim.loop.cwd(), string.format('%s/.github', original_cwd)) carbon.reset() + assert.equal(vim.loop.cwd(), original_cwd) settings.sync_pwd = settings.defaults.sync_pwd end) - - it('releases registered listeners not in new cwd', function() - local original_listeners = watcher.registered() - - util.cursor(2, 1) - carbon.down() - - assert.not_same(original_listeners, watcher.registered()) - - carbon.reset() - end) end) describe('cd', function() @@ -350,40 +333,40 @@ describe('carbon', function() carbon.explore() helpers.type_keys(settings.actions.quit) - assert.not_equal('carbon', vim.fn.bufname()) + assert.not_equal('carbon.explorer', vim.bo.filetype) end) end) describe('create', function() - it('calls buffer.create', function() - local buffer_create = spy.on(buffer, 'create') + it('calls view.create', function() + local view_create = spy.on(view, 'create') carbon.create() helpers.type_keys('') - assert.spy(buffer_create).is_called() + assert.spy(view_create).is_called() end) end) describe('delete', function() - it('calls buffer.delete', function() - local buffer_delete = spy.on(buffer, 'delete') + it('calls view.delete', function() + local view_delete = spy.on(view, 'delete') carbon.delete() helpers.type_keys('') - assert.spy(buffer_delete).is_called() + assert.spy(view_delete).is_called() end) end) describe('move', function() - it('calls buffer.move', function() - local buffer_move = spy.on(buffer, 'move') + it('calls view.move', function() + local view_move = spy.on(view, 'move') carbon.move() helpers.type_keys('') - assert.spy(buffer_move).is_called() + assert.spy(view_move).is_called() end) end) end) diff --git a/test/specs/entry_spec.lua b/test/specs/entry_spec.lua index 43b36ff..20ba715 100644 --- a/test/specs/entry_spec.lua +++ b/test/specs/entry_spec.lua @@ -132,93 +132,6 @@ describe('carbon.entry', function() end) end) - describe('is_compressible', function() - it('is boolean', function() - local target = entry.new(helpers.resolve('lua')) - - assert.is_boolean(target:is_compressible()) - end) - - it('is true by default', function() - local target = entry.new(helpers.resolve('lua')) - - target:set_compressible(nil) - - assert.is_true(target:is_compressible()) - end) - end) - - describe('set_compressible', function() - it('sets compressible status', function() - local target = entry.new(helpers.resolve('lua')) - - target:set_compressible(false) - - assert.is_false(target:is_compressible()) - - target:set_compressible(nil) - end) - end) - - describe('is_open', function() - it('is boolean', function() - local target = entry.new(helpers.resolve('lua')) - - assert.is_boolean(target:is_open()) - end) - - it('is false by default', function() - local target = entry.new(helpers.resolve('lua')) - - assert.is_false(target:is_open()) - end) - end) - - describe('set_open', function() - it('does nothing when called on regular file', function() - local file = entry.new(helpers.resolve('README.md')) - - file:set_open(true) - - assert.is_false(file:is_open()) - end) - - it('sets opened status', function() - local target = entry.new(helpers.resolve('lua')) - - target:set_open(true) - - assert.is_true(target:is_open()) - - target:set_open(nil) - end) - - it('opens recursively when recursive is true', function() - local target = entry.new(helpers.resolve('lua')) - local dirs = vim.tbl_filter(function(child) - return child.is_directory - end, target:children()) - - assert.is_true(#dirs > 0) - - target:set_open(true, true) - - assert.is_true(target:is_open()) - - for _, dir_child in ipairs(dirs) do - assert.is_true(dir_child:is_open()) - end - - target:set_open(nil, true) - - assert.is_false(target:is_open()) - - for _, dir_child in ipairs(dirs) do - assert.is_false(dir_child:is_open()) - end - end) - end) - describe('children', function() it('returns empty table on regular file', function() local file = entry.new(helpers.resolve('README.md')) diff --git a/test/specs/settings_spec.lua b/test/specs/settings_spec.lua index ea18328..c282147 100644 --- a/test/specs/settings_spec.lua +++ b/test/specs/settings_spec.lua @@ -74,9 +74,9 @@ describe('carbon.settings', function() end) end) - describe('always_reveal', function() + describe('auto_reveal', function() it('is a boolean', function() - assert.is_boolean(settings.always_reveal) + assert.is_boolean(settings.auto_reveal) end) end) diff --git a/test/specs/util_spec.lua b/test/specs/util_spec.lua index 5730289..7a50d12 100644 --- a/test/specs/util_spec.lua +++ b/test/specs/util_spec.lua @@ -1,10 +1,29 @@ require('test.config.assertions') local spy = require('luassert.spy') +local view = require('carbon.view') local util = require('carbon.util') local helpers = require('test.config.helpers') describe('carbon.util', function() + describe('explore_path', function() + it('{path} is expanded to an absolute path', function() + local cwd = vim.loop.cwd() + local parent = vim.fn.fnamemodify(cwd, ':h') + + assert.equal(parent, util.explore_path('../')) + assert.equal(parent, util.explore_path('..')) + end) + + it('{path} is expanded relative to {current_view}', function() + local current_view = view.get(vim.fn.tempname()) + local parent = vim.fn.fnamemodify(current_view.root.path, ':h') + + assert.equal(parent, util.explore_path('../', current_view)) + assert.equal(parent, util.explore_path('..', current_view)) + end) + end) + describe('cursor', function() it('{lnum} and {col} are both 1-based', function() util.cursor(2, 2) diff --git a/test/specs/watcher_spec.lua b/test/specs/watcher_spec.lua index 3f56204..016d59d 100644 --- a/test/specs/watcher_spec.lua +++ b/test/specs/watcher_spec.lua @@ -46,20 +46,23 @@ describe('carbon.watcher', function() -- FIXME: remove pending status after figuring out how to wait for fs events describe('carbon:synchronize', function() - pending('triggers on new file', function() + local it_when_only = vim.env.only and it or pending + + it_when_only('triggers on new file', function() local callback = spy() watcher.register(vim.loop.cwd()) watcher.on('carbon:synchronize', callback) helpers.ensure_path('check.txt') + vim.wait(100) assert .spy(callback) .is_called_with('carbon:synchronize', vim.loop.cwd(), 'check.txt', nil) end) - pending('triggers on file change', function() + it_when_only('triggers on file change', function() local callback = spy() helpers.ensure_path('check.sh') @@ -68,6 +71,7 @@ describe('carbon.watcher', function() watcher.on('carbon:synchronize', callback) helpers.change_file('check.sh') + vim.wait(100) assert.spy(callback).is_called() assert @@ -75,7 +79,7 @@ describe('carbon.watcher', function() .is_called_with('carbon:synchronize', vim.loop.cwd(), 'check.sh', nil) end) - pending('triggers on file remove', function() + it_when_only('triggers on file remove', function() local callback = spy() helpers.ensure_path('check.sh') @@ -84,6 +88,7 @@ describe('carbon.watcher', function() watcher.on('carbon:synchronize', callback) helpers.delete_path('check.sh') + vim.wait(100) assert.spy(callback).is_called() assert