Skip to content

Instantly share code, notes, and snippets.

@dharmx
Last active March 28, 2024 20:49
Show Gist options
  • Save dharmx/b4b890c550f41500419510c75e82f849 to your computer and use it in GitHub Desktop.
Save dharmx/b4b890c550f41500419510c75e82f849 to your computer and use it in GitHub Desktop.
IDEA-like command panel for Gradle.
-- place this at ~/.config/nvim/telescope/_extensions/gradle.lua
-- then require("telescope").load_extension("gradle") and rm these two lines
---@diagnostic disable: undefined-field
local J = require("plenary.job")
local Path = require("plenary.path")
local finders = require("telescope.finders")
local sorters = require("telescope.sorters")
local actions = require("telescope.actions")
local pickers = require("telescope.pickers")
local state = require("telescope.state")
local make_entry = require("telescope.make_entry")
local previewer = require("telescope.previewers.buffer_previewer")
local entry_display = require("telescope.pickers.entry_display")
local actions_layout = require("telescope.actions.layout")
local actions_state = require("telescope.actions.state")
local dialog = require("telescope.previewers.utils").set_preview_message
local HEAD = "<head>"
local NORM = "<norm>"
local LONE = "<lone>"
local MAIN = "<main>"
-- TODO: Implement recents.
local defaults = {
separator = " ",
gradle_icon = "",
results_title = "",
fill = "=",
prompt_title = "Gradle Tasks",
on_choice = function(entry) vim.cmd.terminal(entry.cmd) end,
cache_filename = ".cached-tasks",
previewer = false,
layout_config = {
preview_cutoff = 120,
width = function(_, maximum_columns, _) return math.min(maximum_columns, 90) end,
height = function(_, _, maximum_lines) return math.min(maximum_lines, 20) end,
},
}
-- API Implementations {{{
local function gen_from_gradle_cmds(opts)
local displayer = entry_display.create({
separator = opts.separator,
items = {
{ width = 1 },
{ width = 80 },
{ remaining = true },
},
})
local function make_display(entry)
local entries = {}
if entry.value.type == HEAD then -- make this non-interactive
table.insert(entries, { opts.gradle_icon, "@constant" })
if entry.value.main then
table.insert(entries, { entry.value.label, "@constant" })
else
table.insert(entries, { entry.value.label, "@include" })
end
elseif entry.value.type == NORM then
table.insert(entries, " ")
table.insert(entries, { entry.value.label, "@float" })
elseif entry.value.type == LONE then
table.insert(entries, " ")
table.insert(entries, { entry.value.label, "@debug" })
else
table.insert(entries, " ")
table.insert(entries, { entry.value.label, "@define" })
end
return displayer(entries)
end
return function(entry)
local head = ""
if entry.head then heading = entry.head .. ":" end
return make_entry.set_default_entry_mt({
value = entry,
display = make_display,
ordinal = head .. entry.type .. ":" .. entry.label,
}, opts)
end
end
-- }}}
local function parse_lines(lines, ignore_lines)
local items = {}
local will_break = false
local head
for index, line in ipairs(lines) do
line = vim.trim(line)
if not vim.tbl_contains(ignore_lines, index) and not line:match("^-+$") then
local item = {}
if line == "" then
will_break = true
elseif line:match("^%w") then
if will_break then
item.type = HEAD
item.label = line
item.about = line
head = line
will_break = false
else
local tokens = vim.split(line, " - ", { plain = true })
if #tokens ~= 0 and #tokens > 1 then
item.head = head
item.type = tokens[1]:match("^%w+:") and NORM or LONE
item.label = table.remove(tokens, 1)
item.about = table.concat(tokens, " - ")
item.cmd = "./gradlew " .. item.label
else
item.head = head
item.type = tokens[1]:match("^%w+:") and LONE or MAIN
item.label = table.remove(tokens, 1)
item.about = "No description."
item.cmd = "./gradlew " .. item.label
end
end
end
if item.type then table.insert(items, item) end
end
end
items[1].main = true
items[1].type = HEAD
return items
end
local function get_tasks(on_success, title)
---@type Path
local tasks_path = Path:new("./" .. title)
if not tasks_path:exists() then
tasks_path:touch({ exists_ok = true })
tasks_path:write("{}", "w")
end
local cache_raw = tasks_path:read()
local tasks_cache = {}
if type(cache_raw) == "string" then
tasks_cache = vim.json.decode(cache_raw)
if tasks_cache then
on_success(tasks_cache)
else
tasks_cache = {}
end
end
-- Parser Job {{{
J:new({
command = "./gradlew",
args = { "tasks", "--all" },
on_exit = function(self, code, _)
if code ~= 0 then return end
local tasks = self:result()
tasks = parse_lines(tasks, { 1, 2, 3, #tasks - 2, #tasks - 1, #tasks })
local equal = #tasks == #tasks_cache
if equal then -- deep array equal
for index, item in ipairs(tasks) do
local cache_item = tasks_cache[index]
if cache_item and item.type == cache_item.type then
if item.about ~= cache_item.about or item.label ~= cache_item.label then equal = true end
end
end
end
-- WARN: this will cause problems when implementing recents
vim.schedule(function()
if tasks_cache and equal then return end -- only refresh if the cache and new tasks aren't equal
tasks_path:write(vim.json.encode(tasks), "w")
on_success(tasks)
end)
end,
}):start()
-- }}}
end
return require("telescope").register_extension({
exports = {
gradle = function(opts)
if not vim.loop.fs_realpath("./gradlew") then return end
opts = vim.F.if_nil(opts, {})
opts = vim.tbl_deep_extend("keep", opts, defaults)
---@type Picker
local picker
get_tasks(function(tasks)
if picker then -- refreshed with updated values (if any)
picker:refresh(finders.new_table({ results = tasks, entry_maker = gen_from_gradle_cmds(opts) }), opts)
return
end
picker = pickers.new(opts, { -- currently viewing cached values
finder = finders.new_table({ results = tasks, entry_maker = gen_from_gradle_cmds(opts) }),
sorter = sorters.get_fzy_sorter(opts),
previewer = previewer.new_buffer_previewer({
define_preview = function(self, entry)
local about = vim.F.if_nil(entry.value.about, "NOT APPLICABLE")
dialog(self.state.bufnr, self.state.winid, about, opts.fill)
end,
}),
attach_mappings = function(buffer, map)
map("n", "<C-P>", actions_layout.toggle_preview)
map("i", "<C-P>", actions_layout.toggle_preview)
local function smart_move(move_type)
return function(...)
local entry = actions_state.get_selected_entry().value
if entry.type ~= "<head>" then return end
-- HACK: having all entries as <head>s will result in a infinite loop
-- so we just get the current sorter state and see what was in the prompt
-- and if the prompt matches a case where only heads are viewed then we
-- do not move the current selection
-- TODO: Is there a better way to do this?
local move = true
local sorter = state.get_status(...).picker.sorter
if
sorter
and sorter._discard_state
and sorter._discard_state.prompt:len() > 3 -- hack case
and sorter._discard_state.prompt:match("^<hea") -- hack case
then
move = false
end
if move then actions["move_selection_" .. move_type](...) end
end
end
actions.move_selection_next:enhance({ post = smart_move("next") })
actions.move_selection_previous:enhance({ post = smart_move("previous") })
actions.select_default:replace(function()
local entry = actions_state.get_selected_entry().value
-- do not allow the users to use <head> entries
if entry.type ~= "<head>" then
actions.close(buffer)
opts.on_choice(entry)
end
end)
return true
end,
})
picker:find()
end, opts.cache_filename)
end,
},
})
@dharmx
Copy link
Author

dharmx commented Mar 28, 2024

demo.mp4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment