Last active
March 28, 2024 20:49
-
-
Save dharmx/b4b890c550f41500419510c75e82f849 to your computer and use it in GitHub Desktop.
IDEA-like command panel for Gradle.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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, | |
}, | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
demo.mp4