├── .editorconfig ├── .github └── workflows │ ├── linter.yml │ └── tests.yml ├── .luacheckrc ├── README.md ├── lua └── overfly.lua ├── plugin └── overfly.lua └── tests └── minimal.vim /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.lua] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Lint Code Base 3 | on: 4 | pull_request: ~ 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Lint Code Base 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v3 17 | 18 | - name: Lint Code Base 19 | uses: github/super-linter/slim@v4 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | LINTER_RULES_PATH: / 23 | VALIDATE_JSCPD: false 24 | VALIDATE_PYTHON_BLACK: false 25 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Run tests 3 | on: 4 | pull_request: ~ 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | name: Run tests 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | neovim_version: ['nightly'] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - run: date +%F > todays-date 20 | - name: Restore cache for today's nightly. 21 | uses: actions/cache@v4 22 | with: 23 | path: _neovim 24 | key: ${{ runner.os }}-x64-${{ hashFiles('todays-date') }} 25 | 26 | - name: Prepare plenary 27 | run: | 28 | git clone --depth 1 https://github.com/nvim-lua/plenary.nvim ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim 29 | ln -s "$(pwd)" ~/.local/share/nvim/site/pack/vendor/start 30 | 31 | - name: Setup neovim 32 | uses: rhysd/action-setup-vim@v1 33 | with: 34 | neovim: true 35 | version: ${{ matrix.neovim_version }} 36 | 37 | - name: Run tests 38 | run: | 39 | nvim --headless --noplugin -u tests/minimal.vim -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.vim'}" 40 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | ignore = { 2 | "631", -- max_line_length 3 | } 4 | read_globals = { 5 | "vim", 6 | "describe", 7 | "it", 8 | "assert" 9 | } 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overfly 2 | 3 | Provides keymaps to quickly fly around your source code 4 | 5 | - `]q -> cnext` 6 | - `[q -> cprevious` 7 | - `]Q -> cfirst` 8 | - `[Q -> clast` 9 | - `]l -> lnext` 10 | - `[l -> previous` 11 | - `]L -> lfirst` 12 | - `[L -> llast` 13 | - `]w -> next diagnostic` (by worst severity, e.g. skips warnings if there are any errors) 14 | - `[w -> prev diagnostic` 15 | - `]v -> next document highlight` (highlighted by `vim.lsp.buf.document_highlight()`) 16 | - `[v -> prev document highlight` (highlighted by `vim.lsp.buf.document_highlight()`) 17 | - `]M -> next methods` prompts for selection; requires [nvim-qwahl][qwahl] 18 | - `[M -> prev methods` prompts for selection; requires [nvim-qwahl][qwahl] 19 | 20 | For each category there is also a `]` variant that will enter a 21 | "move-mode". In this mode you can continue navigating with just `]` or `[`. 22 | Turning something like `]q ]q ]q ]q ]q` into `]m` which jumps 25 | between methods without prompt. 26 | 27 | ## Installation 28 | 29 | ```bash 30 | git clone \ 31 | https://github.com/mfussenegger/nvim-overfly.git \ 32 | ~/.config/nvim/pack/plugins/start/nvim-overfly 33 | ``` 34 | 35 | No setup required. 36 | 37 | ## Configuration 38 | 39 | Fork it and edit the code in `plugin/overfly.lua` 40 | 41 | ## Contributions 42 | 43 | Thanks, but no 44 | 45 | ## Bugs 46 | 47 | Keep them 48 | 49 | ## Support 50 | 51 | You are on your own 52 | 53 | 54 | [qwahl]: https://github.com/mfussenegger/nvim-qwahl 55 | -------------------------------------------------------------------------------- /lua/overfly.lua: -------------------------------------------------------------------------------- 1 | local api = vim.api 2 | local M = {} 3 | 4 | 5 | local function move_to_highlight(is_closer) 6 | local lsp = vim.lsp 7 | local util = vim.lsp.util 8 | 9 | local win = api.nvim_get_current_win() 10 | local lnum, col = unpack(api.nvim_win_get_cursor(win)) 11 | lnum = lnum - 1 12 | local cursor = { 13 | start = { line = lnum, character = col } 14 | } 15 | ---@diagnostic disable-next-line: deprecated 16 | local get_clients = lsp.get_clients or lsp.get_active_clients 17 | local method = "textDocument/documentHighlight" 18 | local bufnr = api.nvim_get_current_buf() 19 | local clients = get_clients({ bufnr = 0, method = method }) 20 | if not next(clients) then 21 | return 22 | end 23 | local remaining = #clients 24 | local closest = nil 25 | ---@param result lsp.DocumentHighlight[]|nil 26 | local function on_result(_, result) 27 | for _, hl in ipairs(result or {}) do 28 | local range = hl.range 29 | local cursor_inside_range = ( 30 | range.start.line <= lnum 31 | and range.start.character < col 32 | and range["end"].line >= lnum 33 | and range["end"].character > col 34 | ) 35 | if not cursor_inside_range 36 | and is_closer(cursor, range) 37 | and (closest == nil or is_closer(range, closest)) then 38 | closest = range 39 | end 40 | end 41 | remaining = remaining - 1 42 | end 43 | for _, client in ipairs(clients) do 44 | local params = util.make_position_params(win, client.offset_encoding) 45 | if vim.fn.has("nvim-0.11") == 1 then 46 | client:request(method, params, on_result, bufnr) 47 | else 48 | ---@diagnostic disable-next-line: param-type-mismatch 49 | client.request(method, params, on_result, bufnr) 50 | end 51 | end 52 | vim.wait(1000, function() 53 | return remaining == 0 54 | end) 55 | if closest then 56 | api.nvim_win_set_cursor(win, { closest.start.line + 1, closest.start.character }) 57 | end 58 | end 59 | 60 | local function is_before(x, y) 61 | if x.start.line < y.start.line then 62 | return true 63 | elseif x.start.line == y.start.line then 64 | return x.start.character < y.start.character 65 | else 66 | return false 67 | end 68 | end 69 | 70 | function M.next_highlight() 71 | return move_to_highlight(is_before) 72 | end 73 | 74 | 75 | function M.prev_highlight() 76 | return move_to_highlight(function(x, y) return is_before(y, x) end) 77 | end 78 | 79 | 80 | local function diagnostic_severity() 81 | local num_warnings = 0 82 | for _, d in ipairs(vim.diagnostic.get(0)) do 83 | if d.severity == vim.diagnostic.severity.ERROR then 84 | return vim.diagnostic.severity.ERROR 85 | elseif d.severity == vim.diagnostic.severity.WARN then 86 | num_warnings = num_warnings + 1 87 | end 88 | end 89 | if num_warnings > 0 then 90 | return vim.diagnostic.severity.WARN 91 | else 92 | return nil 93 | end 94 | end 95 | 96 | 97 | function M.next_diagnostic() 98 | ---@diagnostic disable-next-line: deprecated 99 | local jump = vim.diagnostic.jump or vim.diagnostic.goto_next 100 | jump({ 101 | severity = diagnostic_severity(), 102 | float = { border = 'single' }, 103 | count = 1, 104 | }) 105 | end 106 | 107 | 108 | function M.prev_diagnostic() 109 | ---@diagnostic disable-next-line: deprecated 110 | local jump = vim.diagnostic.jump or vim.diagnostic.goto_prev 111 | jump({ 112 | severity = diagnostic_severity(), 113 | float = { border = 'single' }, 114 | count = -1, 115 | }) 116 | end 117 | 118 | 119 | ---@param opts? lsp_tags.opts 120 | function M.select_methods(opts) 121 | local default_opts = { 122 | kind = {"Constructor", "Method", "Function"} 123 | } 124 | opts = vim.tbl_extend("force", default_opts, opts or {}) 125 | require('qwahl').lsp_tags(opts) 126 | end 127 | 128 | 129 | ---@param opts? lsp_tags.opts 130 | function M.select_methods_sync(opts) 131 | local done = false 132 | opts = opts or {} 133 | opts.on_done = function() 134 | done = true 135 | end 136 | M.select_methods(opts) 137 | vim.wait(1000, function() return done == true end) 138 | end 139 | 140 | 141 | ---@param opts {next: function, prev:function} 142 | function M.move(opts) 143 | print("Move mode: Use ] or [ to move, any other char to abort: ") 144 | while true do 145 | vim.cmd.normal("zz") 146 | vim.cmd.redraw() 147 | local ok, keynum = pcall(vim.fn.getchar) 148 | if not ok then 149 | break 150 | end 151 | assert(type(keynum) == "number") 152 | local key = string.char(keynum) 153 | local fn 154 | if key == "]" then 155 | fn = opts.next 156 | elseif key == "[" then 157 | fn = opts.prev 158 | else 159 | break 160 | end 161 | local jump_ok, err = pcall(fn) 162 | if not jump_ok then 163 | vim.notify(err, vim.log.levels.WARN) 164 | end 165 | end 166 | print("Move mode exited") 167 | end 168 | 169 | 170 | return M 171 | -------------------------------------------------------------------------------- /plugin/overfly.lua: -------------------------------------------------------------------------------- 1 | local move_maps = { 2 | {']q', ':cnext'}, 3 | {'[q', ':cprevious'}, 4 | {']Q', ':cfirst'}, 5 | {'[Q', ':clast'}, 6 | {']l', ':lnext'}, 7 | {'[l', ':lprevious'}, 8 | {']L', ':lfirst'}, 9 | {'[L', ':llast'}, 10 | {']w', function() require("overfly").next_diagnostic() end}, 11 | {'[w', function() require("overfly").prev_diagnostic() end}, 12 | {']v', function() require("overfly").next_highlight() end}, 13 | {'[v', function() require("overfly").prev_highlight() end}, 14 | {']M', function() require("overfly").select_methods { mode = "next" } end}, 15 | {'[M', function() require("overfly").select_methods { mode = "prev" } end}, 16 | } 17 | 18 | local keymap = vim.keymap 19 | for _, move_map in pairs(move_maps) do 20 | keymap.set('n', move_map[1], move_map[2]) 21 | end 22 | 23 | 24 | keymap.set('n', ']v', function() 25 | require("overfly").move({ 26 | next = require("overfly").next_highlight, 27 | prev = require("overfly").prev_highlight 28 | }) 29 | end) 30 | keymap.set("n", "]q", function() 31 | require("overfly").move({ 32 | next = function() vim.cmd("cnext") end, 33 | prev = function() vim.cmd("cprev") end, 34 | }) 35 | end) 36 | keymap.set("n", "]l", function() 37 | require("overfly").move({ 38 | next = function() vim.cmd("lnext") end, 39 | prev = function() vim.cmd("lprev") end, 40 | }) 41 | end) 42 | keymap.set("n", "]w", function() 43 | local overfly = require("overfly") 44 | overfly.move({ 45 | next = overfly.next_diagnostic, 46 | prev = overfly.prev_diagnostic 47 | }) 48 | end) 49 | keymap.set("n", "]m", function() 50 | local overfly = require("overfly") 51 | overfly.move({ 52 | next = function() overfly.select_methods_sync { mode = "next", pick_first = true } end, 53 | prev = function() overfly.select_methods_sync { mode = "prev", pick_first = true } end, 54 | }) 55 | end) 56 | keymap.set("n", "]c", function() 57 | require("overfly").move({ 58 | next = function() vim.cmd.normal("]c") end, 59 | prev = function() vim.cmd.normal("[c") end, 60 | }) 61 | end) 62 | -------------------------------------------------------------------------------- /tests/minimal.vim: -------------------------------------------------------------------------------- 1 | set rtp+=. 2 | set rtp+=../plenary.nvim 3 | runtime! plugin/plenary.vim 4 | --------------------------------------------------------------------------------