diff --git a/README.md b/README.md index 3a6edaf..6230165 100644 --- a/README.md +++ b/README.md @@ -209,8 +209,8 @@ require("flutter-tools").setup { -- this will show the current version of the flutter app from the pubspec.yaml file app_version = false, -- set to true to be able use the 'flutter_tools_decorations.device' in your statusline - -- this will show the currently running device if an application was started with a specific - -- device + -- this will show the running device, or, while nothing is running in a Flutter project, + -- the device `flutter run` would pick by default device = false, -- set to true to be able use the 'flutter_tools_decorations.project_config' in your statusline -- this will show the currently selected project configuration @@ -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 running device, or, while nothing is running, the device `flutter run` would default to +- Device - the running device, or, while nothing is running in a Flutter project, the device `flutter run` would pick by default (nothing when it would prompt for one) - Project config - the name of the currently selected project configuration To add them to your config you can do something like diff --git a/lua/flutter-tools/commands.lua b/lua/flutter-tools/commands.lua index 879be37..46e1a6d 100644 --- a/lua/flutter-tools/commands.lua +++ b/lua/flutter-tools/commands.lua @@ -659,6 +659,7 @@ if __TEST then M.__run = run M.__get_run_args = get_run_args M.__update_device_from_output = update_device_from_output + M.__set_current_device = set_current_device end return M diff --git a/lua/flutter-tools/decorations.lua b/lua/flutter-tools/decorations.lua index d536203..81003a8 100644 --- a/lua/flutter-tools/decorations.lua +++ b/lua/flutter-tools/decorations.lua @@ -34,7 +34,9 @@ end local function device_show() local device = commands.current_device() if device then return set_decoration_item("device", device) end - devices.get_default_device(function(default_device) + local project_root = vim.fs.root(0, "pubspec.yaml") + if not project_root then return set_decoration_item("device", nil) end + devices.get_default_device(project_root, function(default_device) if commands.current_device() then return end set_decoration_item("device", default_device) end) diff --git a/lua/flutter-tools/devices.lua b/lua/flutter-tools/devices.lua index 9d13a16..8712968 100644 --- a/lua/flutter-tools/devices.lua +++ b/lua/flutter-tools/devices.lua @@ -16,15 +16,13 @@ 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, +local PLATFORM_DIRECTORIES = { + { prefix = "android", directory = "android", ephemeral = true }, + { prefix = "ios", directory = "ios", ephemeral = true }, + { prefix = "darwin", directory = "macos", ephemeral = false }, + { prefix = "linux", directory = "linux", ephemeral = false }, + { prefix = "windows", directory = "windows", ephemeral = false }, + { prefix = "web", directory = "web", ephemeral = false }, } ---@param result string[] @@ -109,34 +107,67 @@ local function to_device(entry) } end +---@param target_platform string? +local function platform_info(target_platform) + if not target_platform then return end + for _, info in ipairs(PLATFORM_DIRECTORIES) do + if vim.startswith(target_platform, info.prefix) then return info end + end +end + +---@param entry table +---@param project_root string? +local function is_supported(entry, project_root) + if entry.isSupported == false then return false end + local info = platform_info(entry.targetPlatform) + if not project_root or not info then return true end + return vim.uv.fs_stat(vim.fs.joinpath(project_root, info.directory)) ~= nil +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. +---A device only counts as supported if the project has its platform directory. ---@param result string[] +---@param project_root string? ---@return Device? -function M.resolve_default_device(result) - local ok, decoded = pcall(vim.json.decode, table.concat(result, "\n")) +function M.resolve_default_device(result, project_root) + -- flutter can print notices such as the startup lock message before the JSON + local json_start + for index, line in ipairs(result) do + if vim.startswith(line, "[") then + json_start = index + break + end + end + if not json_start then return end + local json = table.concat(vim.list_slice(result, json_start), "\n") + local ok, decoded = pcall(vim.json.decode, json) if not ok or type(decoded) ~= "table" then return end - local supported = vim.tbl_filter(function(entry) return entry.isSupported ~= false end, decoded) + local supported = vim.tbl_filter( + function(entry) return is_supported(entry, project_root) 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 - ) + local ephemeral = vim.tbl_filter(function(entry) + local info = platform_info(entry.targetPlatform) + return not info or info.ephemeral + end, supported) if #ephemeral == 1 then return to_device(ephemeral[1]) end end ---Asynchronously get the device `flutter run` would default to +---@param project_root string ---@param callback fun(device: Device?) -function M.get_default_device(callback) +function M.get_default_device(project_root, 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())) + callback(M.resolve_default_device(j:result(), project_root)) end)) job:start() end) diff --git a/tests/commands_spec.lua b/tests/commands_spec.lua index 1bc2580..aca979d 100644 --- a/tests/commands_spec.lua +++ b/tests/commands_spec.lua @@ -96,9 +96,11 @@ describe("commands", function() end) it("should keep the device id when the name is parsed from the run output", function() + commands.__set_current_device({ id = "macos" }) commands.__update_device_from_output("Launching lib/main.dart on macOS in debug mode...") local device = commands.current_device() + assert.equal("macos", device.id) assert.equal("macOS", device.name) end) diff --git a/tests/devices_spec.lua b/tests/devices_spec.lua index 0193496..dd5c7de 100644 --- a/tests/devices_spec.lua +++ b/tests/devices_spec.lua @@ -100,6 +100,45 @@ INFO | Storing crashdata in: /tmp/android-ts/emu-crash-34.2.14.db, detection assert.is_nil(device) end) + it("should treat every desktop architecture as non-ephemeral", function() + local device = devices.resolve_default_device(machine_output({ + { + name = "Pixel 8", + id = "emulator-5554", + isSupported = true, + targetPlatform = "android-arm64", + }, + { name = "Linux", id = "linux", isSupported = true, targetPlatform = "linux-riscv64" }, + })) + + assert.equal("emulator-5554", device.id) + end) + + it("should skip devices whose platform directory is missing from the project", function() + local project_root = vim.fn.tempname() + vim.fn.mkdir(vim.fs.joinpath(project_root, "macos"), "p") + + 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" }, + }), + project_root + ) + vim.fn.delete(project_root, "rf") + + assert.equal("macos", device.id) + end) + + it("should skip notices printed before the JSON output", function() + local output = machine_output({ + { name = "macOS", id = "macos", isSupported = true, targetPlatform = "darwin" }, + }) + table.insert(output, 1, "Waiting for another flutter command to release the startup lock...") + + assert.equal("macos", devices.resolve_default_device(output).id) + 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" }))