diff --git a/.envrc b/.envrc index 175de894..d522e346 100644 --- a/.envrc +++ b/.envrc @@ -1 +1,3 @@ +export VIRTUAL_ENV=venv layout python +python -c 'import pyparsing' 2>/dev/null || pip install -r scripts/requirements.txt diff --git a/.gitignore b/.gitignore index d8cb86e1..d427c406 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ luac.out *.zip *.tar.gz +# python bytecode +__pycache__ + # Object files *.o *.os @@ -41,6 +44,10 @@ luac.out .direnv/ .testenv/ +venv/ doc/tags scripts/nvim_doc_tools scripts/nvim-typecheck-action +scripts/benchmark.nvim +perf/tmp/ +profile.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 39811f85..3e7284a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [2.13.0](https://github.com/stevearc/oil.nvim/compare/v2.12.2...v2.13.0) (2024-11-11) + + +### Features + +* config option to customize floating window title ([#482](https://github.com/stevearc/oil.nvim/issues/482)) ([5d2dfae](https://github.com/stevearc/oil.nvim/commit/5d2dfae655b9b689bd4017b3bdccd52cbee5b92f)) +* config option to disable lsp file methods ([#477](https://github.com/stevearc/oil.nvim/issues/477)) ([f60bb7f](https://github.com/stevearc/oil.nvim/commit/f60bb7f793477d99ef1acf39e920bf2ca4e644de)) + + +### Bug Fixes + +* actions.preview accepts options ([#497](https://github.com/stevearc/oil.nvim/issues/497)) ([cca1631](https://github.com/stevearc/oil.nvim/commit/cca1631d5ea450c09ba72f3951a9e28105a3632c)) +* add trailing slash to directories on yank_entry ([#504](https://github.com/stevearc/oil.nvim/issues/504)) ([42333bb](https://github.com/stevearc/oil.nvim/commit/42333bb46e34dd47e13927010b1dcd30e6e4ca96)) +* don't deep merge keymaps ([#510](https://github.com/stevearc/oil.nvim/issues/510)) ([709403c](https://github.com/stevearc/oil.nvim/commit/709403ccd6f22d859c2e42c780ab558ae89284d9)) +* guard against nil keymaps ([621f8ba](https://github.com/stevearc/oil.nvim/commit/621f8ba4fa821724e9b646732a26fb2e795fe008)) +* only map ~ for normal mode ([#484](https://github.com/stevearc/oil.nvim/issues/484)) ([ccab9d5](https://github.com/stevearc/oil.nvim/commit/ccab9d5e09e2d0042fbbe5b6bd05e82426247067)) +* sort keymap help entries by description ([#506](https://github.com/stevearc/oil.nvim/issues/506)) ([52cc8a1](https://github.com/stevearc/oil.nvim/commit/52cc8a1fb35ea6ce1df536143add7ce7215c63c0)), closes [#376](https://github.com/stevearc/oil.nvim/issues/376) + ## [2.12.2](https://github.com/stevearc/oil.nvim/compare/v2.12.1...v2.12.2) (2024-09-10) diff --git a/Makefile b/Makefile index 71447f8f..10f01d11 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,66 @@ -.PHONY: all doc test lint fastlint clean +## help: print this help message +.PHONY: help +help: + @echo 'Usage:' + @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /' +## all: generate docs, lint, and run tests +.PHONY: all all: doc lint test -doc: scripts/nvim_doc_tools - python scripts/main.py generate - python scripts/main.py lint +venv: + python3 -m venv venv + venv/bin/pip install -r scripts/requirements.txt +## doc: generate documentation +.PHONY: doc +doc: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py generate + venv/bin/python scripts/main.py lint + +## test: run tests +.PHONY: test test: ./run_tests.sh +## lint: run linters and LuaLS typechecking +.PHONY: lint lint: scripts/nvim-typecheck-action fastlint ./scripts/nvim-typecheck-action/typecheck.sh --workdir scripts/nvim-typecheck-action lua -fastlint: scripts/nvim_doc_tools - python scripts/main.py lint +## fastlint: run only fast linters +.PHONY: fastlint +fastlint: scripts/nvim_doc_tools venv + venv/bin/python scripts/main.py lint luacheck lua tests --formatter plain stylua --check lua tests +## profile: use LuaJIT profiler to profile the plugin +.PHONY: profile +profile: scripts/benchmark.nvim + nvim --clean -u perf/bootstrap.lua -c 'lua jit_profile()' + +## flame_profile: create a trace in the chrome profiler format +.PHONY: flame_profile +flame_profile: scripts/benchmark.nvim + nvim --clean -u perf/bootstrap.lua -c 'lua flame_profile()' + +## benchmark: benchmark performance opening directory with many files +.PHONY: benchmark +benchmark: scripts/benchmark.nvim + nvim --clean -u perf/bootstrap.lua -c 'lua benchmark()' + @cat perf/tmp/benchmark.txt + scripts/nvim_doc_tools: git clone https://github.com/stevearc/nvim_doc_tools scripts/nvim_doc_tools scripts/nvim-typecheck-action: git clone https://github.com/stevearc/nvim-typecheck-action scripts/nvim-typecheck-action +scripts/benchmark.nvim: + git clone https://github.com/stevearc/benchmark.nvim scripts/benchmark.nvim + +## clean: reset the repository to a clean state +.PHONY: clean clean: - rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action + rm -rf scripts/nvim_doc_tools scripts/nvim-typecheck-action venv .testenv perf/tmp profile.json diff --git a/README.md b/README.md index 46063b2c..f690904e 100644 --- a/README.md +++ b/README.md @@ -214,15 +214,16 @@ require("oil").setup({ show_hidden = false, -- This function defines what is considered a "hidden" file is_hidden_file = function(name, bufnr) - return vim.startswith(name, ".") + local m = name:match("^%.") + return m ~= nil end, -- This function defines what will never be shown, even when `show_hidden` is set is_always_hidden = function(name, bufnr) return false end, - -- Sort file names in a more intuitive order for humans. Is less performant, - -- so you may want to set to false if you work with large directories. - natural_order = true, + -- Sort file names with numbers in a more intuitive order for humans. + -- Can be "fast", true, or false. "fast" will turn it off for large directories. + natural_order = "fast", -- Sort file and directory names case insensitive case_insensitive = false, sort = { @@ -231,6 +232,10 @@ require("oil").setup({ { "type", "asc" }, { "name", "asc" }, }, + -- Customize the highlight group for the file name + highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) + return nil + end, }, -- Extra arguments to pass to SCP when moving/copying files over SSH extra_scp_args = {}, @@ -267,8 +272,21 @@ require("oil").setup({ return conf end, }, - -- Configuration for the actions floating preview window - preview = { + -- Configuration for the file preview window + preview_win = { + -- Whether the preview window is automatically updated when the cursor is moved + update_on_cursor_moved = true, + -- How to open the preview window "load"|"scratch"|"fast_scratch" + preview_method = "fast_scratch", + -- A function that returns true to disable preview on a file e.g. to avoid lag + disable_preview = function(filename) + return false + end, + -- Window-local options to use for preview window buffers + win_options = {}, + }, + -- Configuration for the floating action confirmation window + confirmation = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -289,8 +307,6 @@ require("oil").setup({ win_options = { winblend = 0, }, - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, }, -- Configuration for the floating progress window progress = { @@ -356,8 +372,8 @@ Note that at the moment the ssh adapter does not support Windows machines, and i - [open_float(dir)](doc/api.md#open_floatdir) - [toggle_float(dir)](doc/api.md#toggle_floatdir) - [open(dir)](doc/api.md#opendir) -- [close()](doc/api.md#close) -- [open_preview(opts)](doc/api.md#open_previewopts) +- [close(opts)](doc/api.md#closeopts) +- [open_preview(opts, callback)](doc/api.md#open_previewopts-callback) - [select(opts, callback)](doc/api.md#selectopts-callback) - [save(opts, cb)](doc/api.md#saveopts-cb) - [setup(opts)](doc/api.md#setupopts) diff --git a/doc/api.md b/doc/api.md index e107293f..2f51cddc 100644 --- a/doc/api.md +++ b/doc/api.md @@ -13,8 +13,8 @@ - [open_float(dir)](#open_floatdir) - [toggle_float(dir)](#toggle_floatdir) - [open(dir)](#opendir) -- [close()](#close) -- [open_preview(opts)](#open_previewopts) +- [close(opts)](#closeopts) +- [open_preview(opts, callback)](#open_previewopts-callback) - [select(opts, callback)](#selectopts-callback) - [save(opts, cb)](#saveopts-cb) - [setup(opts)](#setupopts) @@ -119,49 +119,54 @@ Open oil browser for a directory | ----- | ------------- | ------------------------------------------------------------------------------------------- | | dir | `nil\|string` | When nil, open the parent of the current buffer, or the cwd if current buffer is not a file | -## close() +## close(opts) -`close()` \ +`close(opts)` \ Restore the buffer that was present when oil was opened +| Param | Type | Desc | +| ----------------- | -------------------- | --------------------------------------------------- | +| opts | `nil\|oil.CloseOpts` | | +| >exit_if_last_buf | `nil\|boolean` | Exit vim if this oil buffer is the last open buffer | -## open_preview(opts) +## open_preview(opts, callback) -`open_preview(opts)` \ +`open_preview(opts, callback)` \ Preview the entry under the cursor in a split -| Param | Type | Desc | | -| ----- | ------------ | -------------------------------------------------- | ------------------------------------- | -| opts | `nil\|table` | | | -| | vertical | `boolean` | Open the buffer in a vertical split | -| | horizontal | `boolean` | Open the buffer in a horizontal split | -| | split | `"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| Param | Type | Desc | +| ----------- | ------------------------------------------------------- | ---------------------------------------------- | +| opts | `nil\|oil.OpenPreviewOpts` | | +| >vertical | `nil\|boolean` | Open the buffer in a vertical split | +| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | +| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| callback | `nil\|fun(err: nil\|string)` | Called once the preview window has been opened | ## select(opts, callback) `select(opts, callback)` \ Select the entry under the cursor -| Param | Type | Desc | | -| -------- | ---------------------------- | ------------------------------------------------------- | ---------------------------------------------------- | -| opts | `nil\|oil.SelectOpts` | | | -| | vertical | `nil\|boolean` | Open the buffer in a vertical split | -| | horizontal | `nil\|boolean` | Open the buffer in a horizontal split | -| | split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | -| | tab | `nil\|boolean` | Open the buffer in a new tab | -| | close | `nil\|boolean` | Close the original oil buffer once selection is made | -| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | | +| Param | Type | Desc | +| ----------- | ------------------------------------------------------- | ---------------------------------------------------- | +| opts | `nil\|oil.SelectOpts` | | +| >vertical | `nil\|boolean` | Open the buffer in a vertical split | +| >horizontal | `nil\|boolean` | Open the buffer in a horizontal split | +| >split | `nil\|"aboveleft"\|"belowright"\|"topleft"\|"botright"` | Split modifier | +| >tab | `nil\|boolean` | Open the buffer in a new tab | +| >close | `nil\|boolean` | Close the original oil buffer once selection is made | +| callback | `nil\|fun(err: nil\|string)` | Called once all entries have been opened | ## save(opts, cb) `save(opts, cb)` \ Save all changes -| Param | Type | Desc | | -| ----- | ---------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------- | -| opts | `nil\|table` | | | -| | confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | -| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | | +| Param | Type | Desc | +| -------- | ---------------------------- | ------------------------------------------------------------------------------------------- | +| opts | `nil\|table` | | +| >confirm | `nil\|boolean` | Show confirmation when true, never when false, respect skip_confirm_for_simple_edits if nil | +| cb | `nil\|fun(err: nil\|string)` | Called when mutations complete. | **Note:**
diff --git a/doc/oil.txt b/doc/oil.txt index 8e96cf45..63bca11a 100644 --- a/doc/oil.txt +++ b/doc/oil.txt @@ -99,15 +99,16 @@ CONFIG *oil-confi show_hidden = false, -- This function defines what is considered a "hidden" file is_hidden_file = function(name, bufnr) - return vim.startswith(name, ".") + local m = name:match("^%.") + return m ~= nil end, -- This function defines what will never be shown, even when `show_hidden` is set is_always_hidden = function(name, bufnr) return false end, - -- Sort file names in a more intuitive order for humans. Is less performant, - -- so you may want to set to false if you work with large directories. - natural_order = true, + -- Sort file names with numbers in a more intuitive order for humans. + -- Can be "fast", true, or false. "fast" will turn it off for large directories. + natural_order = "fast", -- Sort file and directory names case insensitive case_insensitive = false, sort = { @@ -116,6 +117,10 @@ CONFIG *oil-confi { "type", "asc" }, { "name", "asc" }, }, + -- Customize the highlight group for the file name + highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) + return nil + end, }, -- Extra arguments to pass to SCP when moving/copying files over SSH extra_scp_args = {}, @@ -152,8 +157,21 @@ CONFIG *oil-confi return conf end, }, - -- Configuration for the actions floating preview window - preview = { + -- Configuration for the file preview window + preview_win = { + -- Whether the preview window is automatically updated when the cursor is moved + update_on_cursor_moved = true, + -- How to open the preview window "load"|"scratch"|"fast_scratch" + preview_method = "fast_scratch", + -- A function that returns true to disable preview on a file e.g. to avoid lag + disable_preview = function(filename) + return false + end, + -- Window-local options to use for preview window buffers + win_options = {}, + }, + -- Configuration for the floating action confirmation window + confirmation = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -174,8 +192,6 @@ CONFIG *oil-confi win_options = { winblend = 0, }, - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, }, -- Configuration for the floating progress window progress = { @@ -308,19 +324,25 @@ open({dir}) *oil.ope {dir} `nil|string` When nil, open the parent of the current buffer, or the cwd if current buffer is not a file -close() *oil.close* +close({opts}) *oil.close* Restore the buffer that was present when oil was opened + Parameters: + {opts} `nil|oil.CloseOpts` + {exit_if_last_buf} `nil|boolean` Exit vim if this oil buffer is the + last open buffer -open_preview({opts}) *oil.open_preview* +open_preview({opts}, {callback}) *oil.open_preview* Preview the entry under the cursor in a split Parameters: - {opts} `nil|table` - {vertical} `boolean` Open the buffer in a vertical split - {horizontal} `boolean` Open the buffer in a horizontal split - {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + {opts} `nil|oil.OpenPreviewOpts` + {vertical} `nil|boolean` Open the buffer in a vertical split + {horizontal} `nil|boolean` Open the buffer in a horizontal split + {split} `nil|"aboveleft"|"belowright"|"topleft"|"botright"` Split modifier + {callback} `nil|fun(err: nil|string)` Called once the preview window has + been opened select({opts}, {callback}) *oil.select* Select the entry under the cursor @@ -503,6 +525,9 @@ change_sort *actions.change_sor close *actions.close* Close oil and restore original buffer + Parameters: + {exit_if_last_buf} `boolean` Exit vim if oil is closed as the last buffer + open_cmdline *actions.open_cmdline* Open vim cmdline with current entry as an argument @@ -527,6 +552,12 @@ preview *actions.previe Open the entry under the cursor in a preview window, or close the preview window if already open + Parameters: + {horizontal} `boolean` Open the buffer in a horizontal split + {split} `"aboveleft"|"belowright"|"topleft"|"botright"` Split + modifier + {vertical} `boolean` Open the buffer in a vertical split + preview_scroll_down *actions.preview_scroll_down* Scroll down in the preview window @@ -580,24 +611,54 @@ yank_entry *actions.yank_entr -------------------------------------------------------------------------------- HIGHLIGHTS *oil-highlights* +OilHidden *hl-OilHidden* + Hidden entry in an oil buffer + OilDir *hl-OilDir* Directory names in an oil buffer +OilDirHidden *hl-OilDirHidden* + Hidden directory names in an oil buffer + OilDirIcon *hl-OilDirIcon* Icon for directories OilSocket *hl-OilSocket* Socket files in an oil buffer +OilSocketHidden *hl-OilSocketHidden* + Hidden socket files in an oil buffer + OilLink *hl-OilLink* Soft links in an oil buffer +OilOrphanLink *hl-OilOrphanLink* + Orphaned soft links in an oil buffer + +OilLinkHidden *hl-OilLinkHidden* + Hidden soft links in an oil buffer + +OilOrphanLinkHidden *hl-OilOrphanLinkHidden* + Hidden orphaned soft links in an oil buffer + OilLinkTarget *hl-OilLinkTarget* The target of a soft link +OilOrphanLinkTarget *hl-OilOrphanLinkTarget* + The target of an orphaned soft link + +OilLinkTargetHidden *hl-OilLinkTargetHidden* + The target of a hidden soft link + +OilOrphanLinkTargetHidden *hl-OilOrphanLinkTargetHidden* + The target of an hidden orphaned soft link + OilFile *hl-OilFile* Normal files in an oil buffer +OilFileHidden *hl-OilFileHidden* + Hidden normal files in an oil buffer + OilCreate *hl-OilCreate* Create action in the oil preview window @@ -651,7 +712,7 @@ Mac: (instead of being able to see files that were trashed from a directory). Windows: - Oil does not yet support the Windows trash. PRs are welcome! + Oil supports the Windows Recycle Bin. All features should work. ================================================================================ vim:tw=80:ts=2:ft=help:norl:syntax=help: diff --git a/lua/oil/actions.lua b/lua/oil/actions.lua index 6a2a5ffe..8dabe40a 100644 --- a/lua/oil/actions.lua +++ b/lua/oil/actions.lua @@ -69,7 +69,21 @@ M.select_tab = { M.preview = { desc = "Open the entry under the cursor in a preview window, or close the preview window if already open", - callback = function() + parameters = { + vertical = { + type = "boolean", + desc = "Open the buffer in a vertical split", + }, + horizontal = { + type = "boolean", + desc = "Open the buffer in a horizontal split", + }, + split = { + type = '"aboveleft"|"belowright"|"topleft"|"botright"', + desc = "Split modifier", + }, + }, + callback = function(opts) local entry = oil.get_cursor_entry() if not entry then vim.notify("Could not find entry under cursor", vim.log.levels.ERROR) @@ -88,7 +102,7 @@ M.preview = { return end end - oil.open_preview() + oil.open_preview(opts) end, } @@ -129,7 +143,16 @@ M.parent = { M.close = { desc = "Close oil and restore original buffer", - callback = oil.close, + callback = function(opts) + opts = opts or {} + oil.close(opts) + end, + parameters = { + exit_if_last_buf = { + type = "boolean", + desc = "Exit vim if oil is closed as the last buffer", + }, + }, } ---@param cmd string @@ -352,7 +375,11 @@ M.yank_entry = { if not entry or not dir then return end - local path = dir .. entry.name + local name = entry.name + if entry.type == "directory" then + name = name .. "/" + end + local path = dir .. name if opts.modify then path = vim.fn.fnamemodify(path, opts.modify) end diff --git a/lua/oil/adapters/files/permissions.lua b/lua/oil/adapters/files/permissions.lua index cf50b551..6c306a64 100644 --- a/lua/oil/adapters/files/permissions.lua +++ b/lua/oil/adapters/files/permissions.lua @@ -1,6 +1,6 @@ local M = {} ----@param exe_modifier nil|false|string +---@param exe_modifier false|string ---@param num integer ---@return string local function perm_to_str(exe_modifier, num) diff --git a/lua/oil/columns.lua b/lua/oil/columns.lua index 40b7d745..d8827922 100644 --- a/lua/oil/columns.lua +++ b/lua/oil/columns.lua @@ -19,6 +19,7 @@ local all_columns = {} ---@field render_action? fun(action: oil.ChangeAction): string ---@field perform_action? fun(action: oil.ChangeAction, callback: fun(err: nil|string)) ---@field get_sort_value? fun(entry: oil.InternalEntry): number|string +---@field create_sort_value_factory? fun(num_entries: integer): fun(entry: oil.InternalEntry): number|string ---@param name string ---@param column oil.ColumnDefinition @@ -292,18 +293,31 @@ M.register("name", { error("Do not use the name column. It is for sorting only") end, - get_sort_value = function(entry) - local sort_value = entry[FIELD_NAME] - - if config.view_options.natural_order then - sort_value = sort_value:gsub("%d+", pad_number) - end - - if config.view_options.case_insensitive then - sort_value = sort_value:lower() + create_sort_value_factory = function(num_entries) + if + config.view_options.natural_order == false + or (config.view_options.natural_order == "fast" and num_entries > 5000) + then + if config.view_options.case_insensitive then + return function(entry) + return entry[FIELD_NAME]:lower() + end + else + return function(entry) + return entry[FIELD_NAME] + end + end + else + if config.view_options.case_insensitive then + return function(entry) + return entry[FIELD_NAME]:gsub("%d+", pad_number):lower() + end + else + return function(entry) + return entry[FIELD_NAME]:gsub("%d+", pad_number) + end + end end - - return sort_value end, }) diff --git a/lua/oil/config.lua b/lua/oil/config.lua index 5ccde173..0374e43b 100644 --- a/lua/oil/config.lua +++ b/lua/oil/config.lua @@ -84,15 +84,16 @@ local default_config = { show_hidden = false, -- This function defines what is considered a "hidden" file is_hidden_file = function(name, bufnr) - return vim.startswith(name, ".") + local m = name:match("^%.") + return m ~= nil end, -- This function defines what will never be shown, even when `show_hidden` is set is_always_hidden = function(name, bufnr) return false end, - -- Sort file names in a more intuitive order for humans. Is less performant, - -- so you may want to set to false if you work with large directories. - natural_order = true, + -- Sort file names with numbers in a more intuitive order for humans. + -- Can be "fast", true, or false. "fast" will turn it off for large directories. + natural_order = "fast", -- Sort file and directory names case insensitive case_insensitive = false, sort = { @@ -101,6 +102,10 @@ local default_config = { { "type", "asc" }, { "name", "asc" }, }, + -- Customize the highlight group for the file name + highlight_filename = function(entry, is_hidden, is_link_target, is_link_orphan) + return nil + end, }, -- Extra arguments to pass to SCP when moving/copying files over SSH extra_scp_args = {}, @@ -137,8 +142,21 @@ local default_config = { return conf end, }, - -- Configuration for the actions floating preview window - preview = { + -- Configuration for the file preview window + preview_win = { + -- Whether the preview window is automatically updated when the cursor is moved + update_on_cursor_moved = true, + -- How to open the preview window "load"|"scratch"|"fast_scratch" + preview_method = "fast_scratch", + -- A function that returns true to disable preview on a file e.g. to avoid lag + disable_preview = function(filename) + return false + end, + -- Window-local options to use for preview window buffers + win_options = {}, + }, + -- Configuration for the floating action confirmation window + confirmation = { -- Width dimensions can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) -- min_width and max_width can be a single value or a list of mixed integer/float types. -- max_width = {100, 0.8} means "the lesser of 100 columns or 80% of total" @@ -159,8 +177,6 @@ local default_config = { win_options = { winblend = 0, }, - -- Whether the preview window is automatically updated when the cursor is moved - update_on_cursor_moved = true, }, -- Configuration for the floating progress window progress = { @@ -195,6 +211,9 @@ default_config.adapters = { ["oil-trash://"] = "trash", } default_config.adapter_aliases = {} +-- We want the function in the default config for documentation generation, but if we nil it out +-- here we can get some performance wins +default_config.view_options.highlight_filename = nil ---@class oil.Config ---@field adapters tableHidden from SetupOpts @@ -218,7 +237,8 @@ default_config.adapter_aliases = {} ---@field extra_scp_args string[] ---@field git oil.GitOptions ---@field float oil.FloatWindowConfig ----@field preview oil.PreviewWindowConfig +---@field preview_win oil.PreviewWindowConfig +---@field confirmation oil.ConfirmationWindowConfig ---@field progress oil.ProgressWindowConfig ---@field ssh oil.SimpleWindowConfig ---@field keymaps_help oil.SimpleWindowConfig @@ -245,7 +265,8 @@ local M = {} ---@field extra_scp_args? string[] Extra arguments to pass to SCP when moving/copying files over SSH ---@field git? oil.SetupGitOptions EXPERIMENTAL support for performing file operations with git ---@field float? oil.SetupFloatWindowConfig Configuration for the floating window in oil.open_float ----@field preview? oil.SetupPreviewWindowConfig Configuration for the actions floating preview window +---@field preview_win? oil.SetupPreviewWindowConfig Configuration for the file preview window +---@field confirmation? oil.SetupConfirmationWindowConfig Configuration for the floating action confirmation window ---@field progress? oil.SetupProgressWindowConfig Configuration for the floating progress window ---@field ssh? oil.SetupSimpleWindowConfig Configuration for the floating SSH window ---@field keymaps_help? oil.SetupSimpleWindowConfig Configuration for the floating keymaps help window @@ -264,17 +285,19 @@ local M = {} ---@field show_hidden boolean ---@field is_hidden_file fun(name: string, bufnr: integer): boolean ---@field is_always_hidden fun(name: string, bufnr: integer): boolean ----@field natural_order boolean +---@field natural_order boolean|"fast" ---@field case_insensitive boolean ---@field sort oil.SortSpec[] +---@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean): string|nil ---@class (exact) oil.SetupViewOptions ---@field show_hidden? boolean Show files and directories that start with "." ---@field is_hidden_file? fun(name: string, bufnr: integer): boolean This function defines what is considered a "hidden" file ---@field is_always_hidden? fun(name: string, bufnr: integer): boolean This function defines what will never be shown, even when `show_hidden` is set ----@field natural_order? boolean Sort file names in a more intuitive order for humans. Is less performant, so you may want to set to false if you work with large directories. +---@field natural_order? boolean|"fast" Sort file names with numbers in a more intuitive order for humans. Can be slow for large directories. ---@field case_insensitive? boolean Sort file and directory names case insensitive ---@field sort? oil.SortSpec[] Sort order for the file list +---@field highlight_filename? fun(entry: oil.Entry, is_hidden: boolean, is_link_target: boolean, is_link_orphan: boolean): string|nil Customize the highlight group for the file name ---@class (exact) oil.SortSpec ---@field [1] string @@ -316,11 +339,26 @@ local M = {} ---@field border? string|string[] Window border ---@field win_options? table ----@class (exact) oil.PreviewWindowConfig : oil.WindowConfig +---@alias oil.PreviewMethod +---| '"load"' # Load the previewed file into a buffer +---| '"scratch"' # Put the text into a scratch buffer to avoid LSP attaching +---| '"fast_scratch"' # Put only the visible text into a scratch buffer + +---@class (exact) oil.PreviewWindowConfig ---@field update_on_cursor_moved boolean +---@field preview_method oil.PreviewMethod +---@field disable_preview fun(filename: string): boolean +---@field win_options table + +---@class (exact) oil.ConfirmationWindowConfig : oil.WindowConfig ----@class (exact) oil.SetupPreviewWindowConfig : oil.SetupWindowConfig +---@class (exact) oil.SetupPreviewWindowConfig ---@field update_on_cursor_moved? boolean Whether the preview window is automatically updated when the cursor is moved +---@field disable_preview? fun(filename: string): boolean A function that returns true to disable preview on a file e.g. to avoid lag +---@field preview_method? oil.PreviewMethod How to open the preview window +---@field win_options? table Window-local options to use for preview window buffers + +---@class (exact) oil.SetupConfirmationWindowConfig : oil.SetupWindowConfig ---@class (exact) oil.ProgressWindowConfig : oil.WindowConfig ---@field minimized_border string|string[] @@ -355,9 +393,26 @@ local M = {} ---@field border? string|string[] Window border M.setup = function(opts) - local new_conf = vim.tbl_deep_extend("keep", opts or {}, default_config) + opts = opts or {} + + local new_conf = vim.tbl_deep_extend("keep", opts, default_config) if not new_conf.use_default_keymaps then new_conf.keymaps = opts.keymaps or {} + elseif opts.keymaps then + -- We don't want to deep merge the keymaps, we want any keymap defined by the user to override + -- everything about the default. + for k, v in pairs(opts.keymaps) do + new_conf.keymaps[k] = v + end + end + + -- Backwards compatibility. We renamed the 'preview' window config to be called 'confirmation'. + if opts.preview and not opts.confirmation then + new_conf.confirmation = vim.tbl_deep_extend("keep", opts.preview, default_config.confirmation) + end + -- Backwards compatibility. We renamed the 'preview' config to 'preview_win' + if opts.preview and opts.preview.update_on_cursor_moved ~= nil then + new_conf.preview_win.update_on_cursor_moved = opts.preview.update_on_cursor_moved end if new_conf.lsp_rename_autosave ~= nil then diff --git a/lua/oil/init.lua b/lua/oil/init.lua index 06281668..39ee7537 100644 --- a/lua/oil/init.lua +++ b/lua/oil/init.lua @@ -379,8 +379,13 @@ M.open = function(dir) update_preview_window() end +---@class oil.CloseOpts +---@field exit_if_last_buf? boolean Exit vim if this oil buffer is the last open buffer + ---Restore the buffer that was present when oil was opened -M.close = function() +---@param opts? oil.CloseOpts +M.close = function(opts) + opts = opts or {} local mode = vim.api.nvim_get_mode().mode -- If we're in operator pending or visual modes, we should cancel that operation and return if mode == "no" or mode == "v" or mode == "V" then @@ -388,7 +393,6 @@ M.close = function() vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes(" ", true, true, true), "n", false) return end - -- If we're in a floating oil window, close it and try to restore focus to the original window if vim.w.is_oil_win then local original_winid = vim.w.oil_original_win @@ -411,18 +415,26 @@ M.close = function() -- buffer first local oilbuf = vim.api.nvim_get_current_buf() ok = pcall(vim.cmd.bprev) + -- If `bprev` failed, there are no buffers open if not ok then - -- If `bprev` failed, there are no buffers open so we should create a new one with enew - vim.cmd.enew() + -- either exit or create a new blank buffer + if opts.exit_if_last_buf then + vim.cmd.quit() + else + vim.cmd.enew() + end end vim.api.nvim_buf_delete(oilbuf, { force = true }) end +---@class oil.OpenPreviewOpts +---@field vertical? boolean Open the buffer in a vertical split +---@field horizontal? boolean Open the buffer in a horizontal split +---@field split? "aboveleft"|"belowright"|"topleft"|"botright" Split modifier + ---Preview the entry under the cursor in a split ----@param opts nil|table ---- vertical boolean Open the buffer in a vertical split ---- horizontal boolean Open the buffer in a horizontal split ---- split "aboveleft"|"belowright"|"topleft"|"botright" Split modifier +---@param opts? oil.OpenPreviewOpts +---@param callback? fun(err: nil|string) Called once the preview window has been opened M.open_preview = function(opts, callback) opts = opts or {} local config = require("oil.config") @@ -527,14 +539,28 @@ M.open_preview = function(opts, callback) end end - local filebufnr = vim.fn.bufadd(normalized_url) local entry_is_file = not vim.endswith(normalized_url, "/") - - -- If we're previewing a file that hasn't been opened yet, make sure it gets deleted after - -- we close the window - if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then + local filebufnr + if + entry_is_file + and config.preview_win.preview_method ~= "load" + and not util.file_matches_bufreadcmd(normalized_url) + then + filebufnr = + util.read_file_to_scratch_buffer(normalized_url, config.preview_win.preview_method) + elseif entry_is_file and config.preview_win.disable_preview(normalized_url) then + filebufnr = vim.api.nvim_create_buf(false, true) vim.bo[filebufnr].bufhidden = "wipe" - vim.b[filebufnr].oil_preview_buffer = true + vim.bo[filebufnr].buftype = "nofile" + util.render_text(filebufnr, "Preview disabled", { winid = preview_win }) + end + + if not filebufnr then + filebufnr = vim.fn.bufadd(normalized_url) + if entry_is_file and vim.fn.bufloaded(filebufnr) == 0 then + vim.bo[filebufnr].bufhidden = "wipe" + vim.b[filebufnr].oil_preview_buffer = true + end end ---@diagnostic disable-next-line: param-type-mismatch @@ -549,6 +575,9 @@ M.open_preview = function(opts, callback) end vim.api.nvim_set_option_value("previewwindow", true, { scope = "local", win = 0 }) + for k, v in pairs(config.preview_win.win_options) do + vim.api.nvim_set_option_value(k, v, { scope = "local", win = preview_win }) + end vim.w.oil_entry_id = entry.id vim.w.oil_source_win = prev_win if is_visual_mode then @@ -774,11 +803,21 @@ end ---@private M._get_highlights = function() return { + { + name = "OilHidden", + link = "Comment", + desc = "Hidden entry in an oil buffer", + }, { name = "OilDir", link = "Directory", desc = "Directory names in an oil buffer", }, + { + name = "OilDirHidden", + link = "OilHidden", + desc = "Hidden directory names in an oil buffer", + }, { name = "OilDirIcon", link = "OilDir", @@ -789,21 +828,61 @@ M._get_highlights = function() link = "Keyword", desc = "Socket files in an oil buffer", }, + { + name = "OilSocketHidden", + link = "OilHidden", + desc = "Hidden socket files in an oil buffer", + }, { name = "OilLink", link = nil, desc = "Soft links in an oil buffer", }, + { + name = "OilOrphanLink", + link = nil, + desc = "Orphaned soft links in an oil buffer", + }, + { + name = "OilLinkHidden", + link = "OilHidden", + desc = "Hidden soft links in an oil buffer", + }, + { + name = "OilOrphanLinkHidden", + link = "OilLinkHidden", + desc = "Hidden orphaned soft links in an oil buffer", + }, { name = "OilLinkTarget", link = "Comment", desc = "The target of a soft link", }, + { + name = "OilOrphanLinkTarget", + link = "DiagnosticError", + desc = "The target of an orphaned soft link", + }, + { + name = "OilLinkTargetHidden", + link = "OilHidden", + desc = "The target of a hidden soft link", + }, + { + name = "OilOrphanLinkTargetHidden", + link = "OilOrphanLinkTarget", + desc = "The target of an hidden orphaned soft link", + }, { name = "OilFile", link = nil, desc = "Normal files in an oil buffer", }, + { + name = "OilFileHidden", + link = "OilHidden", + desc = "Hidden normal files in an oil buffer", + }, { name = "OilCreate", link = "DiagnosticInfo", diff --git a/lua/oil/keymap_util.lua b/lua/oil/keymap_util.lua index b94581cb..b62756eb 100644 --- a/lua/oil/keymap_util.lua +++ b/lua/oil/keymap_util.lua @@ -78,31 +78,30 @@ M.show_help = function(keymaps) end end - local col_left = {} - local col_desc = {} local max_lhs = 1 + local keymap_entries = {} for k, rhs in pairs(keymaps) do local all_lhs = lhs_to_all_lhs[k] if all_lhs then local _, opts = resolve(rhs) local keystr = table.concat(all_lhs, "/") max_lhs = math.max(max_lhs, vim.api.nvim_strwidth(keystr)) - table.insert(col_left, { str = keystr, all_lhs = all_lhs }) - table.insert(col_desc, opts.desc or "") + table.insert(keymap_entries, { str = keystr, all_lhs = all_lhs, desc = opts.desc or "" }) end end + table.sort(keymap_entries, function(a, b) + return a.desc < b.desc + end) local lines = {} local highlights = {} local max_line = 1 - for i = 1, #col_left do - local left = col_left[i] - local desc = col_desc[i] - local line = string.format(" %s %s", util.rpad(left.str, max_lhs), desc) + for _, entry in ipairs(keymap_entries) do + local line = string.format(" %s %s", util.rpad(entry.str, max_lhs), entry.desc) max_line = math.max(max_line, vim.api.nvim_strwidth(line)) table.insert(lines, line) local start = 1 - for _, key in ipairs(left.all_lhs) do + for _, key in ipairs(entry.all_lhs) do local keywidth = vim.api.nvim_strwidth(key) table.insert(highlights, { "Special", #lines, start, start + keywidth }) start = start + keywidth + 1 diff --git a/lua/oil/layout.lua b/lua/oil/layout.lua index f22d26af..8ed7b4e6 100644 --- a/lua/oil/layout.lua +++ b/lua/oil/layout.lua @@ -182,6 +182,11 @@ M.split_window = function(winid, direction, gap) return dim_root, dim_new end +---@param desired_width integer +---@param desired_height integer +---@param opts table +---@return integer width +---@return integer height M.calculate_dims = function(desired_width, desired_height, opts) local width = M.calculate_width(desired_width, opts) local height = M.calculate_height(desired_height, opts) diff --git a/lua/oil/mutator/preview.lua b/lua/oil/mutator/confirmation.lua similarity index 97% rename from lua/oil/mutator/preview.lua rename to lua/oil/mutator/confirmation.lua index 3f8d87dc..4e86abc3 100644 --- a/lua/oil/mutator/preview.lua +++ b/lua/oil/mutator/confirmation.lua @@ -91,7 +91,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) table.insert(lines, "") -- Create the floating window - local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.preview) + local width, height = layout.calculate_dims(max_line_width, #lines + 1, config.confirmation) local ok, winid = pcall(vim.api.nvim_open_win, bufnr, true, { relative = "editor", width = width, @@ -100,7 +100,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) col = math.floor((layout.get_editor_width() - width) / 2), zindex = 152, -- render on top of the floating window title style = "minimal", - border = config.preview.border, + border = config.confirmation.border, }) if not ok then vim.notify(string.format("Error showing oil preview window: %s", winid), vim.log.levels.ERROR) @@ -108,7 +108,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) end vim.bo[bufnr].filetype = "oil_preview" vim.bo[bufnr].syntax = "oil_preview" - for k, v in pairs(config.preview.win_options) do + for k, v in pairs(config.confirmation.win_options) do vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) end @@ -155,7 +155,7 @@ M.show = vim.schedule_wrap(function(actions, should_confirm, cb) vim.api.nvim_create_autocmd("VimResized", { callback = function() if vim.api.nvim_win_is_valid(winid) then - width, height = layout.calculate_dims(max_line_width, #lines, config.preview) + width, height = layout.calculate_dims(max_line_width, #lines, config.confirmation) vim.api.nvim_win_set_config(winid, { relative = "editor", width = width, diff --git a/lua/oil/mutator/init.lua b/lua/oil/mutator/init.lua index 8dfd4b05..f15c0692 100644 --- a/lua/oil/mutator/init.lua +++ b/lua/oil/mutator/init.lua @@ -3,12 +3,12 @@ local Trie = require("oil.mutator.trie") local cache = require("oil.cache") local columns = require("oil.columns") local config = require("oil.config") +local confirmation = require("oil.mutator.confirmation") local constants = require("oil.constants") local fs = require("oil.fs") local lsp_helpers = require("oil.lsp.helpers") local oil = require("oil") local parser = require("oil.mutator.parser") -local preview = require("oil.mutator.preview") local util = require("oil.util") local view = require("oil.view") local M = {} @@ -564,7 +564,7 @@ M.try_write_changes = function(confirm, cb) end local actions = M.create_actions_from_diffs(all_diffs) - preview.show(actions, confirm, function(proceed) + confirmation.show(actions, confirm, function(proceed) if not proceed then unlock() cb("Canceled") diff --git a/lua/oil/shell.lua b/lua/oil/shell.lua index 2c401efc..b04b27be 100644 --- a/lua/oil/shell.lua +++ b/lua/oil/shell.lua @@ -26,7 +26,8 @@ M.run = function(cmd, opts, callback) if err == "" then err = "Unknown error" end - callback(err) + local cmd_str = type(cmd) == "string" and cmd or table.concat(cmd, " ") + callback(string.format("Error running command '%s'\n%s", cmd_str, err)) end end), }) diff --git a/lua/oil/util.lua b/lua/oil/util.lua index f5c63e2a..441421b9 100644 --- a/lua/oil/util.lua +++ b/lua/oil/util.lua @@ -347,7 +347,8 @@ M.addslash = function(path, os_slash) slash = "\\" end - if not vim.endswith(path, slash) then + local endslash = path:match(slash .. "$") + if not endslash then return path .. slash else return path @@ -896,4 +897,81 @@ M.get_icon_provider = function() end end +---Read a buffer into a scratch buffer and apply syntactic highlighting when possible +---@param path string The path to the file to read +---@param preview_method oil.PreviewMethod +---@return nil|integer +M.read_file_to_scratch_buffer = function(path, preview_method) + local bufnr = vim.api.nvim_create_buf(false, true) + if bufnr == 0 then + return + end + + vim.bo[bufnr].bufhidden = "wipe" + vim.bo[bufnr].buftype = "nofile" + + local max_lines = preview_method == "fast_scratch" and vim.o.lines or nil + local has_lines, read_res = pcall(vim.fn.readfile, path, "", max_lines) + local lines = has_lines and vim.split(table.concat(read_res, "\n"), "\n") or {} + + local ok = pcall(vim.api.nvim_buf_set_lines, bufnr, 0, -1, false, lines) + if not ok then + return + end + local ft = vim.filetype.match({ filename = path, buf = bufnr }) + if ft and ft ~= "" then + local lang = vim.treesitter.language.get_lang(ft) + if not pcall(vim.treesitter.start, bufnr, lang) then + vim.bo[bufnr].syntax = ft + else + end + end + + -- Replace the scratch buffer with a real buffer if we enter it + vim.api.nvim_create_autocmd("BufEnter", { + desc = "oil.nvim replace scratch buffer with real buffer", + buffer = bufnr, + callback = function() + local winid = vim.api.nvim_get_current_win() + -- Have to schedule this so all the FileType, etc autocmds will fire + vim.schedule(function() + if vim.api.nvim_get_current_win() == winid then + vim.cmd.edit({ args = { path } }) + + -- If we're still in a preview window, make sure this buffer still gets treated as a + -- preview + if vim.wo.previewwindow then + vim.bo.bufhidden = "wipe" + vim.b.oil_preview_buffer = true + end + end + end) + end, + }) + + return bufnr +end + +local _regcache = {} +---Check if a file matches a BufReadCmd autocmd +---@param filename string +---@return boolean +M.file_matches_bufreadcmd = function(filename) + local autocmds = vim.api.nvim_get_autocmds({ + event = "BufReadCmd", + }) + for _, au in ipairs(autocmds) do + local pat = _regcache[au.pattern] + if not pat then + pat = vim.fn.glob2regpat(au.pattern) + _regcache[au.pattern] = pat + end + + if vim.fn.match(filename, pat) >= 0 then + return true + end + end + return false +end + return M diff --git a/lua/oil/view.lua b/lua/oil/view.lua index 89e4b8fc..15e50228 100644 --- a/lua/oil/view.lua +++ b/lua/oil/view.lua @@ -19,10 +19,16 @@ local last_cursor_entry = {} ---@param name string ---@param bufnr integer ----@return boolean +---@return boolean display +---@return boolean is_hidden Whether the file is classified as a hidden file M.should_display = function(name, bufnr) - return not config.view_options.is_always_hidden(name, bufnr) - and (config.view_options.show_hidden or not config.view_options.is_hidden_file(name, bufnr)) + if config.view_options.is_always_hidden(name, bufnr) then + return false, true + else + local is_hidden = config.view_options.is_hidden_file(name, bufnr) + local display = config.view_options.show_hidden or not is_hidden + return display, is_hidden + end end ---@param bufname string @@ -179,9 +185,18 @@ end M.set_win_options = function() local winid = vim.api.nvim_get_current_win() + + -- work around https://github.com/neovim/neovim/pull/27422 + vim.api.nvim_set_option_value("foldmethod", "manual", { scope = "local", win = winid }) + for k, v in pairs(config.win_options) do vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) end + if vim.wo[winid].previewwindow then -- apply preview window options last + for k, v in pairs(config.preview_win.win_options) do + vim.api.nvim_set_option_value(k, v, { scope = "local", win = winid }) + end + end end ---Get a list of visible oil buffers and a list of hidden oil buffers @@ -407,13 +422,13 @@ M.initialize = function(bufnr) constrain_cursor() - if config.preview.update_on_cursor_moved then + if config.preview_win.update_on_cursor_moved then -- Debounce and update the preview window if timer then timer:again() return end - timer = vim.loop.new_timer() + timer = uv.new_timer() if not timer then return end @@ -532,8 +547,9 @@ M.initialize = function(bufnr) end ---@param adapter oil.Adapter +---@param num_entries integer ---@return fun(a: oil.InternalEntry, b: oil.InternalEntry): boolean -local function get_sort_function(adapter) +local function get_sort_function(adapter, num_entries) local idx_funs = {} local sort_config = config.view_options.sort @@ -555,7 +571,9 @@ local function get_sort_function(adapter) ) end local col = columns.get_column(adapter, col_name) - if col and col.get_sort_value then + if col and col.create_sort_value_factory then + table.insert(idx_funs, { col.create_sort_value_factory(num_entries), order }) + elseif col and col.get_sort_value then table.insert(idx_funs, { col.get_sort_value, order }) else vim.notify_once( @@ -606,7 +624,10 @@ local function render_buffer(bufnr, opts) local entries = cache.list_url(bufname) local entry_list = vim.tbl_values(entries) - table.sort(entry_list, get_sort_function(adapter)) + -- Only sort the entries once we have them all + if not vim.b[bufnr].oil_rendering then + table.sort(entry_list, get_sort_function(adapter, #entry_list)) + end local jump_idx if opts.jump_first then @@ -622,20 +643,21 @@ local function render_buffer(bufnr, opts) end if M.should_display("..", bufnr) then - local cols = M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter) + local cols = + M.format_entry_cols({ 0, "..", "directory" }, column_defs, col_width, adapter, true) table.insert(line_table, cols) end for _, entry in ipairs(entry_list) do - if M.should_display(entry[FIELD_NAME], bufnr) then - local cols = M.format_entry_cols(entry, column_defs, col_width, adapter) + local should_display, is_hidden = M.should_display(entry[FIELD_NAME], bufnr) + if should_display then + local cols = M.format_entry_cols(entry, column_defs, col_width, adapter, is_hidden) table.insert(line_table, cols) local name = entry[FIELD_NAME] if seek_after_render == name then seek_after_render_found = true jump_idx = #line_table - M.set_last_cursor(bufname, nil) end end end @@ -673,15 +695,42 @@ local function render_buffer(bufnr, opts) return seek_after_render_found end +---@param name string +---@param meta? table +---@return string filename +---@return string|nil link_target +local function get_link_text(name, meta) + local link_text + if meta then + if meta.link_stat and meta.link_stat.type == "directory" then + name = name .. "/" + end + + if meta.link then + link_text = "-> " .. meta.link + if meta.link_stat and meta.link_stat.type == "directory" then + link_text = util.addslash(link_text) + end + end + end + + return name, link_text +end + ---@private ---@param entry oil.InternalEntry ---@param column_defs table[] ---@param col_width integer[] ---@param adapter oil.Adapter +---@param is_hidden boolean ---@return oil.TextChunk[] -M.format_entry_cols = function(entry, column_defs, col_width, adapter) +M.format_entry_cols = function(entry, column_defs, col_width, adapter, is_hidden) local name = entry[FIELD_NAME] local meta = entry[FIELD_META] + local hl_suffix = "" + if is_hidden then + hl_suffix = "Hidden" + end if meta and meta.display_name then name = meta.display_name end @@ -700,32 +749,55 @@ M.format_entry_cols = function(entry, column_defs, col_width, adapter) end -- Always add the entry name at the end local entry_type = entry[FIELD_TYPE] - if entry_type == "directory" then - table.insert(cols, { name .. "/", "OilDir" }) - elseif entry_type == "socket" then - table.insert(cols, { name, "OilSocket" }) - elseif entry_type == "link" then - local link_text - if meta then - if meta.link_stat and meta.link_stat.type == "directory" then - name = name .. "/" + + local get_custom_hl = config.view_options.highlight_filename + local link_name, link_name_hl, link_target, link_target_hl + if get_custom_hl then + local external_entry = util.export_entry(entry) + + if entry_type == "link" then + link_name, link_target = get_link_text(name, meta) + local is_orphan = not (meta and meta.link_stat) + link_name_hl = get_custom_hl(external_entry, is_hidden, false, is_orphan) + + if link_target then + link_target_hl = get_custom_hl(external_entry, is_hidden, true, is_orphan) end - if meta.link then - link_text = "->" .. " " .. meta.link - if meta.link_stat and meta.link_stat.type == "directory" then - link_text = util.addslash(link_text) - end + -- intentional fallthrough + else + local hl = get_custom_hl(external_entry, is_hidden, false, false) + if hl then + table.insert(cols, { name, hl }) + return cols end end + end - table.insert(cols, { name, "OilLink" }) - if link_text then - table.insert(cols, { link_text, "OilLinkTarget" }) + if entry_type == "directory" then + table.insert(cols, { name .. "/", "OilDir" .. hl_suffix }) + elseif entry_type == "socket" then + table.insert(cols, { name, "OilSocket" .. hl_suffix }) + elseif entry_type == "link" then + if not link_name then + link_name, link_target = get_link_text(name, meta) + end + local is_orphan = not (meta and meta.link_stat) + if not link_name_hl then + link_name_hl = (is_orphan and "OilOrphanLink" or "OilLink") .. hl_suffix + end + table.insert(cols, { link_name, link_name_hl }) + + if link_target then + if not link_target_hl then + link_target_hl = (is_orphan and "OilOrphanLinkTarget" or "OilLinkTarget") .. hl_suffix + end + table.insert(cols, { link_target, link_target_hl }) end else - table.insert(cols, { name, "OilFile" }) + table.insert(cols, { name, "OilFile" .. hl_suffix }) end + return cols end @@ -814,6 +886,7 @@ M.render_buffer_async = function(bufnr, opts, callback) vim.b[bufnr].oil_rendering = false loading.set_loading(bufnr, false) render_buffer(bufnr, { jump = true }) + M.set_last_cursor(bufname, nil) vim.bo[bufnr].undolevels = vim.api.nvim_get_option_value("undolevels", { scope = "global" }) vim.bo[bufnr].modifiable = not buffers_locked and adapter.is_modifiable(bufnr) if callback then @@ -838,6 +911,7 @@ M.render_buffer_async = function(bufnr, opts, callback) end cache.begin_update_url(bufname) + local num_iterations = 0 adapter.list(bufname, get_used_columns(), function(err, entries, fetch_more) loading.set_loading(bufnr, false) if err then @@ -854,11 +928,13 @@ M.render_buffer_async = function(bufnr, opts, callback) local now = uv.hrtime() / 1e6 local delta = now - start_ms -- If we've been chugging for more than 40ms, go ahead and render what we have - if delta > 40 then + if (delta > 25 and num_iterations < 1) or delta > 500 then + num_iterations = num_iterations + 1 start_ms = now vim.schedule(function() seek_after_render_found = render_buffer(bufnr, { jump = not seek_after_render_found, jump_first = first }) + start_ms = uv.hrtime() / 1e6 end) end first = false diff --git a/perf/bootstrap.lua b/perf/bootstrap.lua new file mode 100644 index 00000000..5f10c065 --- /dev/null +++ b/perf/bootstrap.lua @@ -0,0 +1,63 @@ +vim.opt.runtimepath:prepend("scripts/benchmark.nvim") +vim.opt.runtimepath:prepend(".") + +local bm = require("benchmark") +bm.sandbox() + +---@module 'oil' +---@type oil.SetupOpts +local setup_opts = { + -- columns = { "icon", "permissions", "size", "mtime" }, +} + +local DIR_SIZE = tonumber(vim.env.DIR_SIZE) or 100000 +local ITERATIONS = tonumber(vim.env.ITERATIONS) or 10 +local WARM_UP = tonumber(vim.env.WARM_UP) or 1 +local OUTLIERS = tonumber(vim.env.OUTLIERS) or math.floor(ITERATIONS / 10) +local TEST_DIR = "perf/tmp/test_" .. DIR_SIZE + +vim.fn.mkdir(TEST_DIR, "p") +require("benchmark.files").create_files(TEST_DIR, "file %d.txt", DIR_SIZE) + +function _G.jit_profile() + require("oil").setup(setup_opts) + local finish = bm.jit_profile({ filename = TEST_DIR .. "/profile.txt" }) + bm.wait_for_user_event("OilEnter", function() + finish() + end) + require("oil").open(TEST_DIR) +end + +function _G.flame_profile() + local start, stop = bm.flame_profile({ + pattern = "oil*", + filename = "profile.json", + }) + require("oil").setup(setup_opts) + start() + bm.wait_for_user_event("OilEnter", function() + stop(function() + vim.cmd.qall({ mods = { silent = true } }) + end) + end) + require("oil").open(TEST_DIR) +end + +function _G.benchmark() + require("oil").setup(setup_opts) + bm.run({ title = "oil.nvim", iterations = ITERATIONS, warm_up = WARM_UP }, function(callback) + bm.wait_for_user_event("OilEnter", callback) + require("oil").open(TEST_DIR) + end, function(times) + local avg = bm.avg(times, { trim_outliers = OUTLIERS }) + local std_dev = bm.std_dev(times, { trim_outliers = OUTLIERS }) + local lines = { + table.concat(vim.tbl_map(bm.format_time, times), " "), + string.format("Average: %s", bm.format_time(avg)), + string.format("Std deviation: %s", bm.format_time(std_dev)), + } + + vim.fn.writefile(lines, "perf/tmp/benchmark.txt") + vim.cmd.qall({ mods = { silent = true } }) + end) +end diff --git a/scripts/generate.py b/scripts/generate.py index 5cc5fe35..a20ad532 100755 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -366,7 +366,7 @@ def get_trash_vimdoc() -> "VimdocSection": (instead of being able to see files that were trashed from a directory). Windows: - Oil does not yet support the Windows trash. PRs are welcome! + Oil supports the Windows Recycle Bin. All features should work. """ ) return section diff --git a/scripts/requirements.txt b/scripts/requirements.txt new file mode 100644 index 00000000..2c6271fd --- /dev/null +++ b/scripts/requirements.txt @@ -0,0 +1,4 @@ +pyparsing==3.0.9 +black +isort +mypy diff --git a/tests/parser_spec.lua b/tests/parser_spec.lua index 527e8217..9884ca1b 100644 --- a/tests/parser_spec.lua +++ b/tests/parser_spec.lua @@ -90,7 +90,7 @@ describe("parser", function() local file = test_adapter.test_set("/foo/a.txt", "file") vim.cmd.edit({ args = { "oil-test:///foo/" } }) local bufnr = vim.api.nvim_get_current_buf() - local cols = view.format_entry_cols(file, {}, {}, test_adapter) + local cols = view.format_entry_cols(file, {}, {}, test_adapter, false) local lines = util.render_table({ cols }, {}) table.insert(lines, "") table.insert(lines, " ")