Skip to content

Commit

Permalink
feat(lsp): add lsp file operations support (#400)
Browse files Browse the repository at this point in the history
* feat(lsp): add lsp file operations support

will/do create/rename/delete request and notification support for
related actions.

Only offered for nvim 0.10+.

* fix some renaming edgecases

* some readme cleanup

* update gendoc

* [docgen] Update doc/telescope-file-browser.txt
skip-checks: true

---------

Co-authored-by: Github Actions <actions@github>
  • Loading branch information
jamestrew and Github Actions authored Aug 2, 2024
1 parent 9e06f14 commit 19a0b7f
Show file tree
Hide file tree
Showing 4 changed files with 388 additions and 62 deletions.
86 changes: 65 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# telescope-file-browser.nvim

`telescope-file-browser.nvim` is a file browser extension for telescope.nvim. It supports synchronized creation, deletion, renaming, and moving of files and folders powered by [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) and [plenary.nvim](https://github.com/nvim-lua/plenary.nvim).
`telescope-file-browser.nvim` is a file browser extension for telescope.nvim.
It supports synchronized creation, deletion, renaming, and moving of files and
folders (with LSP integration with nvim 0.10+) powered by
[telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) and
[plenary.nvim](https://github.com/nvim-lua/plenary.nvim).

![Demo](https://user-images.githubusercontent.com/39233597/149016073-6fcc9383-a761-422b-be40-17d4b854cd3c.gif)
More demo examples can be found in the [showcase issue](https://github.com/nvim-telescope/telescope-file-browser.nvim/issues/53).
Expand Down Expand Up @@ -47,7 +51,7 @@ use {
<details>
<summary>vim-plug</summary>

```viml
```vim
Plug 'nvim-lua/plenary.nvim'
Plug 'nvim-telescope/telescope.nvim'
Plug 'nvim-telescope/telescope-file-browser.nvim'
Expand All @@ -57,7 +61,12 @@ Plug 'nvim-telescope/telescope-file-browser.nvim'

## Setup and Configuration

You can configure the `telescope-file-browser` like any other `telescope.nvim` picker. Please see `:h telescope-file-browser.picker` for the full set of options dedicated to the picker. Unless otherwise stated, you can pass these options either to your configuration at extension setup or picker startup. For instance, you can map `theme` and [mappings](#remappings) as you are used to from `telescope.nvim`.
You can configure the `telescope-file-browser` like any other `telescope.nvim`
picker. Please see `:h telescope-file-browser.picker` for the full set of
options dedicated to the picker. Unless otherwise stated, you can pass these
options either to your configuration at extension setup or picker startup. For
instance, you can map `theme` and [mappings](#remappings) as you are used to
from `telescope.nvim`.

```lua
-- You don't need to set any of these options.
Expand Down Expand Up @@ -179,10 +188,13 @@ end)

## Mappings

`telescope-file-browser.nvim` comes with a lot of default mappings for discoverability. You can use `telescope`'s `which_key` (insert mode: `<C-/>`, normal mode: `?`) to list mappings attached to your picker.
`telescope-file-browser.nvim` comes with a lot of default mappings for
discoverability. You can use `telescope`'s `which_key` (insert mode: `<C-/>`,
normal mode: `?`) to list mappings attached to your picker.

- `path` denotes the folder the `file_browser` is currently in
- `fb_actions` refers to the table of provided `telescope-file-browser.actions` accessible via `require "telescope".extensions.file_browser.actions`
- `fb_actions` refers to the table of provided `telescope-file-browser.actions`
accessible via `require "telescope".extensions.file_browser.actions`

| Insert / Normal | fb_actions | Description |
| --------------- | -------------------- | -------------------------------------------------------------------------------- |
Expand All @@ -208,25 +220,33 @@ end)

#### Remappings

As part of the [setup](#setup-and-configuration), you can remap actions as you like. The default mappings can also be found in this [file](https://github.com/nvim-telescope/telescope-file-browser.nvim/blob/master/lua/telescope/_extensions/file_browser.lua).
As part of the [setup](#setup-and-configuration), you can remap actions as you
like. The default mappings can also be found in this
[file](https://github.com/nvim-telescope/telescope-file-browser.nvim/blob/master/lua/telescope/_extensions/file_browser.lua).

```lua
local fb_actions = require "telescope".extensions.file_browser.actions
-- mappings in file_browser extension of telescope.setup
...

require("telescope").setup {
defaults = { --[[ your defaults]] },
pickers = {
file_browser = {
mappings = {
["i"] = {
-- remap to going to home directory
["<C-h>"] = fb_actions.goto_home_dir
["<C-h>"] = fb_actions.goto_home_dir,
["<C-x>"] = function(prompt_bufnr)
-- your custom function
end
end,
},
["n"] = {
-- unmap toggling `fb_actions.toggle_browser`
f = false,
},
...
},
},
},
}
```

## Documentation
Expand All @@ -251,23 +271,43 @@ Please make sure to consult the docs prior to raising issues for asking question
1. `file_browser`: finds files and folders in the (currently) selected folder (denoted as `path`, default: `cwd`)
2. `folder_browser`: swiftly fuzzy find folders from `cwd` downwards to switch folders for the `file_browser` (i.e. set `path` to selected folder)

Within a single session, `path` always refers to the folder the `file_browser` is currently in and changes by selecting folders from within the `file` or `folder_browser`.
Within a single session, `path` always refers to the folder the `file_browser`
is currently in and changes by selecting folders from within the `file` or
`folder_browser`.

If you want to open the `file_browser` from within the folder of your current buffer, you should pass `path = "%:p:h"` to the `opts` table of the picker (Vimscript: `:Telescope file_browser path=%:p:h`) or to the extension setup configuration. Strings passed to `path` or `cwd` are expanded automatically.
If you want to open the `file_browser` from within the folder of your current
buffer, you should pass `path = "%:p:h"` to the `opts` table of the picker
(Vimscript: `:Telescope file_browser path=%:p:h`) or to the extension setup
configuration. Strings passed to `path` or `cwd` are expanded automatically.

By default, the `folder_browser` always launches from `cwd`, but it can be configured to launch from `path` via passing the `cwd_to_path = true` to picker `opts` table or at extension setup. The former corresponds to a more project-centric file browser workflow, whereas the latter typically facilitates file and folder browsing across the entire file system.
By default, the `folder_browser` always launches from `cwd`, but it can be
configured to launch from `path` via passing the `cwd_to_path = true` to picker
`opts` table or at extension setup. The former corresponds to a more
project-centric file browser workflow, whereas the latter typically facilitates
file and folder browsing across the entire file system.

In practice, it mostly affects how you navigate the file system in multi-hop scenarios, for instance, when moving files from varying folders into a separate folder. The default works well in projects from which the `folder_browser` can easily reach any folder. `cwd_to_path = true` would possibly require returning to parent directories or `cwd` intermittently. However, if you move deeply through the file system, launching the `folder_browser` from `cwd` every time is tedious. Hence, it can be configured to follow `path` instead.
In practice, it mostly affects how you navigate the file system in multi-hop
scenarios, for instance, when moving files from varying folders into a separate
folder. The default works well in projects from which the `folder_browser` can
easily reach any folder. `cwd_to_path = true` would possibly require returning
to parent directories or `cwd` intermittently. However, if you move deeply
through the file system, launching the `folder_browser` from `cwd` every time
is tedious. Hence, it can be configured to follow `path` instead.

In general, `telescope-file-browser.nvim` intends to enable any workflow without comprise via opting in as virtually any component can be overriden.
In general, `telescope-file-browser.nvim` intends to enable any workflow
without comprise via opting in as virtually any component can be overriden.

## Multi-Selections

Multiple files and directories can be selected at the same time using default bindings (`<Tab>`/`<S-Tab>`) from `telescope.nvim`.
Multiple files and directories can be selected at the same time using default
bindings (`<Tab>`/`<S-Tab>`) from `telescope.nvim`.

One distinct difference to `telescope.nvim` is that multi-selections are preserved between browsers.
One distinct difference to `telescope.nvim` is that multi-selections are
preserved between browsers.

Hence, whenever you (de-)select a file or folder within `{file, folder}_browser`, respectively, this change persists across browsers (in a single session).
Hence, whenever you (de-)select a file or folder within `{file,
folder}_browser`, respectively, this change persists across browsers (in a
single session).

## File System Operations

Expand Down Expand Up @@ -302,6 +342,10 @@ The extension exports the following attributes via `:lua require "telescope".ext

# Roadmap & Contributing

Please see the associated [issue](https://github.com/nvim-telescope/telescope-file-browser.nvim/issues/3) on more immediate open `TODOs` for `telescope-file-browser.nvim`.
Please see the associated
[issue](https://github.com/nvim-telescope/telescope-file-browser.nvim/issues/3)
on more immediate open `TODOs` for `telescope-file-browser.nvim`.

That said, the primary work surrounds on enabling users to tailor the extension to their individual workflow, primarily through opting in and possibly overriding specific components.
That said, the primary work surrounds on enabling users to tailor the extension
to their individual workflow, primarily through opting in and possibly
overriding specific components.
100 changes: 64 additions & 36 deletions lua/telescope/_extensions/file_browser/actions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
local a = vim.api

local fb_utils = require "telescope._extensions.file_browser.utils"
local fb_lsp = require "telescope._extensions.file_browser.lsp"

local actions = require "telescope.actions"
local state = require "telescope.state"
Expand Down Expand Up @@ -78,11 +79,17 @@ local create = function(file, finder)
fb_utils.notify("actions.create", { msg = "Selection already exists!", level = "WARN", quiet = finder.quiet })
return
end

local filename = file:absolute()
fb_lsp.will_create_files { filename }

if not fb_utils.is_dir(file.filename) then
file:touch { parents = true }
else
Path:new(file.filename:sub(1, -2)):mkdir { parents = true, mode = 493 } -- 493 => decimal for mode 0755
end

fb_lsp.did_create_files { filename }
return file
end

Expand Down Expand Up @@ -173,6 +180,31 @@ fb_actions.create_from_prompt = function(prompt_bufnr)
end
end

local rename_au_group = a.nvim_create_augroup("TelescopeBatchRename", { clear = true })

---@param path_map table table<Path, Path> of old -> new
local function rename_files(path_map)
local str_map = {} ---@type table<string, string>
for old, new in pairs(path_map) do
str_map[old:absolute()] = new:absolute()
end

fb_lsp.will_rename_files(str_map)

for old, new in pairs(path_map) do
local old_name = old:absolute()
local new_name = new:absolute()
old:rename { new_name = new_name }
if new:is_dir() then
fb_utils.rename_dir_buf(old_name, new_name)
else
fb_utils.rename_buf(old_name, new_name)
end
end

fb_lsp.did_rename_files(str_map)
end

local batch_rename = function(prompt_bufnr, selections)
local current_picker = action_state.get_current_picker(prompt_bufnr)
local prompt_win = a.nvim_get_current_win()
Expand Down Expand Up @@ -208,42 +240,40 @@ local batch_rename = function(prompt_bufnr, selections)
})
end

_G.__TelescopeBatchRename = function()
local _batch_rename = function()
local lines = a.nvim_buf_get_lines(buf, 0, -1, false)
assert(#lines == #what, "Keep a line unchanged if you do not want to rename")
local path_map = {}
for idx, file in ipairs(lines) do
local old_path = selections[idx]:absolute()
local new_path = Path:new(file):absolute()
if old_path ~= new_path then
local is_dir = selections[idx]:is_dir()
selections[idx]:rename { new_name = new_path }
if not is_dir then
fb_utils.rename_buf(old_path, new_path)
else
fb_utils.rename_dir_buf(old_path, new_path)
end
local old = selections[idx]
local new = Path:new(file)
if old.filename ~= new.filename then
path_map[old] = new
end
end
rename_files(path_map)
a.nvim_set_current_win(prompt_win)
current_picker:refresh(current_picker.finder, { reset_prompt = true })
end

local set_bkm = a.nvim_buf_set_keymap
local opts = { noremap = true, silent = true }
set_bkm(buf, "n", "<ESC>", string.format("<cmd>lua vim.api.nvim_set_current_win(%s)<CR>", prompt_win), opts)
set_bkm(buf, "i", "<C-c>", string.format("<cmd>lua vim.api.nvim_set_current_win(%s)<CR>", prompt_win), opts)
set_bkm(buf, "n", "<CR>", "<cmd>lua _G.__TelescopeBatchRename()<CR>", opts)
set_bkm(buf, "i", "<CR>", "<cmd>lua _G.__TelescopeBatchRename()<CR>", opts)

vim.cmd(string.format(
"autocmd BufLeave <buffer> ++once lua %s",
table.concat({
string.format("_G.__TelescopeBatchRename = nil", win),
string.format("pcall(vim.api.nvim_win_close, %s, true)", win),
string.format("pcall(vim.api.nvim_win_close, %s, true)", win_opts.border.win_id),
string.format("require 'telescope.utils'.buf_delete(%s)", buf),
}, ";")
))
local opts = { noremap = true, silent = true, buffer = buf }
-- stylua: ignore start
vim.keymap.set("n", "<ESC>", function() a.nvim_set_current_win(prompt_win) end, opts)
vim.keymap.set("i", "<C-c>", function() a.nvim_set_current_win(prompt_win) end, opts)
-- stylua: ignore end
vim.keymap.set("n", "<CR>", _batch_rename, opts)
vim.keymap.set("i", "<C-c>", _batch_rename, opts)

a.nvim_create_autocmd("BufLeave", {
once = true,
callback = function()
pcall(a.nvim_win_close, win, true)
pcall(a.nvim_win_close, win_opts.border.win_id, true)
require("telescope.utils").buf_delete(buf)
end,
group = rename_au_group,
buffer = buf,
})
end

--- Rename files or folders for |telescope-file-browser.picker.file_browser|.
Expand Down Expand Up @@ -301,15 +331,7 @@ fb_actions.rename = function(prompt_bufnr)
return
end

-- rename changes old_name in place
local old_name = old_path:absolute()

old_path:rename { new_name = new_path.filename }
if not new_path:is_dir() then
fb_utils.rename_buf(old_name, new_path:absolute())
else
fb_utils.rename_dir_buf(old_name, new_path:absolute())
end
rename_files { [old_path] = new_path }

-- persist multi selections unambiguously by only removing renamed entry
if current_picker:is_multi_selected(entry) then
Expand Down Expand Up @@ -519,6 +541,8 @@ fb_actions.remove = function(prompt_bufnr)
get_confirmation({ prompt = "Remove selection? (" .. #files .. " items)" }, function(confirmed)
vim.cmd [[ redraw ]] -- redraw to clear out vim.ui.prompt to avoid hit-enter prompt
if confirmed then
fb_lsp.will_delete_files(files)

for _, p in ipairs(selections) do
local is_dir = p:is_dir()
p:rm { recursive = is_dir }
Expand All @@ -530,6 +554,8 @@ fb_actions.remove = function(prompt_bufnr)
end
table.insert(removed, p.filename:sub(#p:parent().filename + 2))
end

fb_lsp.did_delete_files(files)
fb_utils.notify(
"actions.remove",
{ msg = "Removed: " .. table.concat(removed, ", "), level = "INFO", quiet = quiet }
Expand All @@ -551,8 +577,10 @@ fb_actions.toggle_hidden = function(prompt_bufnr)
finder.hidden = not finder.hidden
else
if finder.files then
---@diagnostic disable-next-line: inject-field
finder.hidden.file_browser = not finder.hidden.file_browser
else
---@diagnostic disable-next-line: inject-field
finder.hidden.folder_browser = not finder.hidden.folder_browser
end
end
Expand Down
Loading

0 comments on commit 19a0b7f

Please sign in to comment.