Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ dictionary that is created if you have set any of the decorations to `true` in y
The currently supported decorations are:

- App version - The current version of the app from the `pubspec.yaml`.
- Device - the device passed to the flutter run command
- Device - the running device, or, while nothing is running, the device `flutter run` would default to
- Project config - the name of the currently selected project configuration

To add them to your config you can do something like
Expand Down
21 changes: 19 additions & 2 deletions lua/flutter-tools/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ local function use_debugger_runner(force_debug)
return false
end

---@param device Device?
local function set_current_device(device)
current_device = device
utils.emit_event(utils.events.DEVICE_CHANGED, { data = device })
end

---Resolve the device name from the `Launching ... on <device> in <mode> mode` output line
---@param line string
local function update_device_from_output(line)
local name = line:match("^Launching .+ on (.+) in %w+ mode")
if not name then return end
local device = vim.tbl_extend("force", current_device or {}, { name = vim.trim(name) })
set_current_device(device)
end

function M.current_device() return current_device end

function M.is_running() return runner ~= nil and runner:is_running() end
Expand Down Expand Up @@ -76,13 +91,14 @@ end
---@param is_err boolean if this is stdout or stderr
local function on_run_data(is_err, data)
if is_err and config.dev_log.notify_errors then ui.notify(data, ui.ERROR, { timeout = 5000 }) end
update_device_from_output(data)
dev_log.log(data)
end

local function shutdown()
if runner then runner:cleanup() end
runner = nil
current_device = nil
set_current_device(nil)
utils.emit_event(utils.events.PROJECT_CONFIG_CHANGED)
dev_tools.on_flutter_shutdown()
end
Expand Down Expand Up @@ -240,7 +256,7 @@ local function run(opts, project_conf, launch_config)
executable.get(function(paths)
local args = opts.cli_args or get_run_args(opts, project_conf)

current_device = opts.device or get_device_from_args(args)
set_current_device(opts.device or get_device_from_args(args))
if project_conf then
if project_conf.pre_run_callback then
local callback_args = {
Expand Down Expand Up @@ -642,6 +658,7 @@ end
if __TEST then
M.__run = run
M.__get_run_args = get_run_args
M.__update_device_from_output = update_device_from_output
end

return M
10 changes: 8 additions & 2 deletions lua/flutter-tools/decorations.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local lazy = require("flutter-tools.lazy")
local commands = lazy.require("flutter-tools.commands") ---@module "flutter-tools.commands"
local devices = lazy.require("flutter-tools.devices") ---@module "flutter-tools.devices"
local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path"
local utils = lazy.require("flutter-tools.utils") ---@module "flutter-tools.utils"
local Path = require("plenary.path")
Expand Down Expand Up @@ -32,12 +33,17 @@ end

local function device_show()
local device = commands.current_device()
if device then set_decoration_item("device", device) end
if device then return set_decoration_item("device", device) end
devices.get_default_device(function(default_device)
if commands.current_device() then return end
set_decoration_item("device", default_device)
end)
end

function M.statusline.device()
device_show()
api.nvim_create_autocmd("User", {
pattern = utils.events.APP_STARTED,
pattern = { utils.events.APP_STARTED, utils.events.DEVICE_CHANGED },
callback = device_show,
})
end
Expand Down
56 changes: 56 additions & 0 deletions lua/flutter-tools/devices.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ local M = {
local EMULATOR = 1
local DEVICE = 2

local NON_EPHEMERAL_PLATFORMS = {
["darwin"] = true,
["darwin-arm64"] = true,
["darwin-x64"] = true,
["linux-arm64"] = true,
["linux-x64"] = true,
["windows-arm64"] = true,
["windows-x64"] = true,
["web-javascript"] = true,
}

---@param result string[]
---@param type integer
local function get_devices(result, type)
Expand Down Expand Up @@ -86,6 +97,51 @@ function M.to_selection_entries(result, device_type)
end, devices)
end

---@param entry table
---@return Device
local function to_device(entry)
return {
name = entry.name,
id = entry.id,
platform = entry.targetPlatform,
system = entry.sdk,
type = DEVICE,
}
end

---Pick the device `flutter run` would use without an explicit `-d` argument.
---Mirrors flutter's own resolution: a single supported device wins, otherwise
---a single ephemeral (non desktop/web) device wins, otherwise flutter prompts.
---@param result string[]
---@return Device?
function M.resolve_default_device(result)
local ok, decoded = pcall(vim.json.decode, table.concat(result, "\n"))
if not ok or type(decoded) ~= "table" then return end

local supported = vim.tbl_filter(function(entry) return entry.isSupported ~= false end, decoded)
if #supported == 0 then return end
if #supported == 1 then return to_device(supported[1]) end

local ephemeral = vim.tbl_filter(
function(entry) return not NON_EPHEMERAL_PLATFORMS[entry.targetPlatform] end,
supported
)
if #ephemeral == 1 then return to_device(ephemeral[1]) end
end

---Asynchronously get the device `flutter run` would default to
---@param callback fun(device: Device?)
function M.get_default_device(callback)
executable.flutter(function(cmd)
local job = Job:new({ command = cmd, args = { "devices", "--machine" } })
job:after(vim.schedule_wrap(function(j, code)
if code ~= 0 then return callback(nil) end
callback(M.resolve_default_device(j:result()))
end))
job:start()
end)
end

-----------------------------------------------------------------------------//
-- Emulators
-----------------------------------------------------------------------------//
Expand Down
1 change: 1 addition & 0 deletions lua/flutter-tools/utils/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ function M.join(lines) return table.concat(lines, "\n") end
M.events = {
PROJECT_CONFIG_CHANGED = "FlutterToolsProjectConfigChanged",
APP_STARTED = "FlutterToolsAppStarted",
DEVICE_CHANGED = "FlutterToolsDeviceChanged",
OUTLINE_CHANGED = "FlutterToolsOutlineChanged",
LSP_ANALYSIS_COMPLETED = "FlutterToolsLspAnalysisCompleted",
}
Expand Down
21 changes: 21 additions & 0 deletions tests/commands_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,25 @@ describe("commands", function()
)
end
)

it("should pick up the device name from the run output", function()
commands.__update_device_from_output(
"Launching lib/main.dart on iPhone 16 Pro in debug mode..."
)

assert.equal("iPhone 16 Pro", commands.current_device().name)
end)

it("should keep the device id when the name is parsed from the run output", function()
commands.__update_device_from_output("Launching lib/main.dart on macOS in debug mode...")

local device = commands.current_device()
assert.equal("macOS", device.name)
end)

it("should ignore unrelated output lines", function()
commands.__update_device_from_output('Running "flutter pub get" in example...')

assert.is_nil(commands.current_device())
end)
end)
49 changes: 49 additions & 0 deletions tests/devices_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,53 @@ INFO | Storing crashdata in: /tmp/android-ts/emu-crash-34.2.14.db, detection
assert.same({}, devices.to_selection_entries(nil))
end)
end)

describe("default device resolution - ", function()
local devices = require("flutter-tools.devices")

local function machine_output(entries) return { vim.json.encode(entries) } end

it("should pick the only supported device", function()
local device = devices.resolve_default_device(machine_output({
{ name = "macOS", id = "macos", isSupported = true, targetPlatform = "darwin" },
}))

assert.equal("macos", device.id)
assert.equal("macOS", device.name)
assert.equal("darwin", device.platform)
end)

it("should prefer the single ephemeral device over desktop and web", function()
local device = devices.resolve_default_device(machine_output({
{ name = "iPhone 16", id = "sim-id", isSupported = true, targetPlatform = "ios" },
{ name = "macOS", id = "macos", isSupported = true, targetPlatform = "darwin" },
{ name = "Chrome", id = "chrome", isSupported = true, targetPlatform = "web-javascript" },
}))

assert.equal("sim-id", device.id)
end)

it("should ignore unsupported devices", function()
local device = devices.resolve_default_device(machine_output({
{ name = "iPhone 16", id = "sim-id", isSupported = false, targetPlatform = "ios" },
{ name = "macOS", id = "macos", isSupported = true, targetPlatform = "darwin" },
}))

assert.equal("macos", device.id)
end)

it("should return nil when several ephemeral devices are connected", function()
local device = devices.resolve_default_device(machine_output({
{ name = "iPhone 16", id = "sim-id", isSupported = true, targetPlatform = "ios" },
{ name = "Pixel 8", id = "emulator-5554", isSupported = true, targetPlatform = "android" },
}))

assert.is_nil(device)
end)

it("should return nil for no devices or invalid output", function()
assert.is_nil(devices.resolve_default_device(machine_output({})))
assert.is_nil(devices.resolve_default_device({ "not json" }))
end)
end)
end)