├── .editorconfig
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── config.yml
│ └── feature_request.yml
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── markdownlint.yml
│ ├── nvim-type-check.yml
│ ├── panvimdoc.yml
│ ├── pr-title.yml
│ ├── stale-bot.yml
│ └── stylua.yml
├── .gitignore
├── .ignore
├── .luarc.json
├── .markdownlint.yaml
├── .stylua.toml
├── Justfile
├── LICENSE
├── README.md
├── doc
└── nvim-genghis.txt
├── lua
└── genghis
│ ├── config.lua
│ ├── init.lua
│ ├── operations
│ ├── copy.lua
│ ├── file.lua
│ └── navigation.lua
│ └── support
│ ├── lsp-rename.lua
│ └── utils.lua
└── plugin
└── ex-commands.lua
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | max_line_length = 100
5 | end_of_line = lf
6 | charset = utf-8
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 3
10 | tab_width = 3
11 | trim_trailing_whitespace = true
12 |
13 | [*.{yml,yaml,scm}]
14 | indent_style = space
15 | indent_size = 2
16 | tab_width = 2
17 |
18 | [*.py]
19 | indent_style = space
20 | indent_size = 4
21 | tab_width = 4
22 |
23 | [*.md]
24 | indent_size = 4
25 | tab_width = 4
26 | trim_trailing_whitespace = false
27 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/github/administering-a-repository/managing-repository-settings/displaying-a-sponsor-button-in-your-repository
2 |
3 | custom: https://www.paypal.me/ChrisGrieser
4 | ko_fi: pseudometa
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: File a bug report
3 | title: "[Bug]: "
4 | labels: ["bug"]
5 | body:
6 | - type: textarea
7 | id: bug-description
8 | attributes:
9 | label: Bug Description
10 | description: A clear and concise description of the bug.
11 | validations:
12 | required: true
13 | - type: textarea
14 | id: screenshot
15 | attributes:
16 | label: Relevant Screenshot
17 | description: If applicable, add screenshots or a screen recording to help explain your problem.
18 | - type: textarea
19 | id: reproduction-steps
20 | attributes:
21 | label: To Reproduce
22 | description: Steps to reproduce the problem
23 | placeholder: |
24 | For example:
25 | 1. Go to '...'
26 | 2. Click on '...'
27 | 3. Scroll down to '...'
28 | - type: textarea
29 | id: version-info
30 | attributes:
31 | label: neovim version
32 | render: Text
33 | validations:
34 | required: true
35 | - type: checkboxes
36 | id: checklist
37 | attributes:
38 | label: Make sure you have done the following
39 | options:
40 | - label: I have updated to the latest version of the plugin.
41 | required: true
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 | description: Suggest an idea
3 | title: "Feature Request: "
4 | labels: ["enhancement"]
5 | body:
6 | - type: textarea
7 | id: feature-requested
8 | attributes:
9 | label: Feature Requested
10 | description: A clear and concise description of the feature.
11 | validations:
12 | required: true
13 | - type: textarea
14 | id: screenshot
15 | attributes:
16 | label: Relevant Screenshot
17 | description: If applicable, add screenshots or a screen recording to help explain the request.
18 | - type: checkboxes
19 | id: checklist
20 | attributes:
21 | label: Checklist
22 | options:
23 | - label: The feature would be useful to more users than just me.
24 | required: true
25 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "weekly"
7 | commit-message:
8 | prefix: "chore(dependabot): "
9 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## What problem does this PR solve?
2 |
3 | ## How does the PR solve it?
4 |
5 | ## Checklist
6 | - [ ] Used only `camelCase` variable names.
7 | - [ ] If functionality is added or modified, also made respective changes to the
8 | `README.md` (the `.txt` file is auto-generated and does not need to be
9 | modified).
10 |
--------------------------------------------------------------------------------
/.github/workflows/markdownlint.yml:
--------------------------------------------------------------------------------
1 | name: Markdownlint check
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - "**.md"
8 | - ".github/workflows/markdownlint.yml"
9 | - ".markdownlint.*" # markdownlint config files
10 | pull_request:
11 | paths:
12 | - "**.md"
13 |
14 | jobs:
15 | markdownlint:
16 | name: Markdownlint
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - uses: DavidAnson/markdownlint-cli2-action@v20
21 | with:
22 | globs: "**/*.md"
23 |
--------------------------------------------------------------------------------
/.github/workflows/nvim-type-check.yml:
--------------------------------------------------------------------------------
1 | name: nvim type check
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths: ["**.lua"]
7 | pull_request:
8 | paths: ["**.lua"]
9 |
10 | jobs:
11 | build:
12 | name: nvim type check
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: stevearc/nvim-typecheck-action@v2
17 |
--------------------------------------------------------------------------------
/.github/workflows/panvimdoc.yml:
--------------------------------------------------------------------------------
1 | name: panvimdoc
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths:
7 | - README.md
8 | - .github/workflows/panvimdoc.yml
9 | workflow_dispatch: {} # allows manual execution
10 |
11 | permissions:
12 | contents: write
13 |
14 | #───────────────────────────────────────────────────────────────────────────────
15 |
16 | jobs:
17 | docs:
18 | runs-on: ubuntu-latest
19 | name: README.md to vimdoc
20 | steps:
21 | - uses: actions/checkout@v4
22 | - run: git pull # fix failure when multiple commits are pushed in succession
23 | - run: mkdir -p doc
24 |
25 | - name: panvimdoc
26 | uses: kdheepak/panvimdoc@main
27 | with:
28 | vimdoc: ${{ github.event.repository.name }}
29 | version: "Neovim"
30 | demojify: true
31 | treesitter: true
32 |
33 | - run: git pull
34 | - name: push changes
35 | uses: stefanzweifel/git-auto-commit-action@v5
36 | with:
37 | commit_message: "chore: auto-generate vimdocs"
38 | branch: ${{ github.head_ref }}
39 |
--------------------------------------------------------------------------------
/.github/workflows/pr-title.yml:
--------------------------------------------------------------------------------
1 | name: PR title
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - synchronize
9 | - reopened
10 | - ready_for_review
11 |
12 | permissions:
13 | pull-requests: read
14 |
15 | jobs:
16 | semantic-pull-request:
17 | name: Check PR title
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: amannn/action-semantic-pull-request@v5
21 | env:
22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
23 | with:
24 | requireScope: false
25 | subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital
26 | types: | # add `improv` to the list of allowed types
27 | improv
28 | fix
29 | feat
30 | refactor
31 | build
32 | ci
33 | style
34 | test
35 | chore
36 | perf
37 | docs
38 | break
39 | revert
40 |
--------------------------------------------------------------------------------
/.github/workflows/stale-bot.yml:
--------------------------------------------------------------------------------
1 | name: Stale bot
2 | on:
3 | schedule:
4 | - cron: "18 04 * * 3"
5 |
6 | permissions:
7 | issues: write
8 | pull-requests: write
9 |
10 | jobs:
11 | stale:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Close stale issues
15 | uses: actions/stale@v9
16 | with:
17 | repo-token: ${{ secrets.GITHUB_TOKEN }}
18 |
19 | # DOCS https://github.com/actions/stale#all-options
20 | days-before-stale: 180
21 | days-before-close: 7
22 | stale-issue-label: "Stale"
23 | stale-issue-message: |
24 | This issue has been automatically marked as stale.
25 | **If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open.
26 | close-issue-message: |
27 | This issue has been closed due to inactivity, and will not be monitored.
28 |
--------------------------------------------------------------------------------
/.github/workflows/stylua.yml:
--------------------------------------------------------------------------------
1 | name: Stylua check
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | paths: ["**.lua"]
7 | pull_request:
8 | paths: ["**.lua"]
9 |
10 | jobs:
11 | stylua:
12 | name: Stylua
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 | - uses: JohnnyMorganz/stylua-action@v4
17 | with:
18 | token: ${{ secrets.GITHUB_TOKEN }}
19 | version: latest
20 | args: --check .
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # help-tags auto-generated by lazy.nvim
2 | doc/tags
3 |
--------------------------------------------------------------------------------
/.ignore:
--------------------------------------------------------------------------------
1 | # auto-generated by panvimdoc
2 | doc
3 |
--------------------------------------------------------------------------------
/.luarc.json:
--------------------------------------------------------------------------------
1 | {
2 | "runtime.version": "LuaJIT",
3 | "diagnostics.globals": ["vim"]
4 | }
5 |
--------------------------------------------------------------------------------
/.markdownlint.yaml:
--------------------------------------------------------------------------------
1 | # Defaults https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml
2 | # DOCS https://github.com/markdownlint/markdownlint/blob/main/docs/RULES.md
3 | #───────────────────────────────────────────────────────────────────────────────
4 |
5 | # MODIFIED SETTINGS
6 | blanks-around-headings:
7 | lines_below: 0 # space waster
8 | ul-style: { style: sublist }
9 |
10 | # not autofixable
11 | ol-prefix: { style: ordered }
12 | line-length:
13 | tables: false
14 | code_blocks: false
15 | no-inline-html:
16 | allowed_elements: [img, details, summary, kbd, a, br]
17 |
18 | #─────────────────────────────────────────────────────────────────────────────
19 | # DISABLED
20 | ul-indent: false # not compatible with using tabs
21 | no-hard-tabs: false # taken care of by editorconfig
22 | blanks-around-lists: false # space waster
23 | first-line-heading: false # e.g., ignore-comments
24 | no-emphasis-as-heading: false # sometimes useful
25 |
--------------------------------------------------------------------------------
/.stylua.toml:
--------------------------------------------------------------------------------
1 | # https://github.com/JohnnyMorganz/StyLua#options
2 | column_width = 100
3 | indent_type = "Tabs"
4 | indent_width = 3
5 | quote_style = "AutoPreferDouble"
6 | call_parentheses = "NoSingleTable"
7 | collapse_simple_statement = "Always"
8 |
9 | [sort_requires]
10 | enabled = true
11 |
--------------------------------------------------------------------------------
/Justfile:
--------------------------------------------------------------------------------
1 | set quiet := true
2 |
3 | masonPath := "$HOME/.local/share/nvim/mason/bin/"
4 |
5 | #───────────────────────────────────────────────────────────────────────────────
6 |
7 | stylua:
8 | #!/usr/bin/env zsh
9 | {{ masonPath }}/stylua --check --output-format=summary . && return 0
10 | {{ masonPath }}/stylua .
11 | echo "\nFiles formatted."
12 |
13 | lua_ls_check:
14 | {{ masonPath }}/lua-language-server --check .
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-2023 Christopher Grieser
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # nvim-genghis ⚔️
3 |
4 |
5 |
6 |
7 | Lightweight and quick file operations without being a full-blown file manager.
8 | For when you prefer a fuzzy finder over a file tree, but still want some
9 | convenient file operations inside nvim.
10 |
11 |
12 |
13 | ## Table of contents
14 |
15 |
16 |
17 | - [Features](#features)
18 | - [Installation](#installation)
19 | - [Configuration](#configuration)
20 | - [Usage](#usage)
21 | * [File operations](#file-operations)
22 | * [Copy operations](#copy-operations)
23 | * [File navigation](#file-navigation)
24 | - [Why the name "Genghis"?](#why-the-name-genghis)
25 | - [About the author](#about-the-author)
26 |
27 |
28 |
29 | ## Features
30 | **Commands**
31 | - Perform **common file operations**: moving, renaming, creating, deleting, or
32 | duplicating files.
33 | - **Copy** the path or name of the current file in various formats.
34 | - **Navigate** to the next or previous file in the current folder.
35 | - **Lightweight**: This plugin only provides utility file operations, it does
36 | not provide a full-blown file manager UI.
37 |
38 | **Quality-of-life**
39 | - All movement and renaming commands **update `import` statements** to the
40 | renamed file (if the LSP supports `workspace/willRenameFiles`).
41 | - Automatically keep the extension when no extension is given.
42 | - Use vim motions in the input field.
43 |
44 | ## Installation
45 | **Requirements**
46 | - nvim 0.10+
47 | - A `vim.ui.input` provider such as
48 | [dressing.nvim](http://github.com/stevearc/dressing.nvim) or
49 | [snacks.nvim](http://github.com/folke/snacks.nvim) for an input UI that
50 | **supports vim motions** and looks much nicer.
51 | - *For the trash command*: an OS-specific trash CLI like `trash` or `gio trash`.
52 | On macOS 14+, there is a `trash` cli already built-in, so there is no need to
53 | install anything.
54 |
55 | ```lua
56 | -- lazy.nvim
57 | { "chrisgrieser/nvim-genghis" },
58 |
59 | -- packer
60 | use { "chrisgrieser/nvim-genghis" }
61 | ```
62 |
63 | ## Configuration
64 | The `setup` call is required for `lazy.nvim`, but otherwise optional.
65 |
66 | ```lua
67 | -- default config
68 | require("genghis").setup {
69 | ---@type fun(): string|string[]
70 | trashCmd = function()
71 | if jit.os == "OSX" then return "trash" end -- builtin since macOS 14
72 | if jit.os == "Windows" then return "trash" end
73 | if jit.os == "Linux" then return { "gio", "trash" } end
74 | return "trash-cli"
75 | end,
76 |
77 | navigation = {
78 | onlySameExtAsCurrentFile = false,
79 | ignoreDotfiles = true,
80 | ignoreExt = { "png", "svg", "webp", "jpg", "jpeg", "gif", "pdf", "zip", "DS_Store" },
81 | },
82 |
83 | successNotifications = true,
84 |
85 | icons = { -- set to empty string to disable
86 | chmodx = "",
87 | copyFile = "",
88 | copyPath = "",
89 | duplicate = "",
90 | file = "",
91 | move = "",
92 | new = "",
93 | nextFile = "",
94 | prevFile = "",
95 | rename = "",
96 | trash = "",
97 | },
98 | }
99 | ```
100 |
101 | ## Usage
102 | You can access a command as lua function:
103 |
104 | ```lua
105 | require("genghis").createNewFile()
106 | ```
107 |
108 | Or you can use the ex command `:Genghis` with the respective sub-command:
109 |
110 | ```vim
111 | :Genghis createNewFile
112 | ```
113 |
114 | ### File operations
115 | - `createNewFile`: Create a new file.
116 | - `duplicateFile`: Duplicate the current file.
117 | - `moveSelectionToNewFile`: Prompts for a new filename
118 | and moves the current selection to that new file. (Visual
119 | Line command, the selection is moved linewise.)
120 | - `renameFile`: Rename the current file.
121 | - `moveAndRenameFile`: Move and Rename the current file. Keeps the
122 | old name if the new path ends with `/`. Works like the Unix `mv` command.
123 | - `moveToFolderInCwd`: Move the current file to an existing folder in the
124 | current working directory.
125 | - `chmodx`: Makes current file executable. Equivalent to `chmod +x`.
126 | - `trashFile`: Move the current file to the trash. Defaults to `gio trash` on
127 | *Linux*, and `trash` on *macOS* or *Windows*. (The trash CLIs must usually be
128 | installed.)
129 | - `showInSystemExplorer`: Reveals the current file in the system explorer, such
130 | as macOS Finder. (Currently only on macOS, PRs welcome.)
131 |
132 | The following applies to all commands above:
133 | 1. If no extension has been provided, uses the extension of the original file.
134 | 2. If the new filename includes a `/`, the new file is placed in the
135 | respective subdirectory, creating any non-existing intermediate folders.
136 | 3. All movement and renaming commands update `import` statements to the renamed
137 | file (if the LSP supports `workspace/willRenameFiles`).
138 |
139 | ### Copy operations
140 | - `copyFilename`: Copy the filename.
141 | - `copyFilepath`: Copy the absolute file path.
142 | - `copyFilepathWithTilde`: Copy the absolute file path, replacing the home
143 | directory with `~`.
144 | - `copyRelativePath`: Copy the relative file path.
145 | - `copyDirectoryPath`: Copy the absolute directory path.
146 | - `copyRelativeDirectoryPath`: Copy the relative directory path.
147 | - `copyFileItself`: Copies the file itself. This means you can paste it into
148 | the browser or file manager. (Currently only on macOS, PRs welcome.)
149 |
150 | All commands use the system clipboard.
151 |
152 | ### File navigation
153 | `.navigateToFileInFolder("next"|"prev")`: Move to the next/previous file in the
154 | current folder of the current file, in alphabetical order.
155 | - If `snacks.nvim` is installed, displays a cycling notification.
156 |
157 | ## Why the name "Genghis"?
158 | A nod to [vim.eunuch](https://github.com/tpope/vim-eunuch), an older vimscript
159 | plugin with a similar goal. As opposed to childless eunuchs, it is said that
160 | Genghis Khan [has fathered thousands of
161 | children](https://allthatsinteresting.com/genghis-khan-children).
162 |
163 | ## About the author
164 | In my day job, I am a sociologist studying the social mechanisms underlying the
165 | digital economy. For my PhD project, I investigate the governance of the app
166 | economy and how software ecosystems manage the tension between innovation and
167 | compatibility. If you are interested in this subject, feel free to get in touch.
168 |
169 | - [Website](https://chris-grieser.de/)
170 | - [Mastodon](https://pkm.social/@pseudometa)
171 | - [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser)
172 | - [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/)
173 |
174 |
177 |
--------------------------------------------------------------------------------
/doc/nvim-genghis.txt:
--------------------------------------------------------------------------------
1 | *nvim-genghis.txt* For Neovim Last change: 2025 May 20
2 |
3 | ==============================================================================
4 | Table of Contents *nvim-genghis-table-of-contents*
5 |
6 | 1. nvim-genghis |nvim-genghis-nvim-genghis-|
7 | - Table of contents |nvim-genghis-nvim-genghis--table-of-contents|
8 | - Features |nvim-genghis-nvim-genghis--features|
9 | - Installation |nvim-genghis-nvim-genghis--installation|
10 | - Configuration |nvim-genghis-nvim-genghis--configuration|
11 | - Usage |nvim-genghis-nvim-genghis--usage|
12 | - Why the name “Genghis”?|nvim-genghis-nvim-genghis--why-the-name-“genghis”?|
13 | - About the author |nvim-genghis-nvim-genghis--about-the-author|
14 |
15 | ==============================================================================
16 | 1. nvim-genghis *nvim-genghis-nvim-genghis-*
17 |
18 |
19 |
20 | Lightweightand quick file operations without being a full-blown file manager.
21 | For when you prefer a fuzzy finder over a file tree, but still want some
22 | convenient file operations inside nvim.
23 |
24 |
25 |
26 |
27 | TABLE OF CONTENTS *nvim-genghis-nvim-genghis--table-of-contents*
28 |
29 | - |nvim-genghis-features|
30 | - |nvim-genghis-installation|
31 | - |nvim-genghis-configuration|
32 | - |nvim-genghis-usage|
33 | - |nvim-genghis-file-operations|
34 | - |nvim-genghis-copy-operations|
35 | - |nvim-genghis-file-navigation|
36 | - |nvim-genghis-why-the-name-"genghis"?|
37 | - |nvim-genghis-about-the-author|
38 |
39 |
40 | FEATURES *nvim-genghis-nvim-genghis--features*
41 |
42 | **Commands** - Perform **common file operations**moving, renaming, creating,
43 | deleting, or duplicating files. - **Copy** the path or name of the current file
44 | in various formats. - **Navigate** to the next or previous file in the current
45 | folder. - **Lightweight**This plugin only provides utility file operations, it
46 | does not provide a full-blown file manager UI.
47 |
48 | **Quality-of-life** - All movement and renaming commands **update import
49 | statements** to the renamed file (if the LSP supports
50 | `workspace/willRenameFiles`). - Automatically keep the extension when no
51 | extension is given. - Use vim motions in the input field.
52 |
53 |
54 | INSTALLATION *nvim-genghis-nvim-genghis--installation*
55 |
56 | **Requirements** - nvim 0.10+ - A `vim.ui.input` provider such as dressing.nvim
57 | or snacks.nvim
58 | for an input UI that **supports vim
59 | motions** and looks much nicer. - _For the trash command_an OS-specific trash
60 | CLI like `trash` or `gio trash`. On macOS 14+, there is a `trash` cli already
61 | built-in, so there is no need to install anything.
62 |
63 | >lua
64 | -- lazy.nvim
65 | { "chrisgrieser/nvim-genghis" },
66 |
67 | -- packer
68 | use { "chrisgrieser/nvim-genghis" }
69 | <
70 |
71 |
72 | CONFIGURATION *nvim-genghis-nvim-genghis--configuration*
73 |
74 | The `setup` call is required for `lazy.nvim`, but otherwise optional.
75 |
76 | >lua
77 | -- default config
78 | require("genghis").setup {
79 | ---@type fun(): string|string[]
80 | trashCmd = function()
81 | if jit.os == "OSX" then return "trash" end -- builtin since macOS 14
82 | if jit.os == "Windows" then return "trash" end
83 | if jit.os == "Linux" then return { "gio", "trash" } end
84 | return "trash-cli"
85 | end,
86 |
87 | navigation = {
88 | onlySameExtAsCurrentFile = false,
89 | ignoreDotfiles = true,
90 | ignoreExt = { "png", "svg", "webp", "jpg", "jpeg", "gif", "pdf", "zip", "DS_Store" },
91 | },
92 |
93 | successNotifications = true,
94 |
95 | icons = { -- set to empty string to disable
96 | chmodx = "",
97 | copyFile = "",
98 | copyPath = "",
99 | duplicate = "",
100 | file = "",
101 | move = "",
102 | new = "",
103 | nextFile = "",
104 | prevFile = "",
105 | rename = "",
106 | trash = "",
107 | },
108 | }
109 | <
110 |
111 |
112 | USAGE *nvim-genghis-nvim-genghis--usage*
113 |
114 | You can access a command as lua function:
115 |
116 | >lua
117 | require("genghis").createNewFile()
118 | <
119 |
120 | Or you can use the ex command `:Genghis` with the respective sub-command:
121 |
122 | >vim
123 | :Genghis createNewFile
124 | <
125 |
126 |
127 | FILE OPERATIONS ~
128 |
129 | - `createNewFile`Create a new file.
130 | - `duplicateFile`Duplicate the current file.
131 | - `moveSelectionToNewFile`Prompts for a new filename
132 | and moves the current selection to that new file. (Visual
133 | Line command, the selection is moved linewise.)
134 | - `renameFile`Rename the current file.
135 | - `moveAndRenameFile`Move and Rename the current file. Keeps the
136 | old name if the new path ends with `/`. Works like the Unix `mv` command.
137 | - `moveToFolderInCwd`Move the current file to an existing folder in the
138 | current working directory.
139 | - `chmodx`Makes current file executable. Equivalent to `chmod +x`.
140 | - `trashFile`Move the current file to the trash. Defaults to `gio trash` on
141 | _Linux_, and `trash` on _macOS_ or _Windows_. (The trash CLIs must usually be
142 | installed.)
143 | - `showInSystemExplorer`Reveals the current file in the system explorer, such
144 | as macOS Finder. (Currently only on macOS, PRs welcome.)
145 |
146 | The following applies to all commands above: 1. If no extension has been
147 | provided, uses the extension of the original file. 2. If the new filename
148 | includes a `/`, the new file is placed in the respective subdirectory, creating
149 | any non-existing intermediate folders. 3. All movement and renaming commands
150 | update `import` statements to the renamed file (if the LSP supports
151 | `workspace/willRenameFiles`).
152 |
153 |
154 | COPY OPERATIONS ~
155 |
156 | - `copyFilename`Copy the filename.
157 | - `copyFilepath`Copy the absolute file path.
158 | - `copyFilepathWithTilde`Copy the absolute file path, replacing the home
159 | directory with `~`.
160 | - `copyRelativePath`Copy the relative file path.
161 | - `copyDirectoryPath`Copy the absolute directory path.
162 | - `copyRelativeDirectoryPath`Copy the relative directory path.
163 | - `copyFileItself`Copies the file itself. This means you can paste it into
164 | the browser or file manager. (Currently only on macOS, PRs welcome.)
165 |
166 | All commands use the system clipboard.
167 |
168 |
169 | FILE NAVIGATION ~
170 |
171 | `.navigateToFileInFolder("next"|"prev")`Move to the next/previous file in the
172 | current folder of the current file, in alphabetical order. - If `snacks.nvim`
173 | is installed, displays a cycling notification.
174 |
175 |
176 | WHY THE NAME “GENGHIS”?*nvim-genghis-nvim-genghis--why-the-name-“genghis”?*
177 |
178 | A nod to vim.eunuch , an older vimscript
179 | plugin with a similar goal. As opposed to childless eunuchs, it is said that
180 | Genghis Khan has fathered thousands of children
181 | .
182 |
183 |
184 | ABOUT THE AUTHOR *nvim-genghis-nvim-genghis--about-the-author*
185 |
186 | In my day job, I am a sociologist studying the social mechanisms underlying the
187 | digital economy. For my PhD project, I investigate the governance of the app
188 | economy and how software ecosystems manage the tension between innovation and
189 | compatibility. If you are interested in this subject, feel free to get in
190 | touch.
191 |
192 | - Website
193 | - Mastodon
194 | - ResearchGate
195 | - LinkedIn
196 |
197 |
198 |
199 | Generated by panvimdoc
200 |
201 | vim:tw=78:ts=8:noet:ft=help:norl:
202 |
--------------------------------------------------------------------------------
/lua/genghis/config.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 | --------------------------------------------------------------------------------
3 |
4 | ---@class Genghis.config
5 | local defaultConfig = {
6 | ---@type fun(): string|string[]
7 | trashCmd = function()
8 | if jit.os == "OSX" then return "trash" end -- builtin since macOS 14
9 | if jit.os == "Windows" then return "trash" end
10 | if jit.os == "Linux" then return { "gio", "trash" } end
11 | return "trash-cli"
12 | end,
13 |
14 | navigation = {
15 | onlySameExtAsCurrentFile = false,
16 | ignoreDotfiles = true,
17 | ignoreExt = { "png", "svg", "webp", "jpg", "jpeg", "gif", "pdf", "zip", "DS_Store" },
18 | },
19 |
20 | successNotifications = true,
21 |
22 | icons = { -- set to empty string to disable
23 | chmodx = "",
24 | copyFile = "",
25 | copyPath = "",
26 | duplicate = "",
27 | file = "",
28 | move = "",
29 | new = "",
30 | nextFile = "",
31 | prevFile = "",
32 | rename = "",
33 | trash = "",
34 | },
35 | }
36 |
37 | M.config = defaultConfig
38 |
39 | ---@param userConfig? Genghis.config
40 | function M.setup(userConfig)
41 | M.config = vim.tbl_deep_extend("force", defaultConfig, userConfig or {})
42 | end
43 |
44 | --------------------------------------------------------------------------------
45 | return M
46 |
--------------------------------------------------------------------------------
/lua/genghis/init.lua:
--------------------------------------------------------------------------------
1 | local version = vim.version()
2 | if version.major == 0 and version.minor < 10 then
3 | vim.notify("nvim-genghis requires at least nvim 0.10.", vim.log.levels.WARN)
4 | return
5 | end
6 | --------------------------------------------------------------------------------
7 |
8 | local M = {}
9 |
10 | ---@param userConfig? Genghis.config
11 | function M.setup(userConfig) require("genghis.config").setup(userConfig) end
12 |
13 | ---@param direction? "next"|"prev"
14 | function M.navigateToFileInFolder(direction)
15 | require("genghis.operations.navigation").fileInFolder(direction)
16 | end
17 |
18 | setmetatable(M, {
19 | __index = function(_, key)
20 | return function(...)
21 | local module = vim.startswith(key, "copy") and "copy" or "file"
22 | require("genghis.operations." .. module)[key](...)
23 | end
24 | end,
25 | })
26 |
27 | --------------------------------------------------------------------------------
28 | return M
29 |
--------------------------------------------------------------------------------
/lua/genghis/operations/copy.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 | local u = require("genghis.support.utils")
3 | --------------------------------------------------------------------------------
4 |
5 | ---@param expandOperation string
6 | local function copyOp(expandOperation)
7 | local icon = require("genghis.config").config.icons.copyPath
8 |
9 | local register = "+"
10 | local toCopy = vim.fn.expand(expandOperation)
11 | vim.fn.setreg(register, toCopy)
12 |
13 | u.notify(toCopy, "info", { title = "Copied", icon = icon })
14 | end
15 |
16 | -- DOCS for the modifiers
17 | -- https://neovim.io/doc/user/builtin.html#expand()
18 | -- https://neovim.io/doc/user/cmdline.html#filename-modifiers
19 | function M.copyFilepath() copyOp("%:p") end
20 | function M.copyFilepathWithTilde() copyOp("%:~") end
21 | function M.copyFilename() copyOp("%:t") end
22 | function M.copyRelativePath() copyOp("%:.") end
23 | function M.copyDirectoryPath() copyOp("%:p:h") end
24 | function M.copyRelativeDirectoryPath() copyOp("%:.:h") end
25 |
26 | function M.copyFileItself()
27 | if jit.os ~= "OSX" then
28 | u.notify("Currently only available on macOS.", "warn")
29 | return
30 | end
31 |
32 | local icon = require("genghis.config").config.icons.copyFile
33 | local path = vim.api.nvim_buf_get_name(0)
34 | local applescript = 'tell application "Finder" to set the clipboard to '
35 | .. ("POSIX file %q"):format(path)
36 |
37 | vim.system({ "osascript", "-e", applescript }, {}, function(out)
38 | if out.code ~= 0 then
39 | u.notify("Failed to copy file: " .. out.stderr, "error", { title = "Copy file" })
40 | else
41 | u.notify(vim.fs.basename(path), "info", { title = "Copied file", icon = icon })
42 | end
43 | end)
44 | end
45 |
46 | --------------------------------------------------------------------------------
47 | return M
48 |
--------------------------------------------------------------------------------
/lua/genghis/operations/file.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | local rename = require("genghis.support.lsp-rename")
4 | local u = require("genghis.support.utils")
5 | local pathSep = package.config:sub(1, 1)
6 | --------------------------------------------------------------------------------
7 |
8 | ---@param op "rename"|"duplicate"|"new"|"new-from-selection"|"move-rename"
9 | local function fileOp(op)
10 | local origBufNr = vim.api.nvim_get_current_buf()
11 | local oldFilePath = vim.api.nvim_buf_get_name(0)
12 | local oldName = vim.fs.basename(oldFilePath)
13 | local dir = vim.fs.dirname(oldFilePath) -- same directory, *not* pwd
14 | local oldNameNoExt = oldName:gsub("%.%w+$", "")
15 | local oldExt = vim.fn.expand("%:e")
16 | if oldExt ~= "" then oldExt = "." .. oldExt end
17 | local icons = require("genghis.config").config.icons
18 | local lspSupportsRenaming = rename.lspSupportsRenaming()
19 |
20 | local prevReg
21 | if op == "new-from-selection" then
22 | prevReg = vim.fn.getreg("z")
23 | -- leaves visual mode, needed for '<,'> marks to be set
24 | vim.cmd.normal { vim.fn.mode(), bang = true }
25 | vim.cmd([['<,'>delete z]])
26 | end
27 |
28 | local promptStr, prefill
29 | if op == "duplicate" then
30 | promptStr = icons.duplicate .. " Duplicate file as: "
31 | prefill = oldNameNoExt .. "-1"
32 | elseif op == "rename" then
33 | local text = lspSupportsRenaming and "Rename file & update imports:" or "Rename file to:"
34 | promptStr = icons.rename .. " " .. text
35 | prefill = oldNameNoExt
36 | elseif op == "move-rename" then
37 | local text = lspSupportsRenaming and " Move and rename file & update imports:"
38 | or " Move & rename file to:"
39 | promptStr = icons.rename .. " " .. text
40 | prefill = dir .. pathSep
41 | elseif op == "new" or op == "new-from-selection" then
42 | promptStr = icons.new .. " Name for new file: "
43 | prefill = ""
44 | end
45 | promptStr = vim.trim(promptStr) -- in case of empty icon
46 |
47 | vim.ui.input({
48 | prompt = promptStr,
49 | default = prefill,
50 | completion = "dir", -- allows for completion via cmp-omni
51 | }, function(newName)
52 | vim.cmd.redraw() -- Clear message area from ui.input prompt
53 | if not newName then return end -- input has been canceled
54 |
55 | if op == "move-rename" and newName:find("/$") then newName = newName .. oldName end
56 | if op == "new" and newName == "" then newName = "Untitled" end
57 |
58 | -- GUARD Validate filename
59 | local invalidName = newName:find("^%s+$")
60 | or newName:find("[\\:]")
61 | or (newName:find("^/") and not op == "move-rename")
62 | local sameName = newName == oldName
63 | local emptyInput = newName == ""
64 |
65 | if invalidName or sameName or emptyInput then
66 | if op == "new-from-selection" then
67 | vim.cmd.undo() -- undo deletion
68 | vim.fn.setreg("z", prevReg) -- restore register content
69 | end
70 | if invalidName or emptyInput then
71 | u.notify("Invalid filename.", "error")
72 | elseif sameName then
73 | u.notify("Cannot use the same filename.", "warn")
74 | end
75 | return
76 | end
77 |
78 | -- DETERMINE PATH AND EXTENSION
79 | local hasPath = newName:find(pathSep)
80 | if hasPath then
81 | local newFolder = vim.fs.dirname(newName)
82 | vim.fn.mkdir(newFolder, "p") -- create folders if necessary
83 | end
84 |
85 | local extProvided = newName:find(".%.[^/]*$") -- non-leading dot to not include dotfiles without extension
86 | if not extProvided then newName = newName .. oldExt end
87 | local newFilePath = (op == "move-rename") and newName or dir .. pathSep .. newName
88 |
89 | if vim.uv.fs_stat(newFilePath) ~= nil then
90 | u.notify(("File with name %q already exists."):format(newFilePath), "error")
91 | return
92 | end
93 |
94 | -- EXECUTE FILE OPERATION
95 | vim.cmd.update()
96 | if op == "duplicate" then
97 | local success = vim.uv.fs_copyfile(oldFilePath, newFilePath)
98 | if success then
99 | vim.cmd.edit(newFilePath)
100 | local msg = ("Duplicated %q as %q."):format(oldName, newName)
101 | u.notify(msg, "info", { icon = icons.duplicate })
102 | end
103 | elseif op == "rename" or op == "move-rename" then
104 | rename.sendWillRenameToLsp(oldFilePath, newFilePath)
105 | local success = u.moveFileConsideringPartition(oldFilePath, newFilePath)
106 | if success then
107 | vim.cmd.edit(newFilePath)
108 | vim.api.nvim_buf_delete(origBufNr, { force = true })
109 | local msg = ("Renamed %q to %q."):format(oldName, newName)
110 | u.notify(msg, "info", { icon = icons.rename })
111 | if lspSupportsRenaming then vim.cmd.wall() end
112 | end
113 | elseif op == "new" or op == "new-from-selection" then
114 | vim.cmd.edit(newFilePath)
115 | if op == "new-from-selection" then
116 | vim.cmd("put z") -- cmd.put("z") does not work
117 | vim.fn.setreg("z", prevReg) -- restore register content
118 | end
119 | vim.cmd.write(newFilePath)
120 | end
121 | end)
122 | end
123 |
124 | function M.renameFile() fileOp("rename") end
125 | function M.moveAndRenameFile() fileOp("move-rename") end
126 | function M.duplicateFile() fileOp("duplicate") end
127 | function M.createNewFile() fileOp("new") end
128 | function M.moveSelectionToNewFile() fileOp("new-from-selection") end
129 |
130 | --------------------------------------------------------------------------------
131 |
132 | function M.moveToFolderInCwd()
133 | local curFilePath = vim.api.nvim_buf_get_name(0)
134 | local parentOfCurFile = vim.fs.dirname(curFilePath)
135 | local filename = vim.fs.basename(curFilePath)
136 | local lspSupportsRenaming = rename.lspSupportsRenaming()
137 | local cwd = vim.uv.cwd()
138 | local icons = require("genghis.config").config.icons
139 | local origBufNr = vim.api.nvim_get_current_buf()
140 |
141 | -- determine destinations in cwd
142 | local foldersInCwd = vim.fs.find(function(name, path)
143 | local absPath = vim.fs.joinpath(path, name)
144 | local relPath = absPath:sub(#cwd + 1) .. pathSep
145 | local ignoreDirs = absPath == parentOfCurFile
146 | or relPath:find("/node_modules/") -- js/ts
147 | or relPath:find("/typings/") -- python
148 | or relPath:find("%.app/") -- macos pseudo-folders
149 | or relPath:find("/%.") -- hidden folders
150 | return not ignoreDirs
151 | end, { type = "directory", limit = math.huge })
152 |
153 | -- sort by modification time
154 | table.sort(foldersInCwd, function(a, b)
155 | local aMtime = vim.uv.fs_stat(a).mtime.sec
156 | local bMtime = vim.uv.fs_stat(b).mtime.sec
157 | return aMtime > bMtime
158 | end)
159 | -- insert cwd at bottom, since modification of is likely due to subfolders
160 | if cwd ~= parentOfCurFile then table.insert(foldersInCwd, cwd) end
161 |
162 | -- prompt user and move
163 | local promptStr = icons.new .. " Choose destination folder"
164 | if lspSupportsRenaming then promptStr = promptStr .. " (with updated imports)" end
165 | vim.ui.select(foldersInCwd, {
166 | prompt = promptStr,
167 | kind = "genghis.moveToFolderInCwd",
168 | format_item = function(path)
169 | local relPath = path:sub(#cwd + 1)
170 | return (relPath == "" and "/" or relPath)
171 | end,
172 | }, function(destination)
173 | if not destination then return end
174 | local newFilePath = vim.fs.joinpath(destination, filename)
175 |
176 | -- GUARD
177 | if vim.uv.fs_stat(newFilePath) ~= nil then
178 | u.notify(("File %q already exists at %q."):format(filename, destination), "error")
179 | return
180 | end
181 |
182 | rename.sendWillRenameToLsp(curFilePath, newFilePath)
183 | local success = u.moveFileConsideringPartition(curFilePath, newFilePath)
184 | if success then
185 | vim.cmd.edit(newFilePath)
186 | vim.api.nvim_buf_delete(origBufNr, { force = true })
187 | local msg = ("Moved %q to %q"):format(filename, destination)
188 | local append = lspSupportsRenaming and " and updated imports." or "."
189 | u.notify(msg .. append, "info", { icon = icons.move })
190 | if lspSupportsRenaming then vim.cmd.wall() end
191 | end
192 | end)
193 | end
194 |
195 | function M.chmodx()
196 | local icons = require("genghis.config").config.icons
197 |
198 | local filepath = vim.api.nvim_buf_get_name(0)
199 | local perm = vim.fn.getfperm(filepath)
200 | perm = perm:gsub("r(.)%-", "r%1x") -- add x to every group that has r
201 | vim.fn.setfperm(filepath, perm)
202 |
203 | u.notify("Permission +x granted.", "info", { icon = icons.chmodx })
204 | vim.cmd.edit() -- reload the file
205 | end
206 |
207 | function M.trashFile()
208 | vim.cmd("silent! update")
209 | local filepath = vim.api.nvim_buf_get_name(0)
210 | local filename = vim.fs.basename(filepath)
211 | local icon = require("genghis.config").config.icons.trash
212 | local trashCmd = require("genghis.config").config.trashCmd
213 |
214 | -- execute the trash command
215 | if type(trashCmd) ~= "function" then
216 | -- DEPRECATION (2025-03-29)
217 | u.notify("`trashCmd` now expects a function, see the README.", "warn")
218 | return
219 | end
220 | local cmd = trashCmd()
221 | if type(cmd) ~= "table" then cmd = { cmd } end
222 | table.insert(cmd, filepath)
223 | local out = vim.system(cmd):wait()
224 |
225 | -- handle the result
226 | if out.code == 0 then
227 | vim.api.nvim_buf_delete(0, { force = true })
228 | u.notify(("%q moved to trash."):format(filename), "info", { icon = icon })
229 | else
230 | local outmsg = (out.stdout or "") .. (out.stderr or "")
231 | u.notify(("Trashing %q failed: %s"):format(filename, outmsg), "error")
232 | end
233 | end
234 |
235 | function M.showInSystemExplorer()
236 | if jit.os ~= "OSX" then
237 | u.notify("Currently only available on macOS.", "warn")
238 | return
239 | end
240 |
241 | local out = vim.system({ "open", "-R", vim.api.nvim_buf_get_name(0) }):wait()
242 | if out.code ~= 0 then
243 | local icon = require("genghis.config").config.icons.file
244 | u.notify("Failed: " .. out.stderr, "error", { icon = icon })
245 | end
246 | end
247 | --------------------------------------------------------------------------------
248 | return M
249 |
--------------------------------------------------------------------------------
/lua/genghis/operations/navigation.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 | local u = require("genghis.support.utils")
3 | --------------------------------------------------------------------------------
4 |
5 | ---Notification specific to file switching
6 | ---@param msg string
7 | ---@param level "info"|"warn"|"error"
8 | ---@param opts? table
9 | local function notify(msg, level, opts)
10 | opts = opts or {}
11 | opts = vim.tbl_extend("force", opts, {
12 | id = "next-in-folder", -- replace notifications when quickly cycling
13 | ft = "markdown", -- so `h1` is highlighted
14 | })
15 | vim.notify(msg, vim.log.levels[level:upper()], opts)
16 | end
17 |
18 | ---Cycles files in folder in alphabetical order.
19 | ---If snacks.nvim is installed, adds cycling notification.
20 | ---@param direction? "next"|"prev"
21 | function M.fileInFolder(direction)
22 | if not direction then direction = "next" end
23 | if direction ~= "next" and direction ~= "prev" then
24 | u.notify('Invalid direction. Only "next" and "prev" are allowed.', "warn")
25 | return
26 | end
27 |
28 | local config = require("genghis.config").config
29 | local curPath = vim.api.nvim_buf_get_name(0)
30 | local curFile = vim.fs.basename(curPath)
31 | local curFolder = vim.fs.dirname(curPath)
32 |
33 | local notifyOpts = {
34 | title = direction:sub(1, 1):upper() .. direction:sub(2) .. " file",
35 | icon = direction == "next" and config.icons.nextFile or config.icons.prevFile,
36 | }
37 |
38 | -- get list of files
39 | local itemsInFolder = vim.fs.dir(curFolder) -- INFO `fs.dir` already returns them sorted
40 | local filesInFolder = vim.iter(itemsInFolder):fold({}, function(acc, name, type)
41 | local ext = name:match("%.(%w+)$")
42 | local curExt = curFile:match("%.(%w+)$")
43 |
44 | local ignored = (config.navigation.onlySameExtAsCurrentFile and ext ~= curExt)
45 | or vim.tbl_contains(config.navigation.ignoreExt, ext)
46 | or (config.navigation.ignoreDotfiles and vim.startswith(name, "."))
47 |
48 | if type == "file" and not ignored then
49 | table.insert(acc, name) -- select only name
50 | end
51 | return acc
52 | end)
53 |
54 | -- GUARD no files to navigate to
55 | if #filesInFolder == 0 then -- if currently at a hidden file and there are only hidden files in the dir
56 | notify("No valid files found in folder.", "warn")
57 | return
58 | elseif #filesInFolder == 1 then
59 | notify("Already at the only valid file.", "warn")
60 | return
61 | end
62 |
63 | -- determine next index
64 | local curIdx
65 | for idx = 1, #filesInFolder do
66 | if filesInFolder[idx] == curFile then
67 | curIdx = idx
68 | break
69 | end
70 | end
71 | local nextIdx = curIdx + (direction == "next" and 1 or -1)
72 | if nextIdx < 1 then nextIdx = #filesInFolder end
73 | if nextIdx > #filesInFolder then nextIdx = 1 end
74 |
75 | -- goto file
76 | local nextFile = curFolder .. "/" .. filesInFolder[nextIdx]
77 | vim.cmd.edit(nextFile)
78 |
79 | -- notification
80 | if package.loaded["snacks"] then
81 | local msg = vim
82 | .iter(filesInFolder)
83 | :map(function(file)
84 | -- mark current, using markdown h1
85 | local prefix = file == filesInFolder[nextIdx] and "#" or "-"
86 | return prefix .. " " .. file
87 | end)
88 | :slice(nextIdx - 5, nextIdx + 5) -- display ~5 files before/after
89 | :join("\n")
90 | notifyOpts.title = notifyOpts.title .. (" (%d/%d)"):format(nextIdx, #filesInFolder)
91 | notify(msg, "info", notifyOpts)
92 | end
93 | end
94 |
95 | --------------------------------------------------------------------------------
96 | return M
97 |
--------------------------------------------------------------------------------
/lua/genghis/support/lsp-rename.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 |
3 | --------------------------------------------------------------------------------
4 |
5 | ---Requests a 'workspace/willRenameFiles' on any running LSP client, that supports it
6 | ---SOURCE https://github.com/LazyVim/LazyVim/blob/ac092289f506052cfdd1879f462be05075fe3081/lua/lazyvim/util/lsp.lua#L99-L119
7 | ---@param fromName string
8 | ---@param toName string
9 | function M.sendWillRenameToLsp(fromName, toName)
10 | local clients = vim.lsp.get_clients { bufnr = 0 }
11 | for _, client in ipairs(clients) do
12 | if client:supports_method("workspace/willRenameFiles") then
13 | local response = client:request_sync("workspace/willRenameFiles", {
14 | files = {
15 | { oldUri = vim.uri_from_fname(fromName), newUri = vim.uri_from_fname(toName) },
16 | },
17 | }, 1000, 0)
18 | if response and response.result ~= nil then
19 | vim.lsp.util.apply_workspace_edit(response.result, client.offset_encoding)
20 | end
21 | end
22 | end
23 | end
24 |
25 | ---@nodiscard
26 | ---@return boolean
27 | function M.lspSupportsRenaming()
28 | local clients = vim.lsp.get_clients { bufnr = 0 }
29 | for _, client in ipairs(clients) do
30 | if client:supports_method("workspace/willRenameFiles") then return true end
31 | end
32 | return false
33 | end
34 |
35 | --------------------------------------------------------------------------------
36 | return M
37 |
--------------------------------------------------------------------------------
/lua/genghis/support/utils.lua:
--------------------------------------------------------------------------------
1 | local M = {}
2 | --------------------------------------------------------------------------------
3 |
4 | ---@param msg string
5 | ---@param level? "info"|"trace"|"debug"|"warn"|"error"
6 | ---@param opts? table
7 | function M.notify(msg, level, opts)
8 | local successNotify = require("genghis.config").config.successNotifications
9 | if not level then level = "info" end
10 | if level == "info" and not successNotify then return end
11 | if not opts then opts = {} end
12 |
13 | opts.title = opts.title and "Genghis: " .. opts.title or "Genghis"
14 | opts.ft = "text" -- prevent `~` from creating strikethroughs in `snacks.notifier`
15 | vim.notify(msg, vim.log.levels[level:upper()], opts)
16 | end
17 |
18 | ---@param oldFilePath string
19 | ---@param newFilePath string
20 | function M.moveFileConsideringPartition(oldFilePath, newFilePath)
21 | local renamed, _ = vim.uv.fs_rename(oldFilePath, newFilePath)
22 | if renamed then return true end
23 |
24 | ---try `fs_copyfile` to support moving across partitions
25 | local copied, copiedError = vim.uv.fs_copyfile(oldFilePath, newFilePath)
26 | if copied then
27 | local deleted, deletedError = vim.uv.fs_unlink(oldFilePath)
28 | if deleted then
29 | return true
30 | else
31 | M.notify(("Failed to delete %q: %q"):format(oldFilePath, deletedError), "error")
32 | return false
33 | end
34 | else
35 | local msg = ("Failed to copy %q to %q: %q"):format(oldFilePath, newFilePath, copiedError)
36 | M.notify(msg, "error")
37 | return false
38 | end
39 | end
40 |
41 | --------------------------------------------------------------------------------
42 | return M
43 |
--------------------------------------------------------------------------------
/plugin/ex-commands.lua:
--------------------------------------------------------------------------------
1 | vim.api.nvim_create_user_command("Genghis", function(ctx) require("genghis")[ctx.args]() end, {
2 | nargs = 1,
3 | complete = function(query)
4 | local allOps = {}
5 | vim.list_extend(allOps, vim.tbl_keys(require("genghis.operations.file")))
6 | vim.list_extend(allOps, vim.tbl_keys(require("genghis.operations.copy")))
7 | vim.list_extend(allOps, vim.tbl_keys(require("genghis.operations.navigation")))
8 | return vim.tbl_filter(function(op) return op:lower():find(query, nil, true) end, allOps)
9 | end,
10 | })
11 |
--------------------------------------------------------------------------------