[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nmax_line_length = 100\nend_of_line = lf\ncharset = utf-8\ninsert_final_newline = true\nindent_style = tab\nindent_size = 3\ntab_width = 3\ntrim_trailing_whitespace = true\n\n[*.{yml,yaml,scm,cff}]\nindent_style = space\nindent_size = 2\ntab_width = 2\n\n[*.py]\nindent_style = space\nindent_size = 4\ntab_width = 4\n\n[*.md]\nindent_style = space\nindent_size = 4\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".emmyrc.json",
    "content": "{\n\t\"runtime\": {\n\t\t\"version\": \"LuaJIT\",\n\t\t\"requirePattern\": [\"lua/?.lua\", \"lua/?/init.lua\"]\n\t},\n\t\"workspace\": {\n\t\t\"library\": [\"$VIMRUNTIME\"]\n\t},\n\t\"$schema\": \"https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json\"\n}\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository\n\ngithub: chrisgrieser\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: Bug Report\ndescription: File a bug report\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Bug Description\n      description: A clear and concise description of the bug.\n    validations:\n      required: true\n  - type: textarea\n    id: screenshot\n    attributes:\n      label: Relevant Screenshot\n      description: If applicable, add screenshots or a screen recording to help explain your problem.\n  - type: textarea\n    id: reproduction-steps\n    attributes:\n      label: To Reproduce\n      description: Steps to reproduce the problem\n      placeholder: |\n        For example:\n        1. Go to '...'\n        2. Click on '...'\n        3. Scroll down to '...'\n  - type: textarea\n    id: version-info\n    attributes:\n      label: neovim version\n      render: Text\n    validations:\n      required: true\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Make sure you have done the following\n      options:\n        - label: I have updated to the latest version of the plugin.\n          required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: Feature request\ndescription: Suggest an idea\ntitle: \"Feature Request: \"\nlabels: [\"enhancement\"]\nbody:\n  - type: textarea\n    id: feature-requested\n    attributes:\n      label: Feature Requested\n      description: A clear and concise description of the feature.\n    validations:\n      required: true\n  - type: textarea\n    id: screenshot\n    attributes:\n      label: Relevant Screenshot\n      description: If applicable, add screenshots or a screen recording to help explain the request.\n  - type: checkboxes\n    id: checklist\n    attributes:\n      label: Checklist\n      options:\n        - label: The feature would be useful to more users than just me.\n          required: true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n    commit-message:\n      prefix: \"chore(dependabot): \"\n"
  },
  {
    "path": ".github/pull_request_template.md",
    "content": "## Problem statement\n<!-- Briefly describe the issue this PR addresses. -->\n\n## Proposed solution\n<!-- Explain how this PR resolves the problem. -->\n\n## AI usage disclosure\n<!-- If you used AI beyond simple autocomplete, describe how.\nAI-assisted code is not discouraged if it has been properly reviewed;\nthis disclosure is for transparency. -->\n\n## Checklist\n- [ ] Variable names follow `camelCase` convention.\n- [ ] All AI-generated code has been reviewed by a human.\n- [ ] The `README.md` has been updated for any new or modified functionality\n  (the `.txt` file is auto-generated and does not need to be modified).\n"
  },
  {
    "path": ".github/workflows/nvim-type-check.yml",
    "content": "name: nvim type check\n\non:\n  push:\n    branches: [main]\n    paths: [\"**.lua\"]\n  pull_request: \n    paths: [\"**.lua\"]\n\njobs:\n  build:\n    name: nvim type check\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: stevearc/nvim-typecheck-action@v2\n"
  },
  {
    "path": ".github/workflows/panvimdoc.yml",
    "content": "name: panvimdoc\n\non:\n  push:\n    branches: [main]\n    paths:\n      - README.md\n      - .github/workflows/panvimdoc.yml\n  workflow_dispatch: {} # allows manual execution\n\npermissions:\n  contents: write\n\n#───────────────────────────────────────────────────────────────────────────────\n\njobs:\n  docs:\n    runs-on: ubuntu-latest\n    name: README.md to vimdoc\n    steps:\n      - uses: actions/checkout@v6\n      - run: git pull # fix failure when multiple commits are pushed in succession\n      - run: mkdir -p doc\n\n      - name: panvimdoc\n        uses: kdheepak/panvimdoc@main\n        with:\n          vimdoc: ${{ github.event.repository.name }}\n          version: \"Neovim\"\n          demojify: true\n          treesitter: true\n\n      - run: git pull\n      - name: push changes\n        uses: stefanzweifel/git-auto-commit-action@v7\n        with:\n          commit_message: \"chore: auto-generate vimdocs\"\n          branch: ${{ github.head_ref }}\n"
  },
  {
    "path": ".github/workflows/pr-title.yml",
    "content": "name: PR title\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n      - reopened\n      - ready_for_review\n\npermissions:\n  pull-requests: read\n\njobs:\n  semantic-pull-request:\n    name: Check PR title\n    runs-on: ubuntu-latest\n    steps:\n      - uses: amannn/action-semantic-pull-request@v6\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n        with:\n          requireScope: false\n          subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital\n          types: | # add `improv` to the list of allowed types\n            improv\n            fix\n            feat\n            refactor\n            build\n            ci\n            style\n            test\n            chore\n            perf\n            docs\n            break\n            revert\n"
  },
  {
    "path": ".github/workflows/rumdl-lint.yml",
    "content": "name: Markdown linting via rumdl\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \"**/*.md\"\n      - \".github/workflows/rumdl-lint.yml\"\n      - \".rumdl.toml\"\n  pull_request:\n    paths:\n      - \"**/*.md\"\n\njobs:\n  rumdl:\n    name: rumdl\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: rvben/rumdl@v0\n        with:\n          report-type: annotations\n"
  },
  {
    "path": ".github/workflows/stale-bot.yml",
    "content": "name: Stale bot\non:\n  schedule:\n    - cron: \"18 04 * * 3\" \n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n  stale:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Close stale issues\n        uses: actions/stale@v10\n        with:\n          repo-token: ${{ secrets.GITHUB_TOKEN }}\n\n          # DOCS https://github.com/actions/stale#all-options\n          days-before-stale: 180\n          days-before-close: 7\n          stale-issue-label: \"Stale\"\n          stale-issue-message: |\n            This issue has been automatically marked as stale.\n            **If this issue is still affecting you, please leave any comment**, for example \"bump\", and it will be kept open.\n          close-issue-message: |\n            This issue has been closed due to inactivity, and will not be monitored.\n"
  },
  {
    "path": ".github/workflows/stylua.yml",
    "content": "name: Stylua check\n\non:\n  push:\n    branches: [main]\n    paths: [\"**.lua\"]\n  pull_request:\n    paths: [\"**.lua\"]\n\njobs:\n  stylua:\n    name: Stylua\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v6\n      - uses: JohnnyMorganz/stylua-action@v5\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          version: latest\n          args: --check .\n"
  },
  {
    "path": ".gitignore",
    "content": "# help-tags auto-generated by lazy.nvim\ndoc/tags\n"
  },
  {
    "path": ".harper-dictionary.txt",
    "content": "genghis\nvimscript\n"
  },
  {
    "path": ".ignore",
    "content": "# auto-generated by panvimdoc\ndoc\n"
  },
  {
    "path": ".luarc.jsonc",
    "content": "{\n\t\"runtime.version\": \"LuaJIT\",\n\n\t\"workspace.library\": [\"$VIMRUNTIME/lua\"], // nvim-lua runtime\n\n\t\"diagnostics\": {\n\t\t\"unusedLocalExclude\": [\"_*\"], // allow `_varname` for unused variables\n\t\t\"groupFileStatus\": {\n\t\t\t\"luadoc\": \"Any\", // require stricter annotations\n\t\t\t\"conventions\": \"Any\" // disallow global variables\n\t\t}\n\t},\n\n\t\"$schema\": \"https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json\"\n}\n"
  },
  {
    "path": ".rumdl.toml",
    "content": "# DOCS https://github.com/rvben/rumdl/blob/main/docs/global-settings.md\n\n[global]\nline-length = 80\ndisable = [\"blanks-around-lists\"]  # rule of proximity\n\n# ------------------------------------------------------------------------------\n\n[ul-style]\nstyle = \"dash\"  # GitHub default & quicker to type\n\n[ul-indent]\nindent = 4  # consistent with .editorconfig\n\n[line-length]\ncode-blocks = false\nreflow = true  # enable auto-formatting\n\n[blanks-around-headings]\nlines-below = 0  # rule of proximity\n\n[ol-prefix]\nstyle = \"ordered\"\n\n[no-inline-html]\nallowed-elements = [\"a\", \"img\", \"kbd\"]  # badges\n\n[emphasis-style]\nstyle = \"asterisk\"  # better than underscore, since not considered a word-char\n\n[strong-style]\nstyle = \"asterisk\"\n\n[table-format]\nenabled = true  # opt-in rule\n\n[heading-capitalization]\nenabled = true  # opt-in rule\nstyle = \"sentence_case\"\nignore-words = [\"nvim\", \"Obsidian\", \"Alfred\"]\n\n[toc-validation]\nenabled = true  # opt-in rule\n\n# ------------------------------------------------------------------------------\n\n[per-file-ignores]\n\".github/pull_request_template.md\" = [\"first-line-h1\"]\n"
  },
  {
    "path": ".stylua.toml",
    "content": "# https://github.com/JohnnyMorganz/StyLua#options\n#───────────────────────────────────────────────────────────────────────────────\nsyntax = \"LuaJIT\"  # needed to support `::labels::`\ncolumn_width = 100\nline_endings = \"Unix\"\nindent_type = \"Tabs\"\nindent_width = 3\nquote_style = \"AutoPreferDouble\"\ncall_parentheses = \"NoSingleTable\"\ncollapse_simple_statement = \"Always\"\nsort_requires.enabled = true\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022-2023 Christopher Grieser\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Nvim-genghis ⚔️ <!-- rumdl-disable-line MD063 -->\n<a href=\"https://dotfyle.com/plugins/chrisgrieser/nvim-genghis\">\n<img alt=\"badge\" src=\"https://dotfyle.com/plugins/chrisgrieser/nvim-genghis/shield\"/></a>\n\nLightweight and quick file operations without being a full-blown file manager.\nFor when you prefer a fuzzy finder over a file tree, but still want some\nconvenient file operations inside nvim.\n\n<img alt=\"Showcase for renaming files\" width=50% src=\"https://github.com/user-attachments/assets/010f3786-e4b2-4c4e-8cbb-a7618de93eb7\">\n\n## Table of contents\n\n<!-- toc -->\n- [Features](#features)\n- [Installation](#installation)\n- [Configuration](#configuration)\n    - [UI plugin](#ui-plugin)\n- [Usage](#usage)\n    - [File operations](#file-operations)\n    - [Copy operations](#copy-operations)\n    - [File navigation](#file-navigation)\n- [Why the name \"Genghis\"?](#why-the-name-genghis)\n- [About the author](#about-the-author)\n<!-- tocstop -->\n\n## Features\n**Commands** <!-- rumdl-disable-line MD036 -->\n- Perform **common file operations**: moving, renaming, creating, deleting, or\n  duplicating files.\n- **Copy** the path or name of the current file in various formats.\n- **Navigate** to the next or previous file in the current folder.\n\n**Quality-of-life** <!-- rumdl-disable-line MD036 -->\n- All movement and renaming commands **update `import` statements** to the\n  renamed file (if the LSP supports `workspace/willRenameFiles`).\n- Automatically keep the extension when no extension is given.\n- Use vim motions in the input field.\n\n## Installation\n**Requirements** <!-- rumdl-disable-line MD036 -->\n- nvim 0.10+\n- *For the trash command*: an OS-specific trash CLI like `trash` or `gio trash`.\n  (Since macOS 14+, there is a `trash` CLI already built-in, so there is no need\n  to install anything.)\n- **Recommended:** A provider for `vim.ui.input` and `vim.ui.select` such as\n  [snacks.nvim](http://github.com/folke/snacks.nvim). This enables vim motions\n  in the input field and looks nicer.\n\n```lua\n-- lazy.nvim\n{ \"chrisgrieser/nvim-genghis\" }\n\n-- packer\nuse { \"chrisgrieser/nvim-genghis\" }\n```\n\n## Configuration\nThe `.setup()` call is optional.\n\n```lua\n-- default config\nrequire(\"genghis\").setup {\n\tfileOperations = {\n\t\t-- automatically keep the extension when no file extension is given\n\t\t-- (everything after the first non-leading dot is treated as the extension)\n\t\tautoAddExt = true,\n\n\t\ttrashCmd = function() ---@type fun(): string|string[]\n\t\t\tif jit.os == \"OSX\" then return \"trash\" end -- builtin since macOS 14\n\t\t\tif jit.os == \"Windows\" then return \"trash\" end\n\t\t\tif jit.os == \"Linux\" then return { \"gio\", \"trash\" } end\n\t\t\treturn \"trash-cli\"\n\t\tend,\n\n\t\tignoreInFolderSelection = { -- using lua pattern matching (e.g., escape `-` as `%-`)\n\t\t\t\"/node_modules/\", -- nodejs\n\t\t\t\"/typings/\", -- python\n\t\t\t\"/doc/\", -- vim help files folders\n\t\t\t\"%.app/\", -- macOS pseudo-folders\n\t\t\t\"/%.\", -- hidden folders\n\t\t},\n\t},\n\n\tnavigation = {\n\t\tonlySameExtAsCurrentFile = false,\n\t\tignoreDotfiles = true,\n\t\tignoreExt = { \"png\", \"svg\", \"webp\", \"jpg\", \"jpeg\", \"gif\", \"pdf\", \"zip\" },\n\t\tignoreFilesWithName = { \".DS_Store\" },\n\t},\n\n\tsuccessNotifications = true,\n\n\ticons = { -- set an icon to empty string to disable it\n\t\tchmodx = \"󰒃\",\n\t\tcopyFile = \"󱉥\",\n\t\tcopyPath = \"󰅍\",\n\t\tduplicate = \"\",\n\t\tfile = \"󰈔\",\n\t\tmove = \"󰪹\",\n\t\tnew = \"󰝒\",\n\t\tnextFile = \"󰖽\",\n\t\tprevFile = \"󰖿\",\n\t\trename = \"󰑕\",\n\t\ttrash = \"󰩹\",\n\t},\n}\n```\n\n### UI plugin\nA UI plugin for `vim.ui.input` and `vim.ui.select`, such as\n[snacks.nvim](http://github.com/folke/snacks.nvim), is recommended since it\nenables for vim motions in the input field. (It also looks much nicer.)\n\n```lua\n-- minimal snacks.nvim config to use it for `vim.ui.input` and `vim.ui.select`\nrequire(\"snacks\").setup({\n\tinput = { enabled = true },\n\tpicker = { enabled = true },\n}),\n```\n\n## Usage\nYou can access a command as Lua function:\n\n```lua\nrequire(\"genghis\").createNewFile()\n```\n\nOr you can use the ex command `:Genghis` with the respective subcommand:\n\n```vim\n:Genghis createNewFile\n```\n\n### File operations\n- `createNewFile`: Create a new file in the same directory as the current file.\n- `createNewFileInFolder`: Create a new file in a folder in the current working\n  directory.\n- `duplicateFile`: Duplicate the current file.\n- `moveSelectionToNewFile`: Create a new file and move the current selection\n  to it. (Visual Line command, the selection is moved linewise.)\n- `renameFile`: Rename the current file.\n- `moveToFolderInCwd`: Move the current file to an existing folder in the\n  current working directory.\n- `moveAndRenameFile`: Move and rename the current file. Keeps the\n  old name if the new path ends with `/`. Works like the UNIX `mv` command.\n- `chmodx`: Makes current file executable. Equivalent to `chmod +x`.\n- `trashFile`: Move the current file to the trash. (Defaults to `gio trash` on\n  *Linux*, and `trash` on *macOS* or *Windows*.)\n- `showInSystemExplorer`: Reveals the current file in the system explorer, such\n  as macOS Finder. (Currently only on macOS, PRs welcome.)\n\nThe following applies to all commands above:\n1. If no extension has been provided, uses the extension of the original file.\n   (Everything after the first non-leading dot is treated as the extension; this\n   behavior can be disabled with the config `fileOperations.autoAddExt =\n   false`.)\n2. If the new filename includes a `/`, the new file is placed in the respective\n   subdirectory, creating any non-existing intermediate folders.\n3. All movement and renaming commands update `import` statements, if the LSP\n   supports `workspace/willRenameFiles`.\n\n### Copy operations\n- `copyFilename`: Copy the filename.\n- `copyFilepath`: Copy the absolute filepath.\n- `copyFilepathWithTilde`: Copy the absolute filepath, replacing the home\n  directory with `~`.\n- `copyRelativePath`: Copy the relative filepath.\n- `copyDirectoryPath`: Copy the absolute directory path.\n- `copyRelativeDirectoryPath`: Copy the relative directory path.\n- `copyFileItself`: Copies the file itself. This means you can paste it into\n  the browser or file manager. (Currently only on macOS, PRs welcome.)\n\nAll commands use the system clipboard.\n\n### File navigation\n`require(\"genghis\").navigateToFileInFolder(\"next\"|\"prev\")`: Move to the\nnext/previous file in the current folder of the current file, in alphabetical\norder.\n\nIf `snacks.nvim` is installed, displays a cycling notification.\n\n## Why the name \"Genghis\"?\nA nod to [vim.eunuch](https://github.com/tpope/vim-eunuch), an older vimscript\nplugin with a similar goal. As opposed to childless eunuchs, it is said that\nGenghis Khan [has fathered thousands of\nchildren](https://allthatsinteresting.com/genghis-khan-children).\n\n## About the author\nIn my day job, I am a sociologist studying the social mechanisms underlying the\ndigital economy. For my PhD project, I investigate the governance of the app\neconomy and how software ecosystems manage the tension between innovation and\ncompatibility. If you are interested in this subject, feel free to get in touch.\n\n- [Website](https://chris-grieser.de/)\n- [Mastodon](https://pkm.social/@pseudometa)\n- [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser)\n- [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/)\n\nIf you find this project helpful, you can support me via [🩷 GitHub\nSponsors](https://github.com/sponsors/chrisgrieser?frequency=one-time).\n"
  },
  {
    "path": "doc/nvim-genghis.txt",
    "content": "*nvim-genghis.txt*           For Neovim          Last change: 2026 February 06\n\n==============================================================================\nTable of Contents                             *nvim-genghis-table-of-contents*\n\n1. Nvim-genghis                                   |nvim-genghis-nvim-genghis-|\n  - Table of contents           |nvim-genghis-nvim-genghis--table-of-contents|\n  - Features                             |nvim-genghis-nvim-genghis--features|\n  - Installation                     |nvim-genghis-nvim-genghis--installation|\n  - Configuration                   |nvim-genghis-nvim-genghis--configuration|\n  - Usage                                   |nvim-genghis-nvim-genghis--usage|\n  - Why the name “Genghis”?|nvim-genghis-nvim-genghis--why-the-name-“genghis”?|\n  - About the author             |nvim-genghis-nvim-genghis--about-the-author|\n\n==============================================================================\n1. Nvim-genghis                                   *nvim-genghis-nvim-genghis-*\n\n\n\nLightweight and quick file operations without being a full-blown file manager.\nFor when you prefer a fuzzy finder over a file tree, but still want some\nconvenient file operations inside nvim.\n\n\n\n\nTABLE OF CONTENTS               *nvim-genghis-nvim-genghis--table-of-contents*\n\n- |nvim-genghis-features|\n- |nvim-genghis-installation|\n- |nvim-genghis-configuration|\n    - |nvim-genghis-ui-plugin|\n- |nvim-genghis-usage|\n    - |nvim-genghis-file-operations|\n    - |nvim-genghis-copy-operations|\n    - |nvim-genghis-file-navigation|\n- |nvim-genghis-why-the-name-\"genghis\"?|\n- |nvim-genghis-about-the-author|\n\n\nFEATURES                                 *nvim-genghis-nvim-genghis--features*\n\n**Commands** - Perform **common file operations**: moving, renaming, creating,\ndeleting, or duplicating files. - **Copy** the path or name of the current file\nin various formats. - **Navigate** to the next or previous file in the current\nfolder.\n\n**Quality-of-life** - All movement and renaming commands **update import\nstatements** to the renamed file (if the LSP supports\n`workspace/willRenameFiles`). - Automatically keep the extension when no\nextension is given. - Use vim motions in the input field.\n\n\nINSTALLATION                         *nvim-genghis-nvim-genghis--installation*\n\n**Requirements** - nvim 0.10+ - _For the trash command_: an OS-specific trash\nCLI like `trash` or `gio trash`. (Since macOS 14+, there is a `trash` CLI\nalready built-in, so there is no need to install anything.) - **Recommended:**\nA provider for `vim.ui.input` and `vim.ui.select` such as snacks.nvim\n<http://github.com/folke/snacks.nvim>. This enables vim motions in the input\nfield and looks nicer.\n\n>lua\n    -- lazy.nvim\n    { \"chrisgrieser/nvim-genghis\" }\n    \n    -- packer\n    use { \"chrisgrieser/nvim-genghis\" }\n<\n\n\nCONFIGURATION                       *nvim-genghis-nvim-genghis--configuration*\n\nThe `.setup()` call is optional.\n\n>lua\n    -- default config\n    require(\"genghis\").setup {\n        fileOperations = {\n            -- automatically keep the extension when no file extension is given\n            -- (everything after the first non-leading dot is treated as the extension)\n            autoAddExt = true,\n    \n            trashCmd = function() ---@type fun(): string|string[]\n                if jit.os == \"OSX\" then return \"trash\" end -- builtin since macOS 14\n                if jit.os == \"Windows\" then return \"trash\" end\n                if jit.os == \"Linux\" then return { \"gio\", \"trash\" } end\n                return \"trash-cli\"\n            end,\n    \n            ignoreInFolderSelection = { -- using lua pattern matching (e.g., escape `-` as `%-`)\n                \"/node_modules/\", -- nodejs\n                \"/typings/\", -- python\n                \"/doc/\", -- vim help files folders\n                \"%.app/\", -- macOS pseudo-folders\n                \"/%.\", -- hidden folders\n            },\n        },\n    \n        navigation = {\n            onlySameExtAsCurrentFile = false,\n            ignoreDotfiles = true,\n            ignoreExt = { \"png\", \"svg\", \"webp\", \"jpg\", \"jpeg\", \"gif\", \"pdf\", \"zip\" },\n            ignoreFilesWithName = { \".DS_Store\" },\n        },\n    \n        successNotifications = true,\n    \n        icons = { -- set an icon to empty string to disable it\n            chmodx = \"󰒃\",\n            copyFile = \"󱉥\",\n            copyPath = \"󰅍\",\n            duplicate = \"\",\n            file = \"󰈔\",\n            move = \"󰪹\",\n            new = \"󰝒\",\n            nextFile = \"󰖽\",\n            prevFile = \"󰖿\",\n            rename = \"󰑕\",\n            trash = \"󰩹\",\n        },\n    }\n<\n\n\nUI PLUGIN ~\n\nA UI plugin for `vim.ui.input` and `vim.ui.select`, such as snacks.nvim\n<http://github.com/folke/snacks.nvim>, is recommended since it enables for vim\nmotions in the input field. (It also looks much nicer.)\n\n>lua\n    -- minimal snacks.nvim config to use it for `vim.ui.input` and `vim.ui.select`\n    require(\"snacks\").setup({\n        input = { enabled = true },\n        picker = { enabled = true },\n    }),\n<\n\n\nUSAGE                                       *nvim-genghis-nvim-genghis--usage*\n\nYou can access a command as Lua function:\n\n>lua\n    require(\"genghis\").createNewFile()\n<\n\nOr you can use the ex command `:Genghis` with the respective subcommand:\n\n>vim\n    :Genghis createNewFile\n<\n\n\nFILE OPERATIONS ~\n\n- `createNewFile`: Create a new file in the same directory as the current file.\n- `createNewFileInFolder`: Create a new file in a folder in the current working\n    directory.\n- `duplicateFile`: Duplicate the current file.\n- `moveSelectionToNewFile`: Create a new file and move the current selection\n    to it. (Visual Line command, the selection is moved linewise.)\n- `renameFile`: Rename the current file.\n- `moveToFolderInCwd`: Move the current file to an existing folder in the\n    current working directory.\n- `moveAndRenameFile`: Move and rename the current file. Keeps the\n    old name if the new path ends with `/`. Works like the UNIX `mv` command.\n- `chmodx`: Makes current file executable. Equivalent to `chmod +x`.\n- `trashFile`: Move the current file to the trash. (Defaults to `gio trash` on\n    _Linux_, and `trash` on _macOS_ or _Windows_.)\n- `showInSystemExplorer`: Reveals the current file in the system explorer, such\n    as macOS Finder. (Currently only on macOS, PRs welcome.)\n\nThe following applies to all commands above: 1. If no extension has been\nprovided, uses the extension of the original file. (Everything after the first\nnon-leading dot is treated as the extension; this behavior can be disabled with\nthe config `fileOperations.autoAddExt = false`.) 2. If the new filename\nincludes a `/`, the new file is placed in the respective subdirectory, creating\nany non-existing intermediate folders. 3. All movement and renaming commands\nupdate `import` statements, if the LSP supports `workspace/willRenameFiles`.\n\n\nCOPY OPERATIONS ~\n\n- `copyFilename`: Copy the filename.\n- `copyFilepath`: Copy the absolute filepath.\n- `copyFilepathWithTilde`: Copy the absolute filepath, replacing the home\n    directory with `~`.\n- `copyRelativePath`: Copy the relative filepath.\n- `copyDirectoryPath`: Copy the absolute directory path.\n- `copyRelativeDirectoryPath`: Copy the relative directory path.\n- `copyFileItself`: Copies the file itself. This means you can paste it into\n    the browser or file manager. (Currently only on macOS, PRs welcome.)\n\nAll commands use the system clipboard.\n\n\nFILE NAVIGATION ~\n\n`require(\"genghis\").navigateToFileInFolder(\"next\"|\"prev\")`: Move to the\nnext/previous file in the current folder of the current file, in alphabetical\norder.\n\nIf `snacks.nvim` is installed, displays a cycling notification.\n\n\nWHY THE NAME “GENGHIS”?*nvim-genghis-nvim-genghis--why-the-name-“genghis”?*\n\nA nod to vim.eunuch <https://github.com/tpope/vim-eunuch>, an older vimscript\nplugin with a similar goal. As opposed to childless eunuchs, it is said that\nGenghis Khan has fathered thousands of children\n<https://allthatsinteresting.com/genghis-khan-children>.\n\n\nABOUT THE AUTHOR                 *nvim-genghis-nvim-genghis--about-the-author*\n\nIn my day job, I am a sociologist studying the social mechanisms underlying the\ndigital economy. For my PhD project, I investigate the governance of the app\neconomy and how software ecosystems manage the tension between innovation and\ncompatibility. If you are interested in this subject, feel free to get in\ntouch.\n\n- Website <https://chris-grieser.de/>\n- Mastodon <https://pkm.social/@pseudometa>\n- ResearchGate <https://www.researchgate.net/profile/Christopher-Grieser>\n- LinkedIn <https://www.linkedin.com/in/christopher-grieser-ba693b17a/>\n\nIf you find this project helpful, you can support me via GitHub Sponsors\n<https://github.com/sponsors/chrisgrieser?frequency=one-time>.\n\nGenerated by panvimdoc <https://github.com/kdheepak/panvimdoc>\n\nvim:tw=78:ts=8:noet:ft=help:norl:\n"
  },
  {
    "path": "lua/genghis/config.lua",
    "content": "local M = {}\n--------------------------------------------------------------------------------\n\n---@class Genghis.config\nlocal defaultConfig = {\n\tfileOperations = {\n\t\t-- automatically keep the extension when no file extension is given\n\t\t-- (everything after the first non-leading dot is treated as the extension)\n\t\tautoAddExt = true,\n\n\t\ttrashCmd = function() ---@type fun(): string|string[]\n\t\t\tif jit.os == \"OSX\" then return \"trash\" end -- builtin since macOS 14\n\t\t\tif jit.os == \"Windows\" then return \"trash\" end\n\t\t\tif jit.os == \"Linux\" then return { \"gio\", \"trash\" } end\n\t\t\treturn \"trash-cli\"\n\t\tend,\n\n\t\tignoreInFolderSelection = { -- using lua pattern matching (e.g., escape `-` as `%-`)\n\t\t\t\"/node_modules/\", -- nodejs\n\t\t\t\"/typings/\", -- python\n\t\t\t\"/doc/\", -- vim help files folders\n\t\t\t\"%.app/\", -- macOS pseudo-folders\n\t\t\t\"/%.\", -- hidden folders\n\t\t},\n\t},\n\n\tnavigation = {\n\t\tonlySameExtAsCurrentFile = false,\n\t\tignoreDotfiles = true,\n\t\tignoreExt = { \"png\", \"svg\", \"webp\", \"jpg\", \"jpeg\", \"gif\", \"pdf\", \"zip\" },\n\t\tignoreFilesWithName = { \".DS_Store\" },\n\t},\n\n\tsuccessNotifications = true,\n\n\ticons = { -- set an icon to empty string to disable it\n\t\tchmodx = \"󰒃\",\n\t\tcopyFile = \"󱉥\",\n\t\tcopyPath = \"󰅍\",\n\t\tduplicate = \"\",\n\t\tfile = \"󰈔\",\n\t\tmove = \"󰪹\",\n\t\tnew = \"󰝒\",\n\t\tnextFile = \"󰖽\",\n\t\tprevFile = \"󰖿\",\n\t\trename = \"󰑕\",\n\t\ttrash = \"󰩹\",\n\t},\n}\nM.config = defaultConfig\n\n---@param userConfig? Genghis.config\nfunction M.setup(userConfig)\n\tM.config = vim.tbl_deep_extend(\"force\", defaultConfig, userConfig or {})\n\n\t-- DEPRECATION (2025-11-24)\n\t---@diagnostic disable: undefined-field\n\tif M.config.trashCmd then\n\t\tM.config.fileOperations.trashCmd = M.config.trashCmd\n\t\tlocal notify = require(\"genghis.support.notify\")\n\t\tnotify(\"config `.trashCmd` is deprecated, use `.fileOperations.trashCmd` instead.\", \"warn\")\n\tend\n\t---@diagnostic enable: undefined-field\nend\n\n--------------------------------------------------------------------------------\nreturn M\n"
  },
  {
    "path": "lua/genghis/init.lua",
    "content": "local version = vim.version()\nif version.major == 0 and version.minor < 10 then\n\tvim.notify(\"nvim-genghis requires at least nvim 0.10.\", vim.log.levels.WARN)\n\treturn\nend\n--------------------------------------------------------------------------------\n\nlocal M = {}\n\n---@param userConfig? Genghis.config\nfunction M.setup(userConfig) require(\"genghis.config\").setup(userConfig) end\n\n---@param direction? \"next\"|\"prev\"\nfunction M.navigateToFileInFolder(direction)\n\trequire(\"genghis.operations.navigation\").fileInFolder(direction)\nend\n\n-- redirect calls to this module to the respective submodules\nsetmetatable(M, {\n\t__index = function(_, key)\n\t\treturn function(...)\n\t\t\tlocal fileOps = vim.tbl_keys(require(\"genghis.operations.file\"))\n\t\t\tlocal copyOps = vim.tbl_keys(require(\"genghis.operations.copy\"))\n\n\t\t\tlocal module\n\t\t\tif vim.tbl_contains(fileOps, key) then module = \"file\" end\n\t\t\tif vim.tbl_contains(copyOps, key) then module = \"copy\" end\n\n\t\t\tif module then\n\t\t\t\trequire(\"genghis.operations.\" .. module)[key](...)\n\t\t\telse\n\t\t\t\tlocal notify = require(\"genghis.support.notify\")\n\t\t\t\tlocal msg = (\"There is no operation called `%s`.\\n\\n\"):format(key)\n\t\t\t\t\t.. \"Make sure it exists in the list of operations, and that you haven't misspelled it.\"\n\t\t\t\tnotify(msg, \"error\", { ft = \"markdown\" })\n\t\t\tend\n\t\tend\n\tend,\n})\n\n--------------------------------------------------------------------------------\nreturn M\n"
  },
  {
    "path": "lua/genghis/operations/copy.lua",
    "content": "local M = {}\n--------------------------------------------------------------------------------\n\n---@param expandOperation string\nlocal function copyOp(expandOperation)\n\tlocal icon = require(\"genghis.config\").config.icons.copyPath\n\n\tlocal register = \"+\"\n\tlocal toCopy = vim.fn.expand(expandOperation)\n\tvim.fn.setreg(register, toCopy)\n\n\tlocal notify = require(\"genghis.support.notify\")\n\tnotify(toCopy, \"info\", { title = \"Copied\", icon = icon })\nend\n\n-- DOCS for the modifiers\n-- https://neovim.io/doc/user/builtin.html#expand()\n-- https://neovim.io/doc/user/cmdline.html#filename-modifiers\nfunction M.copyFilepath() copyOp(\"%:p\") end\nfunction M.copyFilepathWithTilde() copyOp(\"%:~\") end\nfunction M.copyFilename() copyOp(\"%:t\") end\nfunction M.copyRelativePath() copyOp(\"%:.\") end\nfunction M.copyDirectoryPath() copyOp(\"%:p:h\") end\nfunction M.copyRelativeDirectoryPath() copyOp(\"%:.:h\") end\n\nfunction M.copyFileItself()\n\tlocal notify = require(\"genghis.support.notify\")\n\tif jit.os ~= \"OSX\" then\n\t\tnotify(\"Currently only available on macOS.\", \"warn\")\n\t\treturn\n\tend\n\n\tlocal icon = require(\"genghis.config\").config.icons.copyFile\n\tlocal path = vim.api.nvim_buf_get_name(0)\n\tlocal applescript = 'tell application \"Finder\" to set the clipboard to '\n\t\t.. (\"POSIX file %q\"):format(path)\n\n\tvim.system({ \"osascript\", \"-e\", applescript }, {}, function(out)\n\t\tif out.code ~= 0 then\n\t\t\tnotify(\"Failed to copy file: \" .. out.stderr, \"error\", { title = \"Copy file\" })\n\t\telse\n\t\t\tnotify(vim.fs.basename(path), \"info\", { title = \"Copied file\", icon = icon })\n\t\tend\n\tend)\nend\n\n--------------------------------------------------------------------------------\nreturn M\n"
  },
  {
    "path": "lua/genghis/operations/file.lua",
    "content": "local M = {}\n--------------------------------------------------------------------------------\n\n---@param op \"rename\"|\"duplicate\"|\"new\"|\"new-from-selection\"|\"move-rename\"\n---@param targetDir? string\nlocal function fileOp(op, targetDir)\n\tlocal moveConsideringPartition = require(\"genghis.support.move-considering-partition\")\n\tlocal notify = require(\"genghis.support.notify\")\n\tlocal lspRename = require(\"genghis.support.lsp-rename\")\n\n\t-- PARAMETERS\n\tlocal origBufNr = vim.api.nvim_get_current_buf()\n\tlocal oldFilePath = vim.api.nvim_buf_get_name(0)\n\tlocal oldName = vim.fs.basename(oldFilePath)\n\tlocal pathSep = package.config:sub(1, 1)\n\tif not targetDir then targetDir = vim.fs.dirname(oldFilePath) end\n\n\t-- * non-greedy 1st capture, so 2nd capture matches double-extensions (see #60)\n\t-- * 1st capture requires at least one char, to not match empty string for dotfiles\n\tlocal oldNameNoExt, oldExt = oldName:match(\"(..-)(%.[%w.]*)\")\n\t-- handle files without extension\n\tif not oldNameNoExt then oldNameNoExt = oldName end\n\tif not oldExt then oldExt = \"\" end\n\n\tlocal autoAddExt = require(\"genghis.config\").config.fileOperations.autoAddExt\n\tlocal icons = require(\"genghis.config\").config.icons\n\tlocal lspSupportsRenaming = lspRename.supported()\n\n\t-- PREPARE\n\tlocal prompt, prefill\n\tif op == \"duplicate\" then\n\t\tprompt = icons.duplicate .. \" Duplicate file as: \"\n\t\tprefill = (autoAddExt and oldNameNoExt or oldName) .. \"-1\"\n\telseif op == \"rename\" then\n\t\tlocal text = lspSupportsRenaming and \"Rename file & update imports:\" or \"Rename file to:\"\n\t\tprompt = icons.rename .. \" \" .. text\n\t\tprefill = autoAddExt and oldNameNoExt or oldName\n\telseif op == \"move-rename\" then\n\t\tlocal text = lspSupportsRenaming and \" Move and rename file & update imports:\"\n\t\t\tor \" Move & rename file to:\"\n\t\tprompt = icons.rename .. \" \" .. text\n\t\tprefill = targetDir .. pathSep\n\telseif op == \"new\" or op == \"new-from-selection\" then\n\t\tprompt = icons.new .. \" Name for new file: \"\n\t\tprefill = \"\"\n\tend\n\n\t-- INPUT\n\tvim.ui.input({\n\t\tprompt = vim.trim(prompt),\n\t\tdefault = prefill,\n\t}, function(newName)\n\t\tvim.cmd.redraw() -- clear message area from vim.ui.input prompt\n\t\tif not newName then return end -- input has been canceled\n\n\t\tif op == \"move-rename\" and vim.endswith(newName, pathSep) then -- user just provided a folder\n\t\t\tnewName = newName .. oldName\n\t\telseif (op == \"new\" or op == \"new-from-selection\") and newName == \"\" then\n\t\t\tnewName = \"Untitled\"\n\t\tend\n\n\t\t-- GUARD validate filename\n\t\tlocal invalidName = newName:find(\"^%s+$\")\n\t\t\tor newName:find(\":\")\n\t\t\tor (vim.startswith(newName, pathSep) and op ~= \"move-rename\")\n\t\tlocal sameName = newName == oldName\n\t\tlocal emptyInput = newName == \"\"\n\t\tif invalidName or sameName or emptyInput then\n\t\t\tif invalidName or emptyInput then\n\t\t\t\tnotify(\"Invalid filename.\", \"error\")\n\t\t\telseif sameName then\n\t\t\t\tnotify(\"Cannot use the same filename.\", \"warn\")\n\t\t\tend\n\t\t\treturn\n\t\tend\n\n\t\t-- DETERMINE PATH AND EXTENSION\n\t\tif newName:find(pathSep) then\n\t\t\tlocal newFolder = vim.fs.dirname(newName)\n\t\t\tlocal absFolder = op == \"move-rename\" and newFolder\n\t\t\t\tor vim.fs.joinpath(targetDir, newFolder)\n\t\t\tvim.fn.mkdir(absFolder, \"p\")\n\t\tend\n\n\t\tlocal userProvidedNoExt = newName:find(\".%.[^/]*$\") == nil -- non-leading dot to not include dotfiles without extension\n\t\tif userProvidedNoExt and autoAddExt then newName = newName .. oldExt end\n\n\t\tlocal newFilePath = op == \"move-rename\" and newName or vim.fs.joinpath(targetDir, newName)\n\t\tif vim.uv.fs_stat(newFilePath) ~= nil then\n\t\t\tnotify((\"File with name %q already exists.\"):format(newFilePath), \"error\")\n\t\t\treturn\n\t\tend\n\n\t\t-- EXECUTE FILE OPERATION\n\t\tvim.cmd(\"silent! update\")\n\t\tif op == \"duplicate\" then\n\t\t\tlocal success = vim.uv.fs_copyfile(oldFilePath, newFilePath)\n\t\t\tif not success then return end\n\t\t\tvim.cmd.edit(newFilePath)\n\t\t\tvim.cmd(\"silent! write\")\n\t\t\tlocal msg = (\"Duplicated %q as %q.\"):format(oldName, newName)\n\t\t\tnotify(msg, \"info\", { icon = icons.duplicate })\n\t\telseif op == \"rename\" or op == \"move-rename\" then\n\t\t\tlspRename.willRename(oldFilePath, newFilePath)\n\t\t\tlocal success = moveConsideringPartition(oldFilePath, newFilePath)\n\t\t\tif not success then return end\n\t\t\tvim.cmd.edit(newFilePath)\n\t\t\tvim.api.nvim_buf_delete(origBufNr, { force = true })\n\t\t\tlocal msg = (\"Renamed %q to %q.\"):format(oldName, newName)\n\t\t\tnotify(msg, \"info\", { icon = icons.rename })\n\t\t\tvim.cmd(lspSupportsRenaming and \"wall\" or \"silent! write\")\n\t\telseif op == \"new\" then\n\t\t\tvim.cmd.edit(newFilePath)\n\t\t\tvim.cmd.write(newFilePath)\n\t\telseif op == \"new-from-selection\" then\n\t\t\tlocal prevReg = vim.fn.getreg(\"z\")\n\t\t\tvim.cmd([['<,'>delete z]]) -- will have already left visual for input, so '<,'> are set\n\n\t\t\tvim.cmd.edit(newFilePath)\n\t\t\tvim.cmd(\"put z\") -- `vim.cmd.put(\"z\")` does not work\n\t\t\tvim.fn.setreg(\"z\", prevReg)\n\t\t\tvim.cmd.write(newFilePath)\n\t\tend\n\tend)\nend\n\nfunction M.renameFile() fileOp(\"rename\") end\nfunction M.moveAndRenameFile() fileOp(\"move-rename\") end\nfunction M.duplicateFile() fileOp(\"duplicate\") end\nfunction M.createNewFile() fileOp(\"new\") end\nfunction M.moveSelectionToNewFile() fileOp(\"new-from-selection\") end\n\n--------------------------------------------------------------------------------\n\n---@param op \"move-file\"|\"new-in-folder\"\nlocal function folderSelection(op)\n\tlocal moveConsideringPartition = require(\"genghis.support.move-considering-partition\")\n\tlocal notify = require(\"genghis.support.notify\")\n\tlocal lspRenaming = require(\"genghis.support.lsp-rename\")\n\tlocal ignoreFolders = require(\"genghis.config\").config.fileOperations.ignoreInFolderSelection\n\tlocal icons = require(\"genghis.config\").config.icons\n\n\t-- PARAMETERS\n\tlocal oldAbsPath = vim.api.nvim_buf_get_name(0)\n\tlocal oldAbsParent = vim.fs.dirname(oldAbsPath)\n\tlocal filename = vim.fs.basename(oldAbsPath)\n\tlocal lspSupportsRenaming = lspRenaming.supported()\n\tlocal cwd = assert(vim.uv.cwd(), \"Could not get current working directory.\")\n\tlocal origBufNr = vim.api.nvim_get_current_buf()\n\n\t-- GET OTHER FOLDERS IN CWD\n\tlocal foldersInCwd = vim.fs.find(function(name, path)\n\t\tlocal absPath = vim.fs.joinpath(path, name)\n\t\tlocal relPath = absPath:sub(#cwd + 1) .. \"/\" -- not pathSep, since `joinpath` uses `/`\n\n\t\tlocal sameFolder = absPath == oldAbsParent\n\t\tlocal ignoredDir = vim.iter(ignoreFolders)\n\t\t\t:any(function(dir) return relPath:find(dir) ~= nil end)\n\n\t\treturn not (ignoredDir or sameFolder)\n\tend, { type = \"directory\", limit = math.huge })\n\n\t-- ORDER OF FOLDERS\n\ttable.sort(foldersInCwd, function(a, b)\n\t\tlocal aMtime = vim.uv.fs_stat(a).mtime.sec\n\t\tlocal bMtime = vim.uv.fs_stat(b).mtime.sec\n\t\treturn aMtime > bMtime\n\tend)\n\t-- insert cwd at bottom, since moving to it unlikely\n\tif cwd ~= oldAbsParent then table.insert(foldersInCwd, cwd) end\n\t-- insert current dir at top, since moving to it likely\n\tif op == \"new-in-folder\" then table.insert(foldersInCwd, 1, oldAbsParent) end\n\n\t-- PROMPT & MOVE\n\tlocal prompt\n\tif op == \"move-file\" then\n\t\tprompt = icons.move .. \" Move file to\"\n\t\tif lspSupportsRenaming then prompt = prompt .. \" (with updated imports)\" end\n\t\tprompt = prompt .. \":\"\n\telseif op == \"new-in-folder\" then\n\t\tprompt = icons.new .. \" Folder for new file:\"\n\tend\n\tvim.ui.select(foldersInCwd, {\n\t\tprompt = prompt,\n\t\tkind = \"genghis.select-folder\",\n\t\tformat_item = function(path)\n\t\t\tlocal relPath = path:sub(#cwd + 1)\n\t\t\treturn (relPath == \"\" and \"/\" or relPath)\n\t\tend,\n\t}, function(newAbsParent)\n\t\tif not newAbsParent then return end\n\t\tlocal newRelParent = newAbsParent:sub(#cwd + 1)\n\t\tnewRelParent = newRelParent == \"\" and \"/\" or newRelParent\n\n\t\tif op == \"new-in-folder\" then\n\t\t\tfileOp(\"new\", newAbsParent)\n\t\telseif op == \"move-file\" then\n\t\t\tlocal newAbsPath = vim.fs.joinpath(newAbsParent, filename)\n\t\t\tif vim.uv.fs_stat(newAbsPath) ~= nil then\n\t\t\t\tnotify((\"File %q already exists at %q.\"):format(filename, newRelParent), \"error\")\n\t\t\t\treturn\n\t\t\tend\n\n\t\t\tvim.cmd(\"silent! update\")\n\t\t\tlspRenaming.willRename(oldAbsPath, newAbsPath)\n\t\t\tlocal success = moveConsideringPartition(oldAbsPath, newAbsPath)\n\t\t\tif not success then return end\n\n\t\t\tvim.cmd.edit(newAbsPath)\n\t\t\tvim.api.nvim_buf_delete(origBufNr, { force = true })\n\t\t\tlocal msg = (\"Moved %q to %q\"):format(filename, newRelParent)\n\t\t\tlocal append = lspSupportsRenaming and \" and updated imports.\" or \".\"\n\t\t\tnotify(msg .. append, \"info\", { icon = icons.move })\n\t\t\tvim.cmd(lspSupportsRenaming and \"wall\" or \"silent! write\")\n\t\tend\n\tend)\nend\n\nfunction M.moveToFolderInCwd() folderSelection(\"move-file\") end\nfunction M.createNewFileInFolder() folderSelection(\"new-in-folder\") end\n\n--------------------------------------------------------------------------------\n\nfunction M.chmodx()\n\tlocal icons = require(\"genghis.config\").config.icons\n\n\tlocal filepath = vim.api.nvim_buf_get_name(0)\n\tlocal perm = vim.fn.getfperm(filepath)\n\tperm = perm:gsub(\"r(.)%-\", \"r%1x\") -- add x to every group that has r\n\tvim.fn.setfperm(filepath, perm)\n\n\tlocal notify = require(\"genghis.support.notify\")\n\tnotify(\"Permission +x granted.\", \"info\", { icon = icons.chmodx })\n\tvim.cmd.edit() -- reload the file\nend\n\nfunction M.trashFile()\n\tvim.cmd(\"silent! update\")\n\tlocal filepath = vim.api.nvim_buf_get_name(0)\n\tlocal filename = vim.fs.basename(filepath)\n\tlocal icon = require(\"genghis.config\").config.icons.trash\n\tlocal trashCmd = require(\"genghis.config\").config.fileOperations.trashCmd\n\n\t-- execute the trash command\n\tlocal cmd = trashCmd()\n\tif type(cmd) ~= \"table\" then cmd = { cmd } end\n\ttable.insert(cmd, filepath)\n\tlocal out = vim.system(cmd):wait()\n\n\t-- handle the result\n\tlocal notify = require(\"genghis.support.notify\")\n\tif out.code == 0 then\n\t\tvim.api.nvim_buf_delete(0, { force = true })\n\t\tnotify((\"%q moved to trash.\"):format(filename), \"info\", { icon = icon })\n\telse\n\t\tlocal outmsg = (out.stdout or \"\") .. (out.stderr or \"\")\n\t\tnotify((\"Trashing %q failed: %s\"):format(filename, outmsg), \"error\")\n\tend\nend\n\nfunction M.showInSystemExplorer()\n\tlocal notify = require(\"genghis.support.notify\")\n\tif jit.os ~= \"OSX\" then\n\t\tnotify(\"Currently only available on macOS.\", \"warn\")\n\t\treturn\n\tend\n\n\tlocal out = vim.system({ \"open\", \"-R\", vim.api.nvim_buf_get_name(0) }):wait()\n\tif out.code ~= 0 then\n\t\tlocal icon = require(\"genghis.config\").config.icons.file\n\t\tnotify(\"Failed: \" .. out.stderr, \"error\", { icon = icon })\n\tend\nend\n--------------------------------------------------------------------------------\nreturn M\n"
  },
  {
    "path": "lua/genghis/operations/navigation.lua",
    "content": "local M = {}\n--------------------------------------------------------------------------------\n\n---Cycles files in folder in alphabetical order.\n---If snacks.nvim is installed, adds cycling notification.\n---@param direction? \"next\"|\"prev\"\nfunction M.fileInFolder(direction)\n\tlocal notify = require(\"genghis.support.notify\")\n\n\tif not direction then direction = \"next\" end\n\tif direction ~= \"next\" and direction ~= \"prev\" then\n\t\tnotify('Invalid direction. Only \"next\" and \"prev\" are allowed.', \"warn\")\n\t\treturn\n\tend\n\n\tlocal config = require(\"genghis.config\").config\n\tlocal curPath = vim.api.nvim_buf_get_name(0)\n\tlocal curFile = vim.fs.basename(curPath)\n\tlocal curFolder = vim.fs.dirname(curPath)\n\tlocal icon = direction == \"next\" and config.icons.nextFile or config.icons.prevFile\n\n\t-- get list of files\n\tlocal itemsInFolder = vim.fs.dir(curFolder) -- INFO `fs.dir` already returns them sorted\n\tlocal filesInFolder = vim.iter(itemsInFolder):fold({}, function(acc, name, type)\n\t\tlocal ext = name:match(\"%.(%w+)$\")\n\t\tlocal curExt = curFile:match(\"%.(%w+)$\")\n\n\t\tlocal ignored = (config.navigation.onlySameExtAsCurrentFile and ext ~= curExt)\n\t\t\tor vim.tbl_contains(config.navigation.ignoreExt, ext)\n\t\t\tor (config.navigation.ignoreDotfiles and vim.startswith(name, \".\"))\n\t\t\tor vim.tbl_contains(config.navigation.ignoreFilesWithName, name)\n\n\t\tif type == \"file\" and not ignored then\n\t\t\ttable.insert(acc, name) -- select only name\n\t\tend\n\t\treturn acc\n\tend)\n\n\t-- GUARD no files to navigate to\n\tif #filesInFolder == 0 then -- if currently at a hidden file and there are only hidden files in the dir\n\t\tnotify(\"No valid files found in folder.\", \"warn\", { icon = icon })\n\t\treturn\n\telseif #filesInFolder == 1 then\n\t\tnotify(\"Already at the only valid file.\", \"warn\", { icon = icon })\n\t\treturn\n\tend\n\n\t-- determine next index\n\tlocal curIdx\n\tfor idx = 1, #filesInFolder do\n\t\tif filesInFolder[idx] == curFile then\n\t\t\tcurIdx = idx\n\t\t\tbreak\n\t\tend\n\tend\n\tif not curIdx then\n\t\tlocal msg = \"Cannot determine next file, current file itself is excluded.\"\n\t\tnotify(msg, \"warn\", { icon = icon })\n\t\treturn\n\tend\n\tlocal nextIdx = curIdx + (direction == \"next\" and 1 or -1)\n\tif nextIdx < 1 then nextIdx = #filesInFolder end\n\tif nextIdx > #filesInFolder then nextIdx = 1 end\n\n\t-- goto file\n\tlocal nextFile = curFolder .. \"/\" .. filesInFolder[nextIdx]\n\tvim.cmd.edit(nextFile)\n\n\t-- notification\n\tif package.loaded[\"snacks\"] then\n\t\tlocal msg = vim\n\t\t\t.iter(filesInFolder)\n\t\t\t:map(function(file)\n\t\t\t\t-- mark current, using markdown h1\n\t\t\t\tlocal prefix = file == filesInFolder[nextIdx] and \"#\" or \"-\"\n\t\t\t\treturn prefix .. \" \" .. file\n\t\t\tend)\n\t\t\t:slice(nextIdx - 5, nextIdx + 5) -- display ~5 files before/after\n\t\t\t:join(\"\\n\")\n\t\tlocal title = direction:sub(1, 1):upper()\n\t\t\t.. direction:sub(2)\n\t\t\t.. \" file\"\n\t\t\t.. (\" (%d/%d)\"):format(nextIdx, #filesInFolder)\n\t\tvim.notify(msg, nil, {\n\t\t\ttitle = title,\n\t\t\ticon = icon,\n\t\t\thistory = false,\n\t\t\tid = \"next-in-folder\", -- replace notifications when quickly cycling\n\t\t\tft = \"markdown\", -- so `h1` is highlighted\n\t\t})\n\tend\nend\n\n--------------------------------------------------------------------------------\nreturn M\n"
  },
  {
    "path": "lua/genghis/support/lsp-rename.lua",
    "content": "local M = {}\n\n--------------------------------------------------------------------------------\n\n---Requests a 'workspace/willRenameFiles' on any running LSP client, that supports it\n---SOURCE https://github.com/LazyVim/LazyVim/blob/ac092289f506052cfdd1879f462be05075fe3081/lua/lazyvim/util/lsp.lua#L99-L119\n---@param fromName string\n---@param toName string\nfunction M.willRename(fromName, toName)\n\tlocal clients = vim.lsp.get_clients { bufnr = 0 }\n\tfor _, client in ipairs(clients) do\n\t\tif client:supports_method(\"workspace/willRenameFiles\") then\n\t\t\tlocal response = client:request_sync(\"workspace/willRenameFiles\", {\n\t\t\t\tfiles = {\n\t\t\t\t\t{ oldUri = vim.uri_from_fname(fromName), newUri = vim.uri_from_fname(toName) },\n\t\t\t\t},\n\t\t\t}, 1000, 0)\n\t\t\tif response and response.result ~= nil then\n\t\t\t\tvim.lsp.util.apply_workspace_edit(response.result, client.offset_encoding)\n\t\t\tend\n\t\tend\n\tend\nend\n\n---@nodiscard\n---@return boolean\nfunction M.supported()\n\tlocal clients = vim.lsp.get_clients { bufnr = 0 }\n\tfor _, client in ipairs(clients) do\n\t\tif client:supports_method(\"workspace/willRenameFiles\") then return true end\n\tend\n\treturn false\nend\n\n--------------------------------------------------------------------------------\nreturn M\n"
  },
  {
    "path": "lua/genghis/support/move-considering-partition.lua",
    "content": "---@param oldFilePath string\n---@param newFilePath string\n---@return boolean success\nreturn function(oldFilePath, newFilePath)\n\tlocal renamed, _ = vim.uv.fs_rename(oldFilePath, newFilePath)\n\tif renamed then return true end\n\n\tlocal notify = require(\"genghis.support.notify\")\n\n\t-- try `fs_copyfile` to support moving across partitions\n\tlocal copied, copiedError = vim.uv.fs_copyfile(oldFilePath, newFilePath)\n\tif copied then\n\t\tlocal deleted, deletedError = vim.uv.fs_unlink(oldFilePath)\n\t\tif deleted then\n\t\t\treturn true\n\t\telse\n\t\t\tnotify((\"Failed to delete %q: %q\"):format(oldFilePath, deletedError), \"error\")\n\t\t\treturn false\n\t\tend\n\telse\n\t\tlocal msg = (\"Failed to copy %q to %q: %q\"):format(oldFilePath, newFilePath, copiedError)\n\t\tnotify(msg, \"error\")\n\t\treturn false\n\tend\nend\n"
  },
  {
    "path": "lua/genghis/support/notify.lua",
    "content": "---@param msg string\n---@param level? \"info\"|\"warn\"|\"error\"\n---@param opts? table\nreturn function(msg, level, opts)\n\tlocal successNotify = require(\"genghis.config\").config.successNotifications\n\tif not level then level = \"info\" end\n\tif level == \"info\" and not successNotify then return end\n\tif not opts then opts = {} end\n\n\topts.title = opts.title and \"Genghis: \" .. opts.title or \"Genghis\"\n\tif not opts.ft then opts.ft = \"text\" end -- prevent `~` from creating strikethroughs in `snacks.notifier`\n\tvim.notify(msg, vim.log.levels[level:upper()], opts)\nend\n"
  },
  {
    "path": "plugin/ex-commands.lua",
    "content": "vim.api.nvim_create_user_command(\"Genghis\", function(ctx) require(\"genghis\")[ctx.args]() end, {\n\tnargs = 1,\n\tcomplete = function(query)\n\t\tlocal allOps = {}\n\t\tvim.list_extend(allOps, vim.tbl_keys(require(\"genghis.operations.file\")))\n\t\tvim.list_extend(allOps, vim.tbl_keys(require(\"genghis.operations.copy\")))\n\t\tvim.list_extend(allOps, vim.tbl_keys(require(\"genghis.operations.navigation\")))\n\t\treturn vim.tbl_filter(function(op) return op:lower():find(query, nil, true) end, allOps)\n\tend,\n})\n"
  }
]