├── .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 | badge 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 | Showcase for renaming files 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 | Buy Me a Coffee at ko-fi.com 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 | --------------------------------------------------------------------------------