Full Code of chrisgrieser/nvim-genghis for AI

main 12d62b0aaeb0 cached
32 files
46.7 KB
13.4k tokens
1 requests
Download .txt
Repository: chrisgrieser/nvim-genghis
Branch: main
Commit: 12d62b0aaeb0
Files: 32
Total size: 46.7 KB

Directory structure:
gitextract_lchxmgi1/

├── .editorconfig
├── .emmyrc.json
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── nvim-type-check.yml
│       ├── panvimdoc.yml
│       ├── pr-title.yml
│       ├── rumdl-lint.yml
│       ├── stale-bot.yml
│       └── stylua.yml
├── .gitignore
├── .harper-dictionary.txt
├── .ignore
├── .luarc.jsonc
├── .rumdl.toml
├── .stylua.toml
├── LICENSE
├── README.md
├── doc/
│   └── nvim-genghis.txt
├── lua/
│   └── genghis/
│       ├── config.lua
│       ├── init.lua
│       ├── operations/
│       │   ├── copy.lua
│       │   ├── file.lua
│       │   └── navigation.lua
│       └── support/
│           ├── lsp-rename.lua
│           ├── move-considering-partition.lua
│           └── notify.lua
└── plugin/
    └── ex-commands.lua

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
max_line_length = 100
end_of_line = lf
charset = utf-8
insert_final_newline = true
indent_style = tab
indent_size = 3
tab_width = 3
trim_trailing_whitespace = true

[*.{yml,yaml,scm,cff}]
indent_style = space
indent_size = 2
tab_width = 2

[*.py]
indent_style = space
indent_size = 4
tab_width = 4

[*.md]
indent_style = space
indent_size = 4
trim_trailing_whitespace = false


================================================
FILE: .emmyrc.json
================================================
{
	"runtime": {
		"version": "LuaJIT",
		"requirePattern": ["lua/?.lua", "lua/?/init.lua"]
	},
	"workspace": {
		"library": ["$VIMRUNTIME"]
	},
	"$schema": "https://raw.githubusercontent.com/EmmyLuaLs/emmylua-analyzer-rust/refs/heads/main/crates/emmylua_code_analysis/resources/schema.json"
}


================================================
FILE: .github/FUNDING.yml
================================================
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/displaying-a-sponsor-button-in-your-repository

github: chrisgrieser


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: File a bug report
title: "[Bug]: "
labels: ["bug"]
body:
  - type: textarea
    id: bug-description
    attributes:
      label: Bug Description
      description: A clear and concise description of the bug.
    validations:
      required: true
  - type: textarea
    id: screenshot
    attributes:
      label: Relevant Screenshot
      description: If applicable, add screenshots or a screen recording to help explain your problem.
  - type: textarea
    id: reproduction-steps
    attributes:
      label: To Reproduce
      description: Steps to reproduce the problem
      placeholder: |
        For example:
        1. Go to '...'
        2. Click on '...'
        3. Scroll down to '...'
  - type: textarea
    id: version-info
    attributes:
      label: neovim version
      render: Text
    validations:
      required: true
  - type: checkboxes
    id: checklist
    attributes:
      label: Make sure you have done the following
      options:
        - label: I have updated to the latest version of the plugin.
          required: true


================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature request
description: Suggest an idea
title: "Feature Request: "
labels: ["enhancement"]
body:
  - type: textarea
    id: feature-requested
    attributes:
      label: Feature Requested
      description: A clear and concise description of the feature.
    validations:
      required: true
  - type: textarea
    id: screenshot
    attributes:
      label: Relevant Screenshot
      description: If applicable, add screenshots or a screen recording to help explain the request.
  - type: checkboxes
    id: checklist
    attributes:
      label: Checklist
      options:
        - label: The feature would be useful to more users than just me.
          required: true


================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"
    commit-message:
      prefix: "chore(dependabot): "


================================================
FILE: .github/pull_request_template.md
================================================
## Problem statement
<!-- Briefly describe the issue this PR addresses. -->

## Proposed solution
<!-- Explain how this PR resolves the problem. -->

## AI usage disclosure
<!-- If you used AI beyond simple autocomplete, describe how.
AI-assisted code is not discouraged if it has been properly reviewed;
this disclosure is for transparency. -->

## Checklist
- [ ] Variable names follow `camelCase` convention.
- [ ] All AI-generated code has been reviewed by a human.
- [ ] The `README.md` has been updated for any new or modified functionality
  (the `.txt` file is auto-generated and does not need to be modified).


================================================
FILE: .github/workflows/nvim-type-check.yml
================================================
name: nvim type check

on:
  push:
    branches: [main]
    paths: ["**.lua"]
  pull_request: 
    paths: ["**.lua"]

jobs:
  build:
    name: nvim type check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: stevearc/nvim-typecheck-action@v2


================================================
FILE: .github/workflows/panvimdoc.yml
================================================
name: panvimdoc

on:
  push:
    branches: [main]
    paths:
      - README.md
      - .github/workflows/panvimdoc.yml
  workflow_dispatch: {} # allows manual execution

permissions:
  contents: write

#───────────────────────────────────────────────────────────────────────────────

jobs:
  docs:
    runs-on: ubuntu-latest
    name: README.md to vimdoc
    steps:
      - uses: actions/checkout@v6
      - run: git pull # fix failure when multiple commits are pushed in succession
      - run: mkdir -p doc

      - name: panvimdoc
        uses: kdheepak/panvimdoc@main
        with:
          vimdoc: ${{ github.event.repository.name }}
          version: "Neovim"
          demojify: true
          treesitter: true

      - run: git pull
      - name: push changes
        uses: stefanzweifel/git-auto-commit-action@v7
        with:
          commit_message: "chore: auto-generate vimdocs"
          branch: ${{ github.head_ref }}


================================================
FILE: .github/workflows/pr-title.yml
================================================
name: PR title

on:
  pull_request_target:
    types:
      - opened
      - edited
      - synchronize
      - reopened
      - ready_for_review

permissions:
  pull-requests: read

jobs:
  semantic-pull-request:
    name: Check PR title
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v6
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          requireScope: false
          subjectPattern: ^(?![A-Z]).+$ # disallow title starting with capital
          types: | # add `improv` to the list of allowed types
            improv
            fix
            feat
            refactor
            build
            ci
            style
            test
            chore
            perf
            docs
            break
            revert


================================================
FILE: .github/workflows/rumdl-lint.yml
================================================
name: Markdown linting via rumdl

on:
  push:
    branches: [main]
    paths:
      - "**/*.md"
      - ".github/workflows/rumdl-lint.yml"
      - ".rumdl.toml"
  pull_request:
    paths:
      - "**/*.md"

jobs:
  rumdl:
    name: rumdl
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: rvben/rumdl@v0
        with:
          report-type: annotations


================================================
FILE: .github/workflows/stale-bot.yml
================================================
name: Stale bot
on:
  schedule:
    - cron: "18 04 * * 3" 

permissions:
  issues: write
  pull-requests: write

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      - name: Close stale issues
        uses: actions/stale@v10
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

          # DOCS https://github.com/actions/stale#all-options
          days-before-stale: 180
          days-before-close: 7
          stale-issue-label: "Stale"
          stale-issue-message: |
            This issue has been automatically marked as stale.
            **If this issue is still affecting you, please leave any comment**, for example "bump", and it will be kept open.
          close-issue-message: |
            This issue has been closed due to inactivity, and will not be monitored.


================================================
FILE: .github/workflows/stylua.yml
================================================
name: Stylua check

on:
  push:
    branches: [main]
    paths: ["**.lua"]
  pull_request:
    paths: ["**.lua"]

jobs:
  stylua:
    name: Stylua
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: JohnnyMorganz/stylua-action@v5
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          version: latest
          args: --check .


================================================
FILE: .gitignore
================================================
# help-tags auto-generated by lazy.nvim
doc/tags


================================================
FILE: .harper-dictionary.txt
================================================
genghis
vimscript


================================================
FILE: .ignore
================================================
# auto-generated by panvimdoc
doc


================================================
FILE: .luarc.jsonc
================================================
{
	"runtime.version": "LuaJIT",

	"workspace.library": ["$VIMRUNTIME/lua"], // nvim-lua runtime

	"diagnostics": {
		"unusedLocalExclude": ["_*"], // allow `_varname` for unused variables
		"groupFileStatus": {
			"luadoc": "Any", // require stricter annotations
			"conventions": "Any" // disallow global variables
		}
	},

	"$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json"
}


================================================
FILE: .rumdl.toml
================================================
# DOCS https://github.com/rvben/rumdl/blob/main/docs/global-settings.md

[global]
line-length = 80
disable = ["blanks-around-lists"]  # rule of proximity

# ------------------------------------------------------------------------------

[ul-style]
style = "dash"  # GitHub default & quicker to type

[ul-indent]
indent = 4  # consistent with .editorconfig

[line-length]
code-blocks = false
reflow = true  # enable auto-formatting

[blanks-around-headings]
lines-below = 0  # rule of proximity

[ol-prefix]
style = "ordered"

[no-inline-html]
allowed-elements = ["a", "img", "kbd"]  # badges

[emphasis-style]
style = "asterisk"  # better than underscore, since not considered a word-char

[strong-style]
style = "asterisk"

[table-format]
enabled = true  # opt-in rule

[heading-capitalization]
enabled = true  # opt-in rule
style = "sentence_case"
ignore-words = ["nvim", "Obsidian", "Alfred"]

[toc-validation]
enabled = true  # opt-in rule

# ------------------------------------------------------------------------------

[per-file-ignores]
".github/pull_request_template.md" = ["first-line-h1"]


================================================
FILE: .stylua.toml
================================================
# https://github.com/JohnnyMorganz/StyLua#options
#───────────────────────────────────────────────────────────────────────────────
syntax = "LuaJIT"  # needed to support `::labels::`
column_width = 100
line_endings = "Unix"
indent_type = "Tabs"
indent_width = 3
quote_style = "AutoPreferDouble"
call_parentheses = "NoSingleTable"
collapse_simple_statement = "Always"
sort_requires.enabled = true


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2022-2023 Christopher Grieser

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Nvim-genghis ⚔️ <!-- rumdl-disable-line MD063 -->
<a href="https://dotfyle.com/plugins/chrisgrieser/nvim-genghis">
<img alt="badge" src="https://dotfyle.com/plugins/chrisgrieser/nvim-genghis/shield"/></a>

Lightweight and quick file operations without being a full-blown file manager.
For when you prefer a fuzzy finder over a file tree, but still want some
convenient file operations inside nvim.

<img alt="Showcase for renaming files" width=50% src="https://github.com/user-attachments/assets/010f3786-e4b2-4c4e-8cbb-a7618de93eb7">

## Table of contents

<!-- toc -->
- [Features](#features)
- [Installation](#installation)
- [Configuration](#configuration)
    - [UI plugin](#ui-plugin)
- [Usage](#usage)
    - [File operations](#file-operations)
    - [Copy operations](#copy-operations)
    - [File navigation](#file-navigation)
- [Why the name "Genghis"?](#why-the-name-genghis)
- [About the author](#about-the-author)
<!-- tocstop -->

## Features
**Commands** <!-- rumdl-disable-line MD036 -->
- Perform **common file operations**: moving, renaming, creating, deleting, or
  duplicating files.
- **Copy** the path or name of the current file in various formats.
- **Navigate** to the next or previous file in the current folder.

**Quality-of-life** <!-- rumdl-disable-line MD036 -->
- All movement and renaming commands **update `import` statements** to the
  renamed file (if the LSP supports `workspace/willRenameFiles`).
- Automatically keep the extension when no extension is given.
- Use vim motions in the input field.

## Installation
**Requirements** <!-- rumdl-disable-line MD036 -->
- nvim 0.10+
- *For the trash command*: an OS-specific trash CLI like `trash` or `gio trash`.
  (Since macOS 14+, there is a `trash` CLI already built-in, so there is no need
  to install anything.)
- **Recommended:** A provider for `vim.ui.input` and `vim.ui.select` such as
  [snacks.nvim](http://github.com/folke/snacks.nvim). This enables vim motions
  in the input field and looks nicer.

```lua
-- lazy.nvim
{ "chrisgrieser/nvim-genghis" }

-- packer
use { "chrisgrieser/nvim-genghis" }
```

## Configuration
The `.setup()` call is optional.

```lua
-- default config
require("genghis").setup {
	fileOperations = {
		-- automatically keep the extension when no file extension is given
		-- (everything after the first non-leading dot is treated as the extension)
		autoAddExt = true,

		trashCmd = function() ---@type fun(): string|string[]
			if jit.os == "OSX" then return "trash" end -- builtin since macOS 14
			if jit.os == "Windows" then return "trash" end
			if jit.os == "Linux" then return { "gio", "trash" } end
			return "trash-cli"
		end,

		ignoreInFolderSelection = { -- using lua pattern matching (e.g., escape `-` as `%-`)
			"/node_modules/", -- nodejs
			"/typings/", -- python
			"/doc/", -- vim help files folders
			"%.app/", -- macOS pseudo-folders
			"/%.", -- hidden folders
		},
	},

	navigation = {
		onlySameExtAsCurrentFile = false,
		ignoreDotfiles = true,
		ignoreExt = { "png", "svg", "webp", "jpg", "jpeg", "gif", "pdf", "zip" },
		ignoreFilesWithName = { ".DS_Store" },
	},

	successNotifications = true,

	icons = { -- set an icon to empty string to disable it
		chmodx = "󰒃",
		copyFile = "󱉥",
		copyPath = "󰅍",
		duplicate = "",
		file = "󰈔",
		move = "󰪹",
		new = "󰝒",
		nextFile = "󰖽",
		prevFile = "󰖿",
		rename = "󰑕",
		trash = "󰩹",
	},
}
```

### UI plugin
A UI plugin for `vim.ui.input` and `vim.ui.select`, such as
[snacks.nvim](http://github.com/folke/snacks.nvim), is recommended since it
enables for vim motions in the input field. (It also looks much nicer.)

```lua
-- minimal snacks.nvim config to use it for `vim.ui.input` and `vim.ui.select`
require("snacks").setup({
	input = { enabled = true },
	picker = { enabled = true },
}),
```

## Usage
You can access a command as Lua function:

```lua
require("genghis").createNewFile()
```

Or you can use the ex command `:Genghis` with the respective subcommand:

```vim
:Genghis createNewFile
```

### File operations
- `createNewFile`: Create a new file in the same directory as the current file.
- `createNewFileInFolder`: Create a new file in a folder in the current working
  directory.
- `duplicateFile`: Duplicate the current file.
- `moveSelectionToNewFile`: Create a new file and move the current selection
  to it. (Visual Line command, the selection is moved linewise.)
- `renameFile`: Rename the current file.
- `moveToFolderInCwd`: Move the current file to an existing folder in the
  current working directory.
- `moveAndRenameFile`: Move and rename the current file. Keeps the
  old name if the new path ends with `/`. Works like the UNIX `mv` command.
- `chmodx`: Makes current file executable. Equivalent to `chmod +x`.
- `trashFile`: Move the current file to the trash. (Defaults to `gio trash` on
  *Linux*, and `trash` on *macOS* or *Windows*.)
- `showInSystemExplorer`: Reveals the current file in the system explorer, such
  as macOS Finder. (Currently only on macOS, PRs welcome.)

The following applies to all commands above:
1. If no extension has been provided, uses the extension of the original file.
   (Everything after the first non-leading dot is treated as the extension; this
   behavior can be disabled with the config `fileOperations.autoAddExt =
   false`.)
2. If the new filename includes a `/`, the new file is placed in the respective
   subdirectory, creating any non-existing intermediate folders.
3. All movement and renaming commands update `import` statements, if the LSP
   supports `workspace/willRenameFiles`.

### Copy operations
- `copyFilename`: Copy the filename.
- `copyFilepath`: Copy the absolute filepath.
- `copyFilepathWithTilde`: Copy the absolute filepath, replacing the home
  directory with `~`.
- `copyRelativePath`: Copy the relative filepath.
- `copyDirectoryPath`: Copy the absolute directory path.
- `copyRelativeDirectoryPath`: Copy the relative directory path.
- `copyFileItself`: Copies the file itself. This means you can paste it into
  the browser or file manager. (Currently only on macOS, PRs welcome.)

All commands use the system clipboard.

### File navigation
`require("genghis").navigateToFileInFolder("next"|"prev")`: Move to the
next/previous file in the current folder of the current file, in alphabetical
order.

If `snacks.nvim` is installed, displays a cycling notification.

## Why the name "Genghis"?
A nod to [vim.eunuch](https://github.com/tpope/vim-eunuch), an older vimscript
plugin with a similar goal. As opposed to childless eunuchs, it is said that
Genghis Khan [has fathered thousands of
children](https://allthatsinteresting.com/genghis-khan-children).

## About the author
In my day job, I am a sociologist studying the social mechanisms underlying the
digital economy. For my PhD project, I investigate the governance of the app
economy and how software ecosystems manage the tension between innovation and
compatibility. If you are interested in this subject, feel free to get in touch.

- [Website](https://chris-grieser.de/)
- [Mastodon](https://pkm.social/@pseudometa)
- [ResearchGate](https://www.researchgate.net/profile/Christopher-Grieser)
- [LinkedIn](https://www.linkedin.com/in/christopher-grieser-ba693b17a/)

If you find this project helpful, you can support me via [🩷 GitHub
Sponsors](https://github.com/sponsors/chrisgrieser?frequency=one-time).


================================================
FILE: doc/nvim-genghis.txt
================================================
*nvim-genghis.txt*           For Neovim          Last change: 2026 February 06

==============================================================================
Table of Contents                             *nvim-genghis-table-of-contents*

1. Nvim-genghis                                   |nvim-genghis-nvim-genghis-|
  - Table of contents           |nvim-genghis-nvim-genghis--table-of-contents|
  - Features                             |nvim-genghis-nvim-genghis--features|
  - Installation                     |nvim-genghis-nvim-genghis--installation|
  - Configuration                   |nvim-genghis-nvim-genghis--configuration|
  - Usage                                   |nvim-genghis-nvim-genghis--usage|
  - Why the name “Genghis”?|nvim-genghis-nvim-genghis--why-the-name-“genghis”?|
  - About the author             |nvim-genghis-nvim-genghis--about-the-author|

==============================================================================
1. Nvim-genghis                                   *nvim-genghis-nvim-genghis-*



Lightweight and quick file operations without being a full-blown file manager.
For when you prefer a fuzzy finder over a file tree, but still want some
convenient file operations inside nvim.




TABLE OF CONTENTS               *nvim-genghis-nvim-genghis--table-of-contents*

- |nvim-genghis-features|
- |nvim-genghis-installation|
- |nvim-genghis-configuration|
    - |nvim-genghis-ui-plugin|
- |nvim-genghis-usage|
    - |nvim-genghis-file-operations|
    - |nvim-genghis-copy-operations|
    - |nvim-genghis-file-navigation|
- |nvim-genghis-why-the-name-"genghis"?|
- |nvim-genghis-about-the-author|


FEATURES                                 *nvim-genghis-nvim-genghis--features*

**Commands** - Perform **common file operations**: moving, renaming, creating,
deleting, or duplicating files. - **Copy** the path or name of the current file
in various formats. - **Navigate** to the next or previous file in the current
folder.

**Quality-of-life** - All movement and renaming commands **update import
statements** to the renamed file (if the LSP supports
`workspace/willRenameFiles`). - Automatically keep the extension when no
extension is given. - Use vim motions in the input field.


INSTALLATION                         *nvim-genghis-nvim-genghis--installation*

**Requirements** - nvim 0.10+ - _For the trash command_: an OS-specific trash
CLI like `trash` or `gio trash`. (Since macOS 14+, there is a `trash` CLI
already built-in, so there is no need to install anything.) - **Recommended:**
A provider for `vim.ui.input` and `vim.ui.select` such as snacks.nvim
<http://github.com/folke/snacks.nvim>. This enables vim motions in the input
field and looks nicer.

>lua
    -- lazy.nvim
    { "chrisgrieser/nvim-genghis" }
    
    -- packer
    use { "chrisgrieser/nvim-genghis" }
<


CONFIGURATION                       *nvim-genghis-nvim-genghis--configuration*

The `.setup()` call is optional.

>lua
    -- default config
    require("genghis").setup {
        fileOperations = {
            -- automatically keep the extension when no file extension is given
            -- (everything after the first non-leading dot is treated as the extension)
            autoAddExt = true,
    
            trashCmd = function() ---@type fun(): string|string[]
                if jit.os == "OSX" then return "trash" end -- builtin since macOS 14
                if jit.os == "Windows" then return "trash" end
                if jit.os == "Linux" then return { "gio", "trash" } end
                return "trash-cli"
            end,
    
            ignoreInFolderSelection = { -- using lua pattern matching (e.g., escape `-` as `%-`)
                "/node_modules/", -- nodejs
                "/typings/", -- python
                "/doc/", -- vim help files folders
                "%.app/", -- macOS pseudo-folders
                "/%.", -- hidden folders
            },
        },
    
        navigation = {
            onlySameExtAsCurrentFile = false,
            ignoreDotfiles = true,
            ignoreExt = { "png", "svg", "webp", "jpg", "jpeg", "gif", "pdf", "zip" },
            ignoreFilesWithName = { ".DS_Store" },
        },
    
        successNotifications = true,
    
        icons = { -- set an icon to empty string to disable it
            chmodx = "󰒃",
            copyFile = "󱉥",
            copyPath = "󰅍",
            duplicate = "",
            file = "󰈔",
            move = "󰪹",
            new = "󰝒",
            nextFile = "󰖽",
            prevFile = "󰖿",
            rename = "󰑕",
            trash = "󰩹",
        },
    }
<


UI PLUGIN ~

A UI plugin for `vim.ui.input` and `vim.ui.select`, such as snacks.nvim
<http://github.com/folke/snacks.nvim>, is recommended since it enables for vim
motions in the input field. (It also looks much nicer.)

>lua
    -- minimal snacks.nvim config to use it for `vim.ui.input` and `vim.ui.select`
    require("snacks").setup({
        input = { enabled = true },
        picker = { enabled = true },
    }),
<


USAGE                                       *nvim-genghis-nvim-genghis--usage*

You can access a command as Lua function:

>lua
    require("genghis").createNewFile()
<

Or you can use the ex command `:Genghis` with the respective subcommand:

>vim
    :Genghis createNewFile
<


FILE OPERATIONS ~

- `createNewFile`: Create a new file in the same directory as the current file.
- `createNewFileInFolder`: Create a new file in a folder in the current working
    directory.
- `duplicateFile`: Duplicate the current file.
- `moveSelectionToNewFile`: Create a new file and move the current selection
    to it. (Visual Line command, the selection is moved linewise.)
- `renameFile`: Rename the current file.
- `moveToFolderInCwd`: Move the current file to an existing folder in the
    current working directory.
- `moveAndRenameFile`: Move and rename the current file. Keeps the
    old name if the new path ends with `/`. Works like the UNIX `mv` command.
- `chmodx`: Makes current file executable. Equivalent to `chmod +x`.
- `trashFile`: Move the current file to the trash. (Defaults to `gio trash` on
    _Linux_, and `trash` on _macOS_ or _Windows_.)
- `showInSystemExplorer`: Reveals the current file in the system explorer, such
    as macOS Finder. (Currently only on macOS, PRs welcome.)

The following applies to all commands above: 1. If no extension has been
provided, uses the extension of the original file. (Everything after the first
non-leading dot is treated as the extension; this behavior can be disabled with
the config `fileOperations.autoAddExt = false`.) 2. If the new filename
includes a `/`, the new file is placed in the respective subdirectory, creating
any non-existing intermediate folders. 3. All movement and renaming commands
update `import` statements, if the LSP supports `workspace/willRenameFiles`.


COPY OPERATIONS ~

- `copyFilename`: Copy the filename.
- `copyFilepath`: Copy the absolute filepath.
- `copyFilepathWithTilde`: Copy the absolute filepath, replacing the home
    directory with `~`.
- `copyRelativePath`: Copy the relative filepath.
- `copyDirectoryPath`: Copy the absolute directory path.
- `copyRelativeDirectoryPath`: Copy the relative directory path.
- `copyFileItself`: Copies the file itself. This means you can paste it into
    the browser or file manager. (Currently only on macOS, PRs welcome.)

All commands use the system clipboard.


FILE NAVIGATION ~

`require("genghis").navigateToFileInFolder("next"|"prev")`: Move to the
next/previous file in the current folder of the current file, in alphabetical
order.

If `snacks.nvim` is installed, displays a cycling notification.


WHY THE NAME “GENGHIS”?*nvim-genghis-nvim-genghis--why-the-name-“genghis”?*

A nod to vim.eunuch <https://github.com/tpope/vim-eunuch>, an older vimscript
plugin with a similar goal. As opposed to childless eunuchs, it is said that
Genghis Khan has fathered thousands of children
<https://allthatsinteresting.com/genghis-khan-children>.


ABOUT THE AUTHOR                 *nvim-genghis-nvim-genghis--about-the-author*

In my day job, I am a sociologist studying the social mechanisms underlying the
digital economy. For my PhD project, I investigate the governance of the app
economy and how software ecosystems manage the tension between innovation and
compatibility. If you are interested in this subject, feel free to get in
touch.

- Website <https://chris-grieser.de/>
- Mastodon <https://pkm.social/@pseudometa>
- ResearchGate <https://www.researchgate.net/profile/Christopher-Grieser>
- LinkedIn <https://www.linkedin.com/in/christopher-grieser-ba693b17a/>

If you find this project helpful, you can support me via GitHub Sponsors
<https://github.com/sponsors/chrisgrieser?frequency=one-time>.

Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>

vim:tw=78:ts=8:noet:ft=help:norl:


================================================
FILE: lua/genghis/config.lua
================================================
local M = {}
--------------------------------------------------------------------------------

---@class Genghis.config
local defaultConfig = {
	fileOperations = {
		-- automatically keep the extension when no file extension is given
		-- (everything after the first non-leading dot is treated as the extension)
		autoAddExt = true,

		trashCmd = function() ---@type fun(): string|string[]
			if jit.os == "OSX" then return "trash" end -- builtin since macOS 14
			if jit.os == "Windows" then return "trash" end
			if jit.os == "Linux" then return { "gio", "trash" } end
			return "trash-cli"
		end,

		ignoreInFolderSelection = { -- using lua pattern matching (e.g., escape `-` as `%-`)
			"/node_modules/", -- nodejs
			"/typings/", -- python
			"/doc/", -- vim help files folders
			"%.app/", -- macOS pseudo-folders
			"/%.", -- hidden folders
		},
	},

	navigation = {
		onlySameExtAsCurrentFile = false,
		ignoreDotfiles = true,
		ignoreExt = { "png", "svg", "webp", "jpg", "jpeg", "gif", "pdf", "zip" },
		ignoreFilesWithName = { ".DS_Store" },
	},

	successNotifications = true,

	icons = { -- set an icon to empty string to disable it
		chmodx = "󰒃",
		copyFile = "󱉥",
		copyPath = "󰅍",
		duplicate = "",
		file = "󰈔",
		move = "󰪹",
		new = "󰝒",
		nextFile = "󰖽",
		prevFile = "󰖿",
		rename = "󰑕",
		trash = "󰩹",
	},
}
M.config = defaultConfig

---@param userConfig? Genghis.config
function M.setup(userConfig)
	M.config = vim.tbl_deep_extend("force", defaultConfig, userConfig or {})

	-- DEPRECATION (2025-11-24)
	---@diagnostic disable: undefined-field
	if M.config.trashCmd then
		M.config.fileOperations.trashCmd = M.config.trashCmd
		local notify = require("genghis.support.notify")
		notify("config `.trashCmd` is deprecated, use `.fileOperations.trashCmd` instead.", "warn")
	end
	---@diagnostic enable: undefined-field
end

--------------------------------------------------------------------------------
return M


================================================
FILE: lua/genghis/init.lua
================================================
local version = vim.version()
if version.major == 0 and version.minor < 10 then
	vim.notify("nvim-genghis requires at least nvim 0.10.", vim.log.levels.WARN)
	return
end
--------------------------------------------------------------------------------

local M = {}

---@param userConfig? Genghis.config
function M.setup(userConfig) require("genghis.config").setup(userConfig) end

---@param direction? "next"|"prev"
function M.navigateToFileInFolder(direction)
	require("genghis.operations.navigation").fileInFolder(direction)
end

-- redirect calls to this module to the respective submodules
setmetatable(M, {
	__index = function(_, key)
		return function(...)
			local fileOps = vim.tbl_keys(require("genghis.operations.file"))
			local copyOps = vim.tbl_keys(require("genghis.operations.copy"))

			local module
			if vim.tbl_contains(fileOps, key) then module = "file" end
			if vim.tbl_contains(copyOps, key) then module = "copy" end

			if module then
				require("genghis.operations." .. module)[key](...)
			else
				local notify = require("genghis.support.notify")
				local msg = ("There is no operation called `%s`.\n\n"):format(key)
					.. "Make sure it exists in the list of operations, and that you haven't misspelled it."
				notify(msg, "error", { ft = "markdown" })
			end
		end
	end,
})

--------------------------------------------------------------------------------
return M


================================================
FILE: lua/genghis/operations/copy.lua
================================================
local M = {}
--------------------------------------------------------------------------------

---@param expandOperation string
local function copyOp(expandOperation)
	local icon = require("genghis.config").config.icons.copyPath

	local register = "+"
	local toCopy = vim.fn.expand(expandOperation)
	vim.fn.setreg(register, toCopy)

	local notify = require("genghis.support.notify")
	notify(toCopy, "info", { title = "Copied", icon = icon })
end

-- DOCS for the modifiers
-- https://neovim.io/doc/user/builtin.html#expand()
-- https://neovim.io/doc/user/cmdline.html#filename-modifiers
function M.copyFilepath() copyOp("%:p") end
function M.copyFilepathWithTilde() copyOp("%:~") end
function M.copyFilename() copyOp("%:t") end
function M.copyRelativePath() copyOp("%:.") end
function M.copyDirectoryPath() copyOp("%:p:h") end
function M.copyRelativeDirectoryPath() copyOp("%:.:h") end

function M.copyFileItself()
	local notify = require("genghis.support.notify")
	if jit.os ~= "OSX" then
		notify("Currently only available on macOS.", "warn")
		return
	end

	local icon = require("genghis.config").config.icons.copyFile
	local path = vim.api.nvim_buf_get_name(0)
	local applescript = 'tell application "Finder" to set the clipboard to '
		.. ("POSIX file %q"):format(path)

	vim.system({ "osascript", "-e", applescript }, {}, function(out)
		if out.code ~= 0 then
			notify("Failed to copy file: " .. out.stderr, "error", { title = "Copy file" })
		else
			notify(vim.fs.basename(path), "info", { title = "Copied file", icon = icon })
		end
	end)
end

--------------------------------------------------------------------------------
return M


================================================
FILE: lua/genghis/operations/file.lua
================================================
local M = {}
--------------------------------------------------------------------------------

---@param op "rename"|"duplicate"|"new"|"new-from-selection"|"move-rename"
---@param targetDir? string
local function fileOp(op, targetDir)
	local moveConsideringPartition = require("genghis.support.move-considering-partition")
	local notify = require("genghis.support.notify")
	local lspRename = require("genghis.support.lsp-rename")

	-- PARAMETERS
	local origBufNr = vim.api.nvim_get_current_buf()
	local oldFilePath = vim.api.nvim_buf_get_name(0)
	local oldName = vim.fs.basename(oldFilePath)
	local pathSep = package.config:sub(1, 1)
	if not targetDir then targetDir = vim.fs.dirname(oldFilePath) end

	-- * non-greedy 1st capture, so 2nd capture matches double-extensions (see #60)
	-- * 1st capture requires at least one char, to not match empty string for dotfiles
	local oldNameNoExt, oldExt = oldName:match("(..-)(%.[%w.]*)")
	-- handle files without extension
	if not oldNameNoExt then oldNameNoExt = oldName end
	if not oldExt then oldExt = "" end

	local autoAddExt = require("genghis.config").config.fileOperations.autoAddExt
	local icons = require("genghis.config").config.icons
	local lspSupportsRenaming = lspRename.supported()

	-- PREPARE
	local prompt, prefill
	if op == "duplicate" then
		prompt = icons.duplicate .. " Duplicate file as: "
		prefill = (autoAddExt and oldNameNoExt or oldName) .. "-1"
	elseif op == "rename" then
		local text = lspSupportsRenaming and "Rename file & update imports:" or "Rename file to:"
		prompt = icons.rename .. " " .. text
		prefill = autoAddExt and oldNameNoExt or oldName
	elseif op == "move-rename" then
		local text = lspSupportsRenaming and " Move and rename file & update imports:"
			or " Move & rename file to:"
		prompt = icons.rename .. " " .. text
		prefill = targetDir .. pathSep
	elseif op == "new" or op == "new-from-selection" then
		prompt = icons.new .. " Name for new file: "
		prefill = ""
	end

	-- INPUT
	vim.ui.input({
		prompt = vim.trim(prompt),
		default = prefill,
	}, function(newName)
		vim.cmd.redraw() -- clear message area from vim.ui.input prompt
		if not newName then return end -- input has been canceled

		if op == "move-rename" and vim.endswith(newName, pathSep) then -- user just provided a folder
			newName = newName .. oldName
		elseif (op == "new" or op == "new-from-selection") and newName == "" then
			newName = "Untitled"
		end

		-- GUARD validate filename
		local invalidName = newName:find("^%s+$")
			or newName:find(":")
			or (vim.startswith(newName, pathSep) and op ~= "move-rename")
		local sameName = newName == oldName
		local emptyInput = newName == ""
		if invalidName or sameName or emptyInput then
			if invalidName or emptyInput then
				notify("Invalid filename.", "error")
			elseif sameName then
				notify("Cannot use the same filename.", "warn")
			end
			return
		end

		-- DETERMINE PATH AND EXTENSION
		if newName:find(pathSep) then
			local newFolder = vim.fs.dirname(newName)
			local absFolder = op == "move-rename" and newFolder
				or vim.fs.joinpath(targetDir, newFolder)
			vim.fn.mkdir(absFolder, "p")
		end

		local userProvidedNoExt = newName:find(".%.[^/]*$") == nil -- non-leading dot to not include dotfiles without extension
		if userProvidedNoExt and autoAddExt then newName = newName .. oldExt end

		local newFilePath = op == "move-rename" and newName or vim.fs.joinpath(targetDir, newName)
		if vim.uv.fs_stat(newFilePath) ~= nil then
			notify(("File with name %q already exists."):format(newFilePath), "error")
			return
		end

		-- EXECUTE FILE OPERATION
		vim.cmd("silent! update")
		if op == "duplicate" then
			local success = vim.uv.fs_copyfile(oldFilePath, newFilePath)
			if not success then return end
			vim.cmd.edit(newFilePath)
			vim.cmd("silent! write")
			local msg = ("Duplicated %q as %q."):format(oldName, newName)
			notify(msg, "info", { icon = icons.duplicate })
		elseif op == "rename" or op == "move-rename" then
			lspRename.willRename(oldFilePath, newFilePath)
			local success = moveConsideringPartition(oldFilePath, newFilePath)
			if not success then return end
			vim.cmd.edit(newFilePath)
			vim.api.nvim_buf_delete(origBufNr, { force = true })
			local msg = ("Renamed %q to %q."):format(oldName, newName)
			notify(msg, "info", { icon = icons.rename })
			vim.cmd(lspSupportsRenaming and "wall" or "silent! write")
		elseif op == "new" then
			vim.cmd.edit(newFilePath)
			vim.cmd.write(newFilePath)
		elseif op == "new-from-selection" then
			local prevReg = vim.fn.getreg("z")
			vim.cmd([['<,'>delete z]]) -- will have already left visual for input, so '<,'> are set

			vim.cmd.edit(newFilePath)
			vim.cmd("put z") -- `vim.cmd.put("z")` does not work
			vim.fn.setreg("z", prevReg)
			vim.cmd.write(newFilePath)
		end
	end)
end

function M.renameFile() fileOp("rename") end
function M.moveAndRenameFile() fileOp("move-rename") end
function M.duplicateFile() fileOp("duplicate") end
function M.createNewFile() fileOp("new") end
function M.moveSelectionToNewFile() fileOp("new-from-selection") end

--------------------------------------------------------------------------------

---@param op "move-file"|"new-in-folder"
local function folderSelection(op)
	local moveConsideringPartition = require("genghis.support.move-considering-partition")
	local notify = require("genghis.support.notify")
	local lspRenaming = require("genghis.support.lsp-rename")
	local ignoreFolders = require("genghis.config").config.fileOperations.ignoreInFolderSelection
	local icons = require("genghis.config").config.icons

	-- PARAMETERS
	local oldAbsPath = vim.api.nvim_buf_get_name(0)
	local oldAbsParent = vim.fs.dirname(oldAbsPath)
	local filename = vim.fs.basename(oldAbsPath)
	local lspSupportsRenaming = lspRenaming.supported()
	local cwd = assert(vim.uv.cwd(), "Could not get current working directory.")
	local origBufNr = vim.api.nvim_get_current_buf()

	-- GET OTHER FOLDERS IN CWD
	local foldersInCwd = vim.fs.find(function(name, path)
		local absPath = vim.fs.joinpath(path, name)
		local relPath = absPath:sub(#cwd + 1) .. "/" -- not pathSep, since `joinpath` uses `/`

		local sameFolder = absPath == oldAbsParent
		local ignoredDir = vim.iter(ignoreFolders)
			:any(function(dir) return relPath:find(dir) ~= nil end)

		return not (ignoredDir or sameFolder)
	end, { type = "directory", limit = math.huge })

	-- ORDER OF FOLDERS
	table.sort(foldersInCwd, function(a, b)
		local aMtime = vim.uv.fs_stat(a).mtime.sec
		local bMtime = vim.uv.fs_stat(b).mtime.sec
		return aMtime > bMtime
	end)
	-- insert cwd at bottom, since moving to it unlikely
	if cwd ~= oldAbsParent then table.insert(foldersInCwd, cwd) end
	-- insert current dir at top, since moving to it likely
	if op == "new-in-folder" then table.insert(foldersInCwd, 1, oldAbsParent) end

	-- PROMPT & MOVE
	local prompt
	if op == "move-file" then
		prompt = icons.move .. " Move file to"
		if lspSupportsRenaming then prompt = prompt .. " (with updated imports)" end
		prompt = prompt .. ":"
	elseif op == "new-in-folder" then
		prompt = icons.new .. " Folder for new file:"
	end
	vim.ui.select(foldersInCwd, {
		prompt = prompt,
		kind = "genghis.select-folder",
		format_item = function(path)
			local relPath = path:sub(#cwd + 1)
			return (relPath == "" and "/" or relPath)
		end,
	}, function(newAbsParent)
		if not newAbsParent then return end
		local newRelParent = newAbsParent:sub(#cwd + 1)
		newRelParent = newRelParent == "" and "/" or newRelParent

		if op == "new-in-folder" then
			fileOp("new", newAbsParent)
		elseif op == "move-file" then
			local newAbsPath = vim.fs.joinpath(newAbsParent, filename)
			if vim.uv.fs_stat(newAbsPath) ~= nil then
				notify(("File %q already exists at %q."):format(filename, newRelParent), "error")
				return
			end

			vim.cmd("silent! update")
			lspRenaming.willRename(oldAbsPath, newAbsPath)
			local success = moveConsideringPartition(oldAbsPath, newAbsPath)
			if not success then return end

			vim.cmd.edit(newAbsPath)
			vim.api.nvim_buf_delete(origBufNr, { force = true })
			local msg = ("Moved %q to %q"):format(filename, newRelParent)
			local append = lspSupportsRenaming and " and updated imports." or "."
			notify(msg .. append, "info", { icon = icons.move })
			vim.cmd(lspSupportsRenaming and "wall" or "silent! write")
		end
	end)
end

function M.moveToFolderInCwd() folderSelection("move-file") end
function M.createNewFileInFolder() folderSelection("new-in-folder") end

--------------------------------------------------------------------------------

function M.chmodx()
	local icons = require("genghis.config").config.icons

	local filepath = vim.api.nvim_buf_get_name(0)
	local perm = vim.fn.getfperm(filepath)
	perm = perm:gsub("r(.)%-", "r%1x") -- add x to every group that has r
	vim.fn.setfperm(filepath, perm)

	local notify = require("genghis.support.notify")
	notify("Permission +x granted.", "info", { icon = icons.chmodx })
	vim.cmd.edit() -- reload the file
end

function M.trashFile()
	vim.cmd("silent! update")
	local filepath = vim.api.nvim_buf_get_name(0)
	local filename = vim.fs.basename(filepath)
	local icon = require("genghis.config").config.icons.trash
	local trashCmd = require("genghis.config").config.fileOperations.trashCmd

	-- execute the trash command
	local cmd = trashCmd()
	if type(cmd) ~= "table" then cmd = { cmd } end
	table.insert(cmd, filepath)
	local out = vim.system(cmd):wait()

	-- handle the result
	local notify = require("genghis.support.notify")
	if out.code == 0 then
		vim.api.nvim_buf_delete(0, { force = true })
		notify(("%q moved to trash."):format(filename), "info", { icon = icon })
	else
		local outmsg = (out.stdout or "") .. (out.stderr or "")
		notify(("Trashing %q failed: %s"):format(filename, outmsg), "error")
	end
end

function M.showInSystemExplorer()
	local notify = require("genghis.support.notify")
	if jit.os ~= "OSX" then
		notify("Currently only available on macOS.", "warn")
		return
	end

	local out = vim.system({ "open", "-R", vim.api.nvim_buf_get_name(0) }):wait()
	if out.code ~= 0 then
		local icon = require("genghis.config").config.icons.file
		notify("Failed: " .. out.stderr, "error", { icon = icon })
	end
end
--------------------------------------------------------------------------------
return M


================================================
FILE: lua/genghis/operations/navigation.lua
================================================
local M = {}
--------------------------------------------------------------------------------

---Cycles files in folder in alphabetical order.
---If snacks.nvim is installed, adds cycling notification.
---@param direction? "next"|"prev"
function M.fileInFolder(direction)
	local notify = require("genghis.support.notify")

	if not direction then direction = "next" end
	if direction ~= "next" and direction ~= "prev" then
		notify('Invalid direction. Only "next" and "prev" are allowed.', "warn")
		return
	end

	local config = require("genghis.config").config
	local curPath = vim.api.nvim_buf_get_name(0)
	local curFile = vim.fs.basename(curPath)
	local curFolder = vim.fs.dirname(curPath)
	local icon = direction == "next" and config.icons.nextFile or config.icons.prevFile

	-- get list of files
	local itemsInFolder = vim.fs.dir(curFolder) -- INFO `fs.dir` already returns them sorted
	local filesInFolder = vim.iter(itemsInFolder):fold({}, function(acc, name, type)
		local ext = name:match("%.(%w+)$")
		local curExt = curFile:match("%.(%w+)$")

		local ignored = (config.navigation.onlySameExtAsCurrentFile and ext ~= curExt)
			or vim.tbl_contains(config.navigation.ignoreExt, ext)
			or (config.navigation.ignoreDotfiles and vim.startswith(name, "."))
			or vim.tbl_contains(config.navigation.ignoreFilesWithName, name)

		if type == "file" and not ignored then
			table.insert(acc, name) -- select only name
		end
		return acc
	end)

	-- GUARD no files to navigate to
	if #filesInFolder == 0 then -- if currently at a hidden file and there are only hidden files in the dir
		notify("No valid files found in folder.", "warn", { icon = icon })
		return
	elseif #filesInFolder == 1 then
		notify("Already at the only valid file.", "warn", { icon = icon })
		return
	end

	-- determine next index
	local curIdx
	for idx = 1, #filesInFolder do
		if filesInFolder[idx] == curFile then
			curIdx = idx
			break
		end
	end
	if not curIdx then
		local msg = "Cannot determine next file, current file itself is excluded."
		notify(msg, "warn", { icon = icon })
		return
	end
	local nextIdx = curIdx + (direction == "next" and 1 or -1)
	if nextIdx < 1 then nextIdx = #filesInFolder end
	if nextIdx > #filesInFolder then nextIdx = 1 end

	-- goto file
	local nextFile = curFolder .. "/" .. filesInFolder[nextIdx]
	vim.cmd.edit(nextFile)

	-- notification
	if package.loaded["snacks"] then
		local msg = vim
			.iter(filesInFolder)
			:map(function(file)
				-- mark current, using markdown h1
				local prefix = file == filesInFolder[nextIdx] and "#" or "-"
				return prefix .. " " .. file
			end)
			:slice(nextIdx - 5, nextIdx + 5) -- display ~5 files before/after
			:join("\n")
		local title = direction:sub(1, 1):upper()
			.. direction:sub(2)
			.. " file"
			.. (" (%d/%d)"):format(nextIdx, #filesInFolder)
		vim.notify(msg, nil, {
			title = title,
			icon = icon,
			history = false,
			id = "next-in-folder", -- replace notifications when quickly cycling
			ft = "markdown", -- so `h1` is highlighted
		})
	end
end

--------------------------------------------------------------------------------
return M


================================================
FILE: lua/genghis/support/lsp-rename.lua
================================================
local M = {}

--------------------------------------------------------------------------------

---Requests a 'workspace/willRenameFiles' on any running LSP client, that supports it
---SOURCE https://github.com/LazyVim/LazyVim/blob/ac092289f506052cfdd1879f462be05075fe3081/lua/lazyvim/util/lsp.lua#L99-L119
---@param fromName string
---@param toName string
function M.willRename(fromName, toName)
	local clients = vim.lsp.get_clients { bufnr = 0 }
	for _, client in ipairs(clients) do
		if client:supports_method("workspace/willRenameFiles") then
			local response = client:request_sync("workspace/willRenameFiles", {
				files = {
					{ oldUri = vim.uri_from_fname(fromName), newUri = vim.uri_from_fname(toName) },
				},
			}, 1000, 0)
			if response and response.result ~= nil then
				vim.lsp.util.apply_workspace_edit(response.result, client.offset_encoding)
			end
		end
	end
end

---@nodiscard
---@return boolean
function M.supported()
	local clients = vim.lsp.get_clients { bufnr = 0 }
	for _, client in ipairs(clients) do
		if client:supports_method("workspace/willRenameFiles") then return true end
	end
	return false
end

--------------------------------------------------------------------------------
return M


================================================
FILE: lua/genghis/support/move-considering-partition.lua
================================================
---@param oldFilePath string
---@param newFilePath string
---@return boolean success
return function(oldFilePath, newFilePath)
	local renamed, _ = vim.uv.fs_rename(oldFilePath, newFilePath)
	if renamed then return true end

	local notify = require("genghis.support.notify")

	-- try `fs_copyfile` to support moving across partitions
	local copied, copiedError = vim.uv.fs_copyfile(oldFilePath, newFilePath)
	if copied then
		local deleted, deletedError = vim.uv.fs_unlink(oldFilePath)
		if deleted then
			return true
		else
			notify(("Failed to delete %q: %q"):format(oldFilePath, deletedError), "error")
			return false
		end
	else
		local msg = ("Failed to copy %q to %q: %q"):format(oldFilePath, newFilePath, copiedError)
		notify(msg, "error")
		return false
	end
end


================================================
FILE: lua/genghis/support/notify.lua
================================================
---@param msg string
---@param level? "info"|"warn"|"error"
---@param opts? table
return function(msg, level, opts)
	local successNotify = require("genghis.config").config.successNotifications
	if not level then level = "info" end
	if level == "info" and not successNotify then return end
	if not opts then opts = {} end

	opts.title = opts.title and "Genghis: " .. opts.title or "Genghis"
	if not opts.ft then opts.ft = "text" end -- prevent `~` from creating strikethroughs in `snacks.notifier`
	vim.notify(msg, vim.log.levels[level:upper()], opts)
end


================================================
FILE: plugin/ex-commands.lua
================================================
vim.api.nvim_create_user_command("Genghis", function(ctx) require("genghis")[ctx.args]() end, {
	nargs = 1,
	complete = function(query)
		local allOps = {}
		vim.list_extend(allOps, vim.tbl_keys(require("genghis.operations.file")))
		vim.list_extend(allOps, vim.tbl_keys(require("genghis.operations.copy")))
		vim.list_extend(allOps, vim.tbl_keys(require("genghis.operations.navigation")))
		return vim.tbl_filter(function(op) return op:lower():find(query, nil, true) end, allOps)
	end,
})
Download .txt
gitextract_lchxmgi1/

├── .editorconfig
├── .emmyrc.json
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   ├── config.yml
│   │   └── feature_request.yml
│   ├── dependabot.yml
│   ├── pull_request_template.md
│   └── workflows/
│       ├── nvim-type-check.yml
│       ├── panvimdoc.yml
│       ├── pr-title.yml
│       ├── rumdl-lint.yml
│       ├── stale-bot.yml
│       └── stylua.yml
├── .gitignore
├── .harper-dictionary.txt
├── .ignore
├── .luarc.jsonc
├── .rumdl.toml
├── .stylua.toml
├── LICENSE
├── README.md
├── doc/
│   └── nvim-genghis.txt
├── lua/
│   └── genghis/
│       ├── config.lua
│       ├── init.lua
│       ├── operations/
│       │   ├── copy.lua
│       │   ├── file.lua
│       │   └── navigation.lua
│       └── support/
│           ├── lsp-rename.lua
│           ├── move-considering-partition.lua
│           └── notify.lua
└── plugin/
    └── ex-commands.lua
Condensed preview — 32 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (53K chars).
[
  {
    "path": ".editorconfig",
    "chars": 393,
    "preview": "root = true\n\n[*]\nmax_line_length = 100\nend_of_line = lf\ncharset = utf-8\ninsert_final_newline = true\nindent_style = tab\ni"
  },
  {
    "path": ".emmyrc.json",
    "chars": 293,
    "preview": "{\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\"librar"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 187,
    "preview": "# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/di"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1082,
    "preview": "name: Bug Report\ndescription: File a bug report\ntitle: \"[Bug]: \"\nlabels: [\"bug\"]\nbody:\n  - type: textarea\n    id: bug-de"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 28,
    "preview": "blank_issues_enabled: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 684,
    "preview": "name: Feature request\ndescription: Suggest an idea\ntitle: \"Feature Request: \"\nlabels: [\"enhancement\"]\nbody:\n  - type: te"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 174,
    "preview": "version: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n  "
  },
  {
    "path": ".github/pull_request_template.md",
    "chars": 619,
    "preview": "## Problem statement\n<!-- Briefly describe the issue this PR addresses. -->\n\n## Proposed solution\n<!-- Explain how this "
  },
  {
    "path": ".github/workflows/nvim-type-check.yml",
    "chars": 279,
    "preview": "name: nvim type check\n\non:\n  push:\n    branches: [main]\n    paths: [\"**.lua\"]\n  pull_request: \n    paths: [\"**.lua\"]\n\njo"
  },
  {
    "path": ".github/workflows/panvimdoc.yml",
    "chars": 936,
    "preview": "name: panvimdoc\n\non:\n  push:\n    branches: [main]\n    paths:\n      - README.md\n      - .github/workflows/panvimdoc.yml\n "
  },
  {
    "path": ".github/workflows/pr-title.yml",
    "chars": 811,
    "preview": "name: PR title\n\non:\n  pull_request_target:\n    types:\n      - opened\n      - edited\n      - synchronize\n      - reopened"
  },
  {
    "path": ".github/workflows/rumdl-lint.yml",
    "chars": 388,
    "preview": "name: Markdown linting via rumdl\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \"**/*.md\"\n      - \".github/workflo"
  },
  {
    "path": ".github/workflows/stale-bot.yml",
    "chars": 799,
    "preview": "name: Stale bot\non:\n  schedule:\n    - cron: \"18 04 * * 3\" \n\npermissions:\n  issues: write\n  pull-requests: write\n\njobs:\n "
  },
  {
    "path": ".github/workflows/stylua.yml",
    "chars": 375,
    "preview": "name: Stylua check\n\non:\n  push:\n    branches: [main]\n    paths: [\"**.lua\"]\n  pull_request:\n    paths: [\"**.lua\"]\n\njobs:\n"
  },
  {
    "path": ".gitignore",
    "chars": 49,
    "preview": "# help-tags auto-generated by lazy.nvim\ndoc/tags\n"
  },
  {
    "path": ".harper-dictionary.txt",
    "chars": 18,
    "preview": "genghis\nvimscript\n"
  },
  {
    "path": ".ignore",
    "chars": 34,
    "preview": "# auto-generated by panvimdoc\ndoc\n"
  },
  {
    "path": ".luarc.jsonc",
    "chars": 419,
    "preview": "{\n\t\"runtime.version\": \"LuaJIT\",\n\n\t\"workspace.library\": [\"$VIMRUNTIME/lua\"], // nvim-lua runtime\n\n\t\"diagnostics\": {\n\t\t\"un"
  },
  {
    "path": ".rumdl.toml",
    "chars": 1101,
    "preview": "# DOCS https://github.com/rvben/rumdl/blob/main/docs/global-settings.md\n\n[global]\nline-length = 80\ndisable = [\"blanks-ar"
  },
  {
    "path": ".stylua.toml",
    "chars": 396,
    "preview": "# https://github.com/JohnnyMorganz/StyLua#options\n#─────────────────────────────────────────────────────────────────────"
  },
  {
    "path": "LICENSE",
    "chars": 1081,
    "preview": "MIT License\n\nCopyright (c) 2022-2023 Christopher Grieser\n\nPermission is hereby granted, free of charge, to any person ob"
  },
  {
    "path": "README.md",
    "chars": 7369,
    "preview": "# Nvim-genghis ⚔️ <!-- rumdl-disable-line MD063 -->\n<a href=\"https://dotfyle.com/plugins/chrisgrieser/nvim-genghis\">\n<im"
  },
  {
    "path": "doc/nvim-genghis.txt",
    "chars": 8868,
    "preview": "*nvim-genghis.txt*           For Neovim          Last change: 2026 February 06\n\n========================================"
  },
  {
    "path": "lua/genghis/config.lua",
    "chars": 1933,
    "preview": "local M = {}\n--------------------------------------------------------------------------------\n\n---@class Genghis.config\n"
  },
  {
    "path": "lua/genghis/init.lua",
    "chars": 1398,
    "preview": "local version = vim.version()\nif version.major == 0 and version.minor < 10 then\n\tvim.notify(\"nvim-genghis requires at le"
  },
  {
    "path": "lua/genghis/operations/copy.lua",
    "chars": 1644,
    "preview": "local M = {}\n--------------------------------------------------------------------------------\n\n---@param expandOperation"
  },
  {
    "path": "lua/genghis/operations/file.lua",
    "chars": 10288,
    "preview": "local M = {}\n--------------------------------------------------------------------------------\n\n---@param op \"rename\"|\"du"
  },
  {
    "path": "lua/genghis/operations/navigation.lua",
    "chars": 3117,
    "preview": "local M = {}\n--------------------------------------------------------------------------------\n\n---Cycles files in folder"
  },
  {
    "path": "lua/genghis/support/lsp-rename.lua",
    "chars": 1224,
    "preview": "local M = {}\n\n--------------------------------------------------------------------------------\n\n---Requests a 'workspace"
  },
  {
    "path": "lua/genghis/support/move-considering-partition.lua",
    "chars": 774,
    "preview": "---@param oldFilePath string\n---@param newFilePath string\n---@return boolean success\nreturn function(oldFilePath, newFil"
  },
  {
    "path": "lua/genghis/support/notify.lua",
    "chars": 555,
    "preview": "---@param msg string\n---@param level? \"info\"|\"warn\"|\"error\"\n---@param opts? table\nreturn function(msg, level, opts)\n\tloc"
  },
  {
    "path": "plugin/ex-commands.lua",
    "chars": 490,
    "preview": "vim.api.nvim_create_user_command(\"Genghis\", function(ctx) require(\"genghis\")[ctx.args]() end, {\n\tnargs = 1,\n\tcomplete = "
  }
]

About this extraction

This page contains the full source code of the chrisgrieser/nvim-genghis GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 32 files (46.7 KB), approximately 13.4k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!