diff --git a/doc/.keep b/doc/.keep new file mode 100644 index 0000000..e69de29 diff --git a/lua/init.lua b/lua/init.lua new file mode 100644 index 0000000..143c780 --- /dev/null +++ b/lua/init.lua @@ -0,0 +1,294 @@ +local Job = require("plenary.job") +local action_state = require "telescope.actions.state" +local actions = require "telescope.actions" +local conf = require("telescope.config").values +local finders = require("telescope.finders") +local pickers = require("telescope.pickers") +local global_state = require("telescope.state") +local utils = require("nvim-flutter-companion.utils") + +local M = {} + +local callbacks = {nil, nil} +local flutter_busy = false +local spinner_frames = {"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"} + +local EMULATOR = 1 +local DEVICE = 2 + +local get_config_path = function() + return utils.create_path(vim.fn.getcwd(), ".flutter-companion") +end + +local load_table = function(path) + local file, err = io.open(path, "r") + if err or file == nil then + return {} + end + + local lines = file:read("a") + file:close() + + local ok, json = + pcall( + function() + return vim.json.decode(lines) + end + ) + return utils.iff(ok, json, {}) +end + +local parse = function(line, device_type) + local parts = vim.split(line, "•") + local is_emulator = device_type == EMULATOR + local name_index = not is_emulator and 1 or 2 + local id_index = not is_emulator and 2 or 1 + if #parts == 4 then + return { + name = vim.trim(parts[name_index]), + id = vim.trim(parts[id_index]), + platform = vim.trim(parts[3]), + system = vim.trim(parts[4]), + type = device_type + } + end +end + +local save_table = function(data, filename) + local file, err = io.open(filename, "wb") + if err or file == nil then + return err + end + + file:write(vim.json.encode(data)) + file:close() +end + +_G.Flutter_Coc_Clear_Selected_Device = function() + local config_path = get_config_path() + local config_data = load_table(config_path) + if config_data.device ~= nil then + config_data.device = nil + save_table(config_data, config_path) + end +end + +_G.Flutter_Coc_Run_Selected_Device = function() + local config_path = get_config_path() + local config_data = load_table(config_path) + if config_data.device == nil then + if callbacks[1] == nil then + callbacks[1] = _G.Flutter_Coc_Run_Selected_Device + vim.api.nvim_command("Telescope coc flutter_devices") + end + else + vim.api.nvim_command("CocCommand flutter.run -d " .. config_data.device.id) + end +end + +_G.Flutter_Coc_Clear_Selected_Emulator = function() + local config_path = get_config_path() + local config_data = load_table(config_path) + if config_data.emulator ~= nil then + config_data.emulator = nil + save_table(config_data, config_path) + end +end + +_G.Flutter_Coc_Run_Selected_Emulator = function() + local config_path = get_config_path() + local config_data = load_table(config_path) + if config_data.emulator == nil then + if callbacks[2] == nil then + callbacks[2] = _G.Flutter_Coc_Run_Selected_Emulator + vim.api.nvim_command("Telescope coc flutter_emulators") + end + else + local cmd = "flutter" + if vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 then + cmd = cmd .. ".bat" + end + + local job = Job:new({command = cmd, args = {"emulators", "--launch", config_data.emulator.id}}) + job:start() + end +end + +local get_devices = function(result, type) + local devices = {} + for _, line in pairs(result) do + local device = parse(line, type) + if device then + table.insert(devices, device) + end + end + return devices +end + +local extract_device_props = function(result, device_type) + device_type = device_type or DEVICE + local devices = {} + local device_list = get_devices(result, device_type) + if #device_list > 0 then + for _, device in pairs(device_list) do + table.insert(devices, device) + end + end + return devices +end + +local parse_devices_output = function(job, callback) + local result = job:result() + local devices = extract_device_props(result, DEVICE) + callback(devices) +end + +local list_devices = function(callback) + local cmd = "flutter" + if vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 then + cmd = cmd .. ".bat" + end + + local job = Job:new({command = cmd, args = {"devices"}}) + job:after_success( + vim.schedule_wrap( + function() + parse_devices_output(job, callback) + end + ) + ) + job:start() +end + +local function list_emulators(callback) + local cmd = "flutter" + if vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 then + cmd = cmd .. ".bat" + end + + local job = Job:new({command = cmd, args = {"emulators"}}) + job:after_success( + vim.schedule_wrap( + function() + local emulators = extract_device_props(job:result(), EMULATOR) + callback(emulators) + end + ) + ) + job:start() +end + +local start_spinner = function(name, get_active) + local spinner = 0 + local record + + local function update_spinner() + if get_active() then + local next_record = + require("notify").notify( + "Detecting " .. name .. "...", + "info", + { + title = "Flutter Action", + hide_from_history = true, + icon = spinner_frames[(spinner % #spinner_frames) + 1], + replace = record + } + ) + record = next_record + spinner = spinner + 1 + vim.defer_fn(update_spinner, 100) + end + end + update_spinner() +end + +local stop_spinner = function(set_inactive) + set_inactive() + require("notify").dismiss() +end + +function M.flutter_list(is_emulators, opts) + if flutter_busy == true then + return + end + flutter_busy = true + + local config_path = get_config_path() + local config_data = load_table(config_path) + + local callback_index = utils.iff(is_emulators, 2, 1) + local list_function = utils.iff(is_emulators, list_emulators, list_devices) + local name = utils.iff(is_emulators, "Emulators", "Devices") + + local completed_callback + if callbacks[callback_index] ~= nil then + completed_callback = callbacks[callback_index] + callbacks[callback_index] = nil + end + + local active = true + start_spinner( + name, + function() + return active + end + ) + + list_function( + function(items) + stop_spinner( + function() + active = false + end + ) + + if #items == 0 then + vim.notify("No " .. name .. " were found", "error", {title = "Flutter Action"}) + else + global_state.set_global_key("selected_entry", nil) + + pickers.new( + opts, + { + prompt_title = "Flutter " .. name, + sorter = conf.generic_sorter(opts), + finder = finders.new_table( + { + results = items, + entry_maker = function(item) + return { + value = item, + ordinal = item.name, + display = item.name + } + end + } + ), + attach_mappings = function(prompt_bufnr --[[, map ]]) + actions.select_default:replace( + function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + if is_emulators then + config_data.emulator = selection.value + else + config_data.device = selection.value + end + save_table(config_data, config_path) + if completed_callback ~= nil then + completed_callback() + end + end + ) + return true + end + } + ):find() + end + flutter_busy = false + end + ) +end + +return M diff --git a/lua/nvim-flutter-companion/utils/init.lua b/lua/nvim-flutter-companion/utils/init.lua new file mode 100644 index 0000000..5337b49 --- /dev/null +++ b/lua/nvim-flutter-companion/utils/init.lua @@ -0,0 +1,25 @@ +local M = {} + +M.iff = function(b, l, r) + if b then + return l + end + + return r +end + +M.is_windows = vim.fn.has("win32") == 1 or vim.fn.has("win64") == 1 +M.directory_sep = M.iff(M.is_windows, "\\\\", "/") +M.not_directory_sep = M.iff(M.is_windows, "/", "\\\\") + +M.create_path = function(...) + local Path = require "plenary.path" + local ret = + Path:new({...}):absolute():gsub(M.not_directory_sep, M.directory_sep):gsub( + M.directory_sep .. M.directory_sep, + M.directory_sep + ) + return ret +end + +return M diff --git a/plugin/nvim-flutter-companion.lua b/plugin/nvim-flutter-companion.lua new file mode 100644 index 0000000..435970f --- /dev/null +++ b/plugin/nvim-flutter-companion.lua @@ -0,0 +1,4 @@ +if vim.g.loaded_nvim_flutter_companion == 1 then + return +end +vim.g.loaded_nvim_flutter_companion = 1