diff --git a/README.md b/README.md index e2fa4a3..3604c08 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,8 @@ require("flutter-tools").setup {} -- use defaults - `FlutterDebug` - Force run current project in debug mode. - `FlutterDevices` - Brings up a list of connected devices to select from. - `FlutterEmulators` - Similar to devices but shows a list of emulators to choose from. + Set `emulators.launcher` to start the selected emulator with your own command instead of + `flutter emulator --launch`, see the [full configuration](#full-configuration). - `FlutterReload` - Reload the running project. - `FlutterRestart` - Restart the current project. - `FlutterQuit` - Ends a running session. @@ -284,6 +286,16 @@ require("flutter-tools").setup { widget_preview = { web_server = true, -- open the widget previewer with vim.ui.open; false lets Flutter launch its own Chrome }, + emulators = { + -- optional function to launch the emulator picked in `FlutterEmulators` with your own command, + -- e.g. the Android SDK `emulator` binary with extra flags; return nil to use `flutter emulator --launch` + -- launcher = function(emulator, paths) + -- if emulator.system ~= "android" then return nil end + -- local args = { "@" .. emulator.id, "-gpu", "host", "-accel", "on" } + -- if emulator.cold_boot then table.insert(args, "-no-snapshot-load") end + -- return { command = "emulator", args = args } + -- end, + }, outline = { open_cmd = "30vnew", -- command to use to open the outline buffer auto_open = false -- if true this will open the outline automatically when it is first populated diff --git a/lua/flutter-tools/config.lua b/lua/flutter-tools/config.lua index b0613f0..b961845 100644 --- a/lua/flutter-tools/config.lua +++ b/lua/flutter-tools/config.lua @@ -27,6 +27,13 @@ local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.util ---@field flutter? table|string -- options applied to `flutter run` command ---@field dart? table|string -- options appliert to `dart run` command --- +---@class flutter.EmulatorLaunchSpec +---@field command string +---@field args? string[] +--- +---@class flutter.EmulatorsOpts +---@field launcher? fun(emulator: Device, paths: flutter.Paths): flutter.EmulatorLaunchSpec? +--- ---@class flutter.Config ---@field flutter_path? string Path to the Flutter SDK ---@field flutter_lookup_cmd? string Command to find Flutter SDK @@ -44,6 +51,7 @@ local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.util ---@field dev_log? flutter.DevLogOpts ---@field dev_tools? {autostart: boolean, auto_open_browser: boolean} ---@field widget_preview? {web_server: boolean} +---@field emulators? flutter.EmulatorsOpts ---@field analyzer_web_port? number local M = {} @@ -152,6 +160,9 @@ local config = { widget_preview = { web_server = true, }, + emulators = { + launcher = nil, + }, } local deprecations = { diff --git a/lua/flutter-tools/devices.lua b/lua/flutter-tools/devices.lua index 8712968..bb5f005 100644 --- a/lua/flutter-tools/devices.lua +++ b/lua/flutter-tools/devices.lua @@ -4,14 +4,12 @@ local ui = lazy.require("flutter-tools.ui") ---@module "flutter-tools.ui" local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.utils" local commands = lazy.require("flutter-tools.commands") ---@module "flutter-tools.commands" local executable = lazy.require("flutter-tools.executable") ---@module "flutter-tools.executable" +local config = lazy.require("flutter-tools.config") ---@module "flutter-tools.config" local fmt = string.format ---@alias Device {name: string, id: string, platform: string, system: string, type: integer, cold_boot: boolean} -local M = { - ---@type Job - emulator_job = nil, -} +local M = {} local EMULATOR = 1 local DEVICE = 2 @@ -180,19 +178,35 @@ end ---@param job Job local function handle_launch(job) ui.notify(utils.join(job:result())) end -function M.close_emulator() - if M.emulator_job then M.emulator_job:shutdown() end +---@param emulator Device +---@param paths flutter.Paths +---@return flutter.EmulatorLaunchSpec? +local function custom_launch_spec(emulator, paths) + local launcher = config.emulators.launcher + if not launcher then return end + return launcher(emulator, paths) end ----@param emulator table +---@param emulator Device function M.launch_emulator(emulator) if not emulator then return end - executable.flutter(function(cmd) - args = { "emulator", "--launch", emulator.id } - if emulator.cold_boot then table.insert(args, "--cold") end - M.emulator_job = Job:new({ command = cmd, args = args }) - M.emulator_job:after_success(vim.schedule_wrap(handle_launch)) - M.emulator_job:start() + executable.get(function(paths) + local spec = custom_launch_spec(emulator, paths) + local job + if spec then + job = Job:new({ command = spec.command, args = spec.args }) + job:after_failure( + vim.schedule_wrap( + function(j) ui.notify(utils.join(j:stderr_result()), ui.ERROR, { timeout = 5000 }) end + ) + ) + else + local args = { "emulator", "--launch", emulator.id } + if emulator.cold_boot then table.insert(args, "--cold") end + job = Job:new({ command = paths.flutter_bin, args = args }) + job:after_success(vim.schedule_wrap(handle_launch)) + end + job:start() end) end diff --git a/tests/devices_spec.lua b/tests/devices_spec.lua index dd5c7de..39e79b9 100644 --- a/tests/devices_spec.lua +++ b/tests/devices_spec.lua @@ -144,4 +144,84 @@ INFO | Storing crashdata in: /tmp/android-ts/emu-crash-34.2.14.db, detection assert.is_nil(devices.resolve_default_device({ "not json" })) end) end) + + describe("emulator launch - ", function() + local devices + local config + local jobs + local modules = { + "flutter-tools.devices", + "flutter-tools.config", + "flutter-tools.executable", + "plenary.job", + } + local paths = { flutter_bin = "/sdk/bin/flutter" } + local emulator = { id = "Pixel_8", name = "Pixel 8", system = "android", type = 1 } + + before_each(function() + jobs = {} + for _, name in ipairs(modules) do + package.loaded[name] = nil + end + package.loaded["plenary.job"] = { + new = function(_, opts) + local job = { opts = opts, started = false } + function job:after_success() end + function job:after_failure() end + function job:start() self.started = true end + table.insert(jobs, job) + return job + end, + } + package.loaded["flutter-tools.executable"] = { + get = function(callback) callback(paths) end, + } + config = require("flutter-tools.config") + devices = require("flutter-tools.devices") + end) + + after_each(function() + for _, name in ipairs(modules) do + package.loaded[name] = nil + end + end) + + it("should launch through flutter by default", function() + devices.launch_emulator(vim.tbl_extend("force", emulator, { cold_boot = true })) + + assert.equal(1, #jobs) + assert.equal("/sdk/bin/flutter", jobs[1].opts.command) + assert.same({ "emulator", "--launch", "Pixel_8", "--cold" }, jobs[1].opts.args) + assert.is_true(jobs[1].started) + end) + + it("should use the command returned by a custom launcher", function() + local received + config.set({ + emulators = { + launcher = function(e, p) + received = { emulator = e, paths = p } + return { command = "emulator", args = { "@" .. e.id, "-gpu", "host" } } + end, + }, + }) + + devices.launch_emulator(emulator) + + assert.equal(emulator, received.emulator) + assert.equal(paths, received.paths) + assert.equal("emulator", jobs[1].opts.command) + assert.same({ "@Pixel_8", "-gpu", "host" }, jobs[1].opts.args) + assert.is_true(jobs[1].started) + end) + + it("should fall back to flutter when the launcher returns nil", function() + config.set({ emulators = { launcher = function() return nil end } }) + + devices.launch_emulator(emulator) + + assert.equal("/sdk/bin/flutter", jobs[1].opts.command) + assert.same({ "emulator", "--launch", "Pixel_8" }, jobs[1].opts.args) + end) + end) end)