diff --git a/README.md b/README.md index b3fe665d..8b19bbb1 100644 --- a/README.md +++ b/README.md @@ -293,6 +293,45 @@ You cannot/should not edit the files in the sdk directly so diagnostic analysis To ignore packages installed with pub, consider adding `vim.fn.expand("$HOME/AppData/Local/Pub/Cache")` to `analysisExcludedFolders` if you are using PowerShell. +#### Project Configuration + +It is possible to configure how each project is run using neovim's `exrc` functionality (see `:help exrc`). +This allows you to create an exrc file e.g. `.nvim.lua` and put the project configurations inside it. +This is similar _conceptually_ to vscode's `launch.json` file. + +```lua +-- .nvim.lua +-- If you have more than one setup configured you will be prompted when you run +-- your app to select which one you want to use +require('flutter-tools').setup_project({ + { + name = 'Development', -- an arbitrary name that you provide so you can recognise this config + flavor = 'DevFlavor', -- your flavour + device = 'pixel6pro', -- the device ID, which you can get by running `flutter devices` + dart_defines = { + API_URL = 'https://dev.example.com/api', + IS_DEV = true, + } + }, + { + name = 'Web', + device = 'chrome', + flavor = 'WebApp' + } +}) +``` + +you can also specify the configuration as an object if there is only one + +```lua +require('flutter-tools').setup_project({ + name = 'Development', + flavor = 'DevFlavor', + device = 'pixel6pro', + dart_defines = { ... } +}) +``` + #### Flutter binary In order to run flutter commands you _might_ need to pass either a _path_ or a _command_ to the plugin so it can find your @@ -308,9 +347,10 @@ was added, you can set your `flutter_path` to `"/snap/flu which is where this is usually installed by `snap`. ### Highlights + Highlight groups that are user configurable to change the appearance of certain UI elements. -* `FlutterToolsOutlineIndentGuides` - indent guides for the outline window +- `FlutterToolsOutlineIndentGuides` - indent guides for the outline window #### Widget guides diff --git a/lua/flutter-tools.lua b/lua/flutter-tools.lua index 91fe9c13..f6e2d9fe 100644 --- a/lua/flutter-tools.lua +++ b/lua/flutter-tools.lua @@ -19,6 +19,9 @@ local command = function(name, callback, opts) api.nvim_create_user_command(name, callback, opts or {}) end +---@param opts flutter.ProjectConfig +function M.setup_project(opts) config.setup_project(opts) end + local function setup_commands() -- Commands command("FlutterRun", function(data) commands.run_command(data.args) end, { nargs = "*" }) diff --git a/lua/flutter-tools/commands.lua b/lua/flutter-tools/commands.lua index 0cfa9e23..77f139c4 100644 --- a/lua/flutter-tools/commands.lua +++ b/lua/flutter-tools/commands.lua @@ -13,19 +13,21 @@ local dev_log = lazy.require("flutter-tools.log") ---@module "flutter-tools.log" local M = {} +---@alias RunOpts {cli_args: string[]?, args: string[]?, device: Device?} + ---@type table? local current_device = nil ----@class FlutterRunner ----@field is_running fun(runner: FlutterRunner):boolean ----@field run fun(runner: FlutterRunner, paths:table, args:table, cwd:string, on_run_data:fun(is_err:boolean, data:string), on_run_exit:fun(data:string[], args: table)) ----@field cleanup fun(funner: FlutterRunner) ----@field send fun(runner: FlutterRunner, cmd:string, quiet: boolean?) +---@class flutter.Runner +---@field is_running fun(runner: flutter.Runner):boolean +---@field run fun(runner: flutter.Runner, paths:table, args:table, cwd:string, on_run_data:fun(is_err:boolean, data:string), on_run_exit:fun(data:string[], args: table)) +---@field cleanup fun(funner: flutter.Runner) +---@field send fun(runner: flutter.Runner, cmd:string, quiet: boolean?) ----@type FlutterRunner? +---@type flutter.Runner? local runner = nil -function M.use_debugger_runner() +local function use_debugger_runner() local dap_ok, dap = pcall(require, "dap") if not config.debugger.run_via_dap then return false end if dap_ok then return true end @@ -98,31 +100,64 @@ function M.run_command(args) M.run({ args = args }) end ----Run the flutter application ----@param opts table -function M.run(opts) - if M.is_running() then return ui.notify("Flutter is already running!") end - opts = opts or {} - local device = opts.device - local cmd_args = opts.args - local cli_args = opts.cli_args - executable.get(function(paths) - local args = cli_args or {} - if not cli_args then - if not M.use_debugger_runner() then vim.list_extend(args, { "run" }) end - if not cmd_args and device and device.id then vim.list_extend(args, { "-d", device.id }) end - - if cmd_args then vim.list_extend(args, cmd_args) end +---@param callback fun(project_config: flutter.ProjectConfig?) +local function select_project_config(callback) + local project_config = config.project --[=[@as flutter.ProjectConfig[]]=] + if #project_config <= 1 then return callback(project_config[1]) end + vim.ui.select(project_config, { + prompt = "Select a project configuration", + format_item = function(item) + if item.name then return item.name end + return vim.inspect(item) + end, + }, function(selected) + if selected then callback(selected) end + end) +end - local dev_url = dev_tools.get_url() - if dev_url then vim.list_extend(args, { "--devtools-server-address", dev_url }) end +---@param opts RunOpts +---@param conf flutter.ProjectConfig? +---@return string[] +local function get_run_args(opts, conf) + local args = {} + local cmd_args = opts.args + local device = conf and conf.device or (opts.device and opts.device.id) + local flavor = conf and conf.flavor + local dart_defines = conf and conf.dart_define + local dev_url = dev_tools.get_url() + + if not use_debugger_runner() then vim.list_extend(args, { "run" }) end + if not cmd_args and device then vim.list_extend(args, { "-d", device }) end + if cmd_args then vim.list_extend(args, cmd_args) end + if flavor then vim.list_extend(args, { "--flavor", flavor }) end + if dart_defines then + for key, value in pairs(dart_defines) do + vim.list_extend(args, { "--dart-define", ("%s=%s"):format(key, value) }) end + end + if dev_url then vim.list_extend(args, { "--devtools-server-address", dev_url }) end + return args +end + +---@param opts RunOpts +---@param project_conf flutter.ProjectConfig? +local function run(opts, project_conf) + opts = opts or {} + executable.get(function(paths) + local args = opts.cli_args or get_run_args(opts, project_conf) ui.notify("Starting flutter project...") - runner = M.use_debugger_runner() and debugger_runner or job_runner + runner = use_debugger_runner() and debugger_runner or job_runner runner:run(paths, args, lsp.get_lsp_root_dir(), on_run_data, on_run_exit) end) end +---Run the flutter application +---@param opts RunOpts +function M.run(opts) + if M.is_running() then return ui.notify("Flutter is already running!") end + select_project_config(function(project_conf) run(opts, project_conf) end) +end + ---@param cmd string ---@param quiet boolean? ---@param on_send function|nil @@ -331,4 +366,9 @@ function M.fvm_use(sdk_name) end end +if __TEST then + M.__run = run + M.__get_run_args = get_run_args +end + return M diff --git a/lua/flutter-tools/config.lua b/lua/flutter-tools/config.lua index 10fb6913..1158a45c 100644 --- a/lua/flutter-tools/config.lua +++ b/lua/flutter-tools/config.lua @@ -2,8 +2,17 @@ local lazy = require("flutter-tools.lazy") local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path" local ui = lazy.require("flutter-tools.ui") ---@module "flutter-tools.ui" +---@class flutter.ProjectConfig +---@field name string? +---@field device string +---@field flavor string +---@field dart_define {[string]: string} + local M = {} +---@type flutter.ProjectConfig[] +local project_config = {} + local fn = vim.fn local fmt = string.format @@ -138,11 +147,10 @@ local function handle_deprecation(key, value, conf) if deprecation.fallback then conf[deprecation.fallback] = value end end ----Get the configuration or just a key of the config ----@param key string? -function M.get(key) - if key then return config[key] end - return config +---@param project flutter.ProjectConfig | flutter.ProjectConfig[] +M.setup_project = function(project) + if not vim.tbl_islist(project) then project = { project } end + project_config = project end function M.set(user_config) @@ -155,6 +163,10 @@ function M.set(user_config) return config end +---@module "flutter-tools.config" return setmetatable(M, { - __index = function(_, k) return M.get(k) end, + __index = function(_, k) + if k == "project" then return project_config end + return config[k] + end, }) diff --git a/lua/flutter-tools/runners/debugger_runner.lua b/lua/flutter-tools/runners/debugger_runner.lua index 99c15750..f99423d7 100644 --- a/lua/flutter-tools/runners/debugger_runner.lua +++ b/lua/flutter-tools/runners/debugger_runner.lua @@ -7,7 +7,7 @@ local api = vim.api local fmt = string.format ----@type FlutterRunner +---@type flutter.Runner local DebuggerRunner = {} local service_extensions_isolateid = {} diff --git a/lua/flutter-tools/runners/job_runner.lua b/lua/flutter-tools/runners/job_runner.lua index c70b29d5..7f1df17c 100644 --- a/lua/flutter-tools/runners/job_runner.lua +++ b/lua/flutter-tools/runners/job_runner.lua @@ -3,7 +3,7 @@ local ui = require("flutter-tools.ui") local dev_tools = require("flutter-tools.dev_tools") local api = vim.api ----@type FlutterRunner +---@type flutter.Runner local JobRunner = {} ---@type Job diff --git a/lua/flutter-tools/utils/init.lua b/lua/flutter-tools/utils/init.lua index 2dec9ac1..25917004 100644 --- a/lua/flutter-tools/utils/init.lua +++ b/lua/flutter-tools/utils/init.lua @@ -29,9 +29,14 @@ function M.highlight(name, opts) api.nvim_create_autocmd("ColorScheme", { callback = hl, group = colorscheme_group }) end -function M.fold(accumulator, callback, list) - for _, v in ipairs(list) do - accumulator = callback(accumulator, v) +---@generic T, S +---@param accumulator S +---@param callback fun(accumulator: S, item: T, index: number|string): S +---@param list T[] +---@return S +function M.fold(callback, list, accumulator) + for k, v in ipairs(list) do + accumulator = callback(accumulator, v, k) end return accumulator end diff --git a/tests/commands_spec.lua b/tests/commands_spec.lua new file mode 100644 index 00000000..a41491d4 --- /dev/null +++ b/tests/commands_spec.lua @@ -0,0 +1,49 @@ +local utils = require("flutter-tools.utils") + +describe("commands", function() + local commands + before_each(function() commands = require("flutter-tools.commands") end) + after_each(function() + commands = nil + package.loaded["flutter-tools.commands"] = nil + end) + it( + "should add project config options correctly", + function() + assert.are.same( + { "run", "--flavor", "Production" }, + commands.__get_run_args({}, { flavor = "Production" }) + ) + end + ) + + it( + "should add 'dart_defines' options correctly", + function() + assert.are.same( + { "run", "--flavor", "Production", "--dart-define", "ENV=prod" }, + commands.__get_run_args({}, { flavor = "Production", dart_define = { ENV = "prod" } }) + ) + end + ) + + it("should add multiple dart_defines", function() + local args = commands.__get_run_args({}, { + flavor = "Production", + dart_define = { ENV = "prod", KEY = "VALUE" }, + }) + local result = utils.fold(function(acc, v) + acc[v] = acc[v] and acc[v] + 1 or 1 + return acc + end, args, {}) + + assert.are.same(result, { + ["run"] = 1, + ["--flavor"] = 1, + ["Production"] = 1, + ["--dart-define"] = 2, + ["ENV=prod"] = 1, + ["KEY=VALUE"] = 1, + }) + end) +end)