Skip to content

Commit

Permalink
feat: cycle scopes with Grapple.cycle_scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
cbochs committed Apr 27, 2024
1 parent 386b583 commit 6c88bfd
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 68 deletions.
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -438,15 +438,48 @@ Where:
<summary><b>Examples</b></summary>

```lua
-- Cycle to the previous tagged file
-- Cycle to the next tagged file
require("grapple").cycle_tags("next")

-- Cycle to the next tagged file
-- Cycle to the previous tagged file
require("grapple").cycle_tags("prev")
```

</details>

#### `Grapple.cycle_scopes`

Cycle through and use the next or previous available scope. By default, will only cycle through non-`hidden` scopes. Use `{ all = true }` to cycle through _all_ defined scopes.

**API**: `require("grapple").cycle_scopes(direction, opts)`

Where:

- **`direction`**: `"next"` | `"prev"`
- **`opts?`**: `table`
- **`scope?`**: `string` scope name (default: `settings.scope`)
- **`all?`**: `boolean` (default: `false`)

<details>
<summary><b>Examples</b></summary>

```lua
-- Cycle to the next scope
require("grapple").cycle_scopes("next")

-- Cycle to the previous scope
require("grapple").cycle_scopes("prev")

-- Hide a scope during Grapple setup
require("grapple").setup({
default_scopes = {
cwd = { hidden = true }
}
})
```

</details>

#### `Grapple.unload`

Unload tags for a give (scope) name or loaded scope (id).
Expand Down
25 changes: 23 additions & 2 deletions lua/grapple.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ local Grapple = {}
---@param err? string
---@return string? err
local function notify_err(err)
if err and not vim.env.CI then
if err then
vim.notify(err, vim.log.levels.ERROR)
end
return err
Expand Down Expand Up @@ -57,7 +57,13 @@ end
---@param opts? grapple.options
---@return string? error
function Grapple.cycle_tags(direction, opts)
return notify_err(Grapple.app():cycle_tags(direction, opts))
local next_index, err = Grapple.app():cycle_tags(direction, opts)
if err then
vim.notify(err, vim.log.levels.ERROR)
return err
elseif next_index then
Grapple.select({ index = next_index })
end
end

-- Cycle through and select the next or previous available tag for a given scope.
Expand Down Expand Up @@ -88,6 +94,21 @@ function Grapple.cycle_backward(opts)
return Grapple.cycle_tags("prev", opts)
end

---Cycle through and use the next or previous available scope.
---By default, will only cycle through non-`hidden` scopes.
---@param direction "next" | "prev"
---@param opts? { scope?: string, all?: boolean }
---@return string? error
function Grapple.cycle_scopes(direction, opts)
local next_scope, err = Grapple.app():cycle_scopes(direction, opts)
if err then
vim.notify(err --[[ @as string ]], vim.log.levels.ERROR)
return err
elseif next_scope then
Grapple.use_scope(next_scope)
end
end

---@param opts? grapple.options
---@return string? error
function Grapple.touch(opts)
Expand Down
86 changes: 55 additions & 31 deletions lua/grapple/app.lua
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ local Settings = require("grapple.settings")
local State = require("grapple.state")
local TagContent = require("grapple.tag_content")
local TagManager = require("grapple.tag_manager")
local Util = require("grapple.util")
local Window = require("grapple.window")

---@class grapple.app
Expand Down Expand Up @@ -196,7 +197,7 @@ function App:select(opts)
local path, _ = self:extract_path(opts.path, opts.buffer)
opts.path = path

return self:enter_with_event(function(container)
return self:enter(function(container)
local index, err = container:find(opts)
if err then
return err
Expand All @@ -205,38 +206,37 @@ function App:select(opts)
local tag = assert(container:get({ index = index }))

tag:select(opts.command)
end, { scope = opts.scope, scope_id = opts.scope_id })
end, { scope = opts.scope, scope_id = opts.scope_id, event = true })
end

---@param current_index? integer
---@param current_idx? integer
---@param direction "next" | "prev"
---@param length integer
---@return integer
local function next_index(current_index, direction, length)
local function next_index(current_idx, direction, length)
-- Fancy maths to get the next index for a given direction
-- 1. Change to 0-based indexing
-- 2. Perform index % container length, being careful of negative values
-- 3. Change back to 1-based indexing
-- stylua: ignore
current_index = (
current_index
current_idx = (
current_idx
or direction == "next" and length
or direction == "prev" and 1
) - 1

local next_inc = direction == "next" and 1 or -1
local next_idx = math.fmod(current_index + next_inc + length, length) + 1
local next_idx = math.fmod(current_idx + next_inc + length, length) + 1

return next_idx
end

-- Cycle through and select the next or previous available tag for a given scope.
-- Cycle through and find the next or previous available tag for a given scope.
---By default, uses the current scope
---@param direction "next" | "prev" | "previous" | "forward" | "backward"
---@param opts? grapple.options
---@return string? error
---@return integer | nil next_index, string? error
function App:cycle_tags(direction, opts)

-- stylua: ignore
direction = direction == "forward" and "next"
or direction == "backward" and "prev"
Expand All @@ -246,32 +246,63 @@ function App:cycle_tags(direction, opts)
---@cast direction "next" | "prev"

if not vim.tbl_contains({ "next", "prev" }, direction) then
return string.format("invalid direction: %s", direction)
return nil, string.format("invalid direction: %s", direction)
end

opts = opts or {}

local path, _ = self:extract_path(opts.path, opts.buffer or 0)
opts.path = path

return self:enter_with_event(function(container)
return self:enter_with_result(function(container)
if container:is_empty() then
return
return nil, nil
end

local index = next_index(container:find(opts), direction, container:len())

local tag, err = container:get({ index = index })
if err or not tag then
return err
local current_idx, _ = container:find(opts)
local next_idx = next_index(container:find(opts), direction, container:len())
if next_idx == current_idx then
return nil, nil
end

---@diagnostic disable-next-line: redefined-local
local err = tag:select()
if err then
return err
end
end, { scope = opts.scope, scope_id = opts.scope_id })
return next_idx, nil
end, { scope = opts.scope, scope_id = opts.scope_id, event = true })
end

-- Cycle through and find the next or previous available scope.
---By default, will only cycle through non-`hidden` scopes.
---@param direction "next" | "prev"
---@param opts? { scope?: string, all?: boolean }
---@return string | nil next_scope, string? error
function App:cycle_scopes(direction, opts)
if not vim.tbl_contains({ "next", "prev" }, direction) then
return nil, string.format("invalid direction: %s", direction)
end

opts = opts or {}

local current_scope, err = self.scope_manager:get(opts.scope or self.settings.scope)
if err or not current_scope then
return nil, err
end

local scopes = self:list_scopes()
if not opts.all then
scopes = vim.tbl_filter(function(scope)
return not scope.hidden
end, scopes)
end

local current_idx = Util.index_of(scopes, function(s)
return s.name == current_scope.name
end)

local next_idx = next_index(current_idx, direction, #scopes)
if next_idx == current_idx then
return nil, nil
end

return scopes[next_idx].name, nil
end

---Update a tag in a given scope
Expand Down Expand Up @@ -590,11 +621,4 @@ function App:enter_with_save(callback, opts)
return self:enter(callback, vim.tbl_deep_extend("force", opts or {}, { sync = true, event = true }))
end

---@param callback fun(container: grapple.tag_container): string? error
---@param opts? grapple.app.enter_options
---@return string? error
function App:enter_with_event(callback, opts)
return self:enter(callback, vim.tbl_deep_extend("force", opts or {}, { sync = false, event = true }))
end

return App
12 changes: 12 additions & 0 deletions lua/grapple/util.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ function Util.reduce(list, fn, init)
return acc
end

---@generic T
---@param list table
---@param fn fun(value: T): boolean
---@return integer | nil
function Util.index_of(list, fn)
for i, v in ipairs(list) do
if fn(v) then
return i
end
end
end

---@generic T
---@param tbl_a T[]
---@param tbl_b T[]
Expand Down
67 changes: 34 additions & 33 deletions plugin/grapple.lua
Original file line number Diff line number Diff line change
Expand Up @@ -81,27 +81,28 @@ vim.api.nvim_create_user_command(
local scope_kwargs = { "scope", "id" }
local window_kwargs = { "style", unpack(scope_kwargs) }

-- stylua: ignore
-- Lookup table of API functions and their available arguments
local subcommand_lookup = {
clear_cache = { args = { "scope" }, kwargs = {} },
cycle_tags = { args = { "direction" }, kwargs = use_kwargs },
open_loaded = { args = {}, kwargs = { "all" } },
open_scopes = { args = {}, kwargs = {} },
open_tags = { args = {}, kwargs = window_kwargs },
prune = { args = {}, kwargs = { "limit" } },
quickfix = { args = {}, kwargs = scope_kwargs },
reset = { args = {}, kwargs = scope_kwargs },
select = { args = {}, kwargs = use_kwargs },
tag = { args = {}, kwargs = new_kwargs },
toggle = { args = {}, kwargs = tag_kwargs },
toggle_loaded = { args = {}, kwargs = { "all" } },
toggle_scopes = { args = {}, kwargs = { "all" } },
toggle_tags = { args = {}, kwargs = window_kwargs },
unload = { args = {}, kwargs = scope_kwargs },
untag = { args = {}, kwargs = use_kwargs },
use_scope = { args = { "scope" }, kwargs = {} },
}
-- stylua: ignore
-- Lookup table of API functions and their available arguments
local subcommand_lookup = {
clear_cache = { args = { "scope" }, kwargs = {} },
cycle_tags = { args = { "direction" }, kwargs = use_kwargs },
cycle_scopes = { args = { "direction" }, kwargs = { "scope", "all" } },
open_loaded = { args = {}, kwargs = { "all" } },
open_scopes = { args = {}, kwargs = {} },
open_tags = { args = {}, kwargs = window_kwargs },
prune = { args = {}, kwargs = { "limit" } },
quickfix = { args = {}, kwargs = scope_kwargs },
reset = { args = {}, kwargs = scope_kwargs },
select = { args = {}, kwargs = use_kwargs },
tag = { args = {}, kwargs = new_kwargs },
toggle = { args = {}, kwargs = tag_kwargs },
toggle_loaded = { args = {}, kwargs = { "all" } },
toggle_scopes = { args = {}, kwargs = { "all" } },
toggle_tags = { args = {}, kwargs = window_kwargs },
unload = { args = {}, kwargs = scope_kwargs },
untag = { args = {}, kwargs = use_kwargs },
use_scope = { args = { "scope" }, kwargs = {} },
}

-- Lookup table of arguments and their known values
local argument_lookup = {
Expand Down Expand Up @@ -154,10 +155,10 @@ vim.api.nvim_create_user_command(
-- "Grapple sub|"

if #input == 2 then
-- stylua: ignore
return current == ""
and subcmds
or vim.tbl_filter(Util.startswith(current), subcmds)
-- stylua: ignore
return current == ""
and subcmds
or vim.tbl_filter(Util.startswith(current), subcmds)
end

local completion = subcommand_lookup[input_subcmd]
Expand All @@ -175,10 +176,10 @@ vim.api.nvim_create_user_command(
local arg_name = completion.args[#input_args]
local arg_values = argument_lookup[arg_name] or {}

-- stylua: ignore
return current == ""
and arg_values
or vim.tbl_filter(Util.startswith(current), arg_values)
-- stylua: ignore
return current == ""
and arg_values
or vim.tbl_filter(Util.startswith(current), arg_values)
end

-- "Grapple subcmd arg |"
Expand All @@ -189,10 +190,10 @@ vim.api.nvim_create_user_command(
local input_keys = vim.tbl_map(Util.match_key, input_kwargs)
local kwarg_keys = Util.subtract(completion.kwargs, input_keys)

-- stylua: ignore
local filtered = current == ""
and kwarg_keys
or vim.tbl_filter(Util.startswith(current), completion.kwargs)
-- stylua: ignore
local filtered = current == ""
and kwarg_keys
or vim.tbl_filter(Util.startswith(current), completion.kwargs)

return vim.tbl_map(Util.with_suffix("="), filtered)
end
Expand Down
16 changes: 16 additions & 0 deletions tests/grapple_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,22 @@ describe("Grapple", function()
end)
end)

describe("Grapple.cycle_scopes", function()
describe("next", function()
it("works", function()
assert.is_nil(Grapple.cycle_scopes("next", { scope = "git" }))
assert.is_same("git_branch", Grapple.app().settings.scope)
end)
end)

describe("prev", function()
it("works", function()
assert.is_nil(Grapple.cycle_scopes("prev", { scope = "git_branch" }))
assert.is_same("git", Grapple.app().settings.scope)
end)
end)
end)

describe("Grapple.touch", function()
it("works", function()
vim.api.nvim_win_set_buf(0, vim.fn.bufnr("/test1", false))
Expand Down
2 changes: 2 additions & 0 deletions tests/minimal_init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ vim.fs.joinpath = vim.fs.joinpath
return path
end

vim.notify = function() end

local root_path = vim.fn.fnamemodify(".", ":p")
local temp_path = vim.fs.joinpath(root_path, ".tests")

Expand Down

0 comments on commit 6c88bfd

Please sign in to comment.