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 @@ -500,7 +500,7 @@ return { 'mfussenegger/nvim-dap' }

This plugin integrates with [nvim-dap](https://github.com/mfussenegger/nvim-dap) to provide debug capabilities for Flutter and Dart applications.

The plugin will automatically set up `nvim-dap` for Flutter/Dart debugging.
The plugin will automatically set up `nvim-dap` for Flutter/Dart debugging. The `dart` adapter and launch configurations are registered when the plugin starts, so `require("dap").continue()` works without running `:FlutterRun` or `:FlutterDebug` first. An adapter or configurations you define yourself are kept.

Also see:

Expand Down
55 changes: 14 additions & 41 deletions lua/flutter-tools/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ local debugger_runner = lazy.require("flutter-tools.runners.debugger_runner") --
local path = lazy.require("flutter-tools.utils.path") ---@module "flutter-tools.utils.path"
local dev_log = lazy.require("flutter-tools.log") ---@module "flutter-tools.log"
local banner = lazy.require("flutter-tools.banner") ---@module "flutter-tools.banner"
local parser = lazy.require("flutter-tools.utils.yaml_parser")
local config_utils = lazy.require("flutter-tools.utils.config_utils") ---@module "flutter-tools.utils.config_utils"

local M = {}
Expand Down Expand Up @@ -150,6 +149,19 @@ local function on_run_exit(result, cli_args, opts, project_config, launch_config
end
end

---Route dart debug sessions started outside flutter-tools, e.g. via `dap.continue()`, through
---the debugger runner so logs and commands like reload work for them
function M.track_debug_sessions()
debugger_runner.on_untracked_session(function()
if runner and runner ~= debugger_runner and runner:is_running() then return end
runner = debugger_runner
return {
on_run_data = on_run_data,
on_run_exit = function(before_start_logs) on_run_exit(before_start_logs, {}) end,
}
end)
end

--- Take arguments from the commandline and pass
--- them to the run command
---@param args string
Expand Down Expand Up @@ -230,45 +242,6 @@ local function get_device_from_args(args)
end
end

--@return table?
local function parse_yaml(str)
local ok, yaml = pcall(parser.parse, str)
if not ok then return nil end
return yaml
end

---@param cwd string
local function has_flutter_dependency_in_pubspec(cwd)
-- As this plugin is tailored for flutter projects,
-- we assume that the project is a flutter project.
local default_has_flutter_dependency = true
local pubspec_path = vim.fn.glob(path.join(cwd, "pubspec.yaml"))
if pubspec_path == "" then return default_has_flutter_dependency end
local pubspec_content = vim.fn.readfile(pubspec_path)
local joined_content = table.concat(pubspec_content, "\n")
local pubspec = parse_yaml(joined_content)
if not pubspec then return default_has_flutter_dependency end
--https://github.com/Dart-Code/Dart-Code/blob/43914cd2709d77668e19a4edf3500f996d5c307b/src/shared/utils/fs.ts#L183
return (
pubspec.dependencies
and (
pubspec.dependencies.flutter
or pubspec.dependencies.flutter_test
or pubspec.dependencies.sky_engine
or pubspec.dependencies.flutter_goldens
)
)
or (
pubspec.devDependencies
and (
pubspec.devDependencies.flutter
or pubspec.devDependencies.flutter_test
or pubspec.devDependencies.sky_engine
or pubspec.devDependencies.flutter_goldens
)
)
end

---@param opts RunOpts
---@param project_conf flutter.ProjectConfig?
---@param launch_config dap.Configuration?
Expand All @@ -295,7 +268,7 @@ local function run(opts, project_conf, launch_config)
-- To determinate if the project is a flutter project we need to check if
-- the pubspec.yaml file has a flutter dependency in it. We need to get
-- cwd first to pick correct pubspec.yaml file.
local is_flutter_project = has_flutter_dependency_in_pubspec(cwd)
local is_flutter_project = config_utils.has_flutter_dependency_in_pubspec(cwd)

local default_run_args = config.default_run_args
local run_args
Expand Down
13 changes: 12 additions & 1 deletion lua/flutter-tools/dap.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,21 @@ function M.setup(config)
local success, dap = pcall(require, "dap")
if success then
local opts = config.debugger
require("flutter-tools.executable").get(function(_)
require("flutter-tools.commands").track_debug_sessions()
require("flutter-tools.executable").get(function(paths)
if opts.exception_breakpoints and type(opts.exception_breakpoints) == "table" then
dap.defaults.dart.exception_breakpoints = opts.exception_breakpoints
end
local config_utils = require("flutter-tools.utils.config_utils")
local projects = config.project
local project_config = #projects == 1 and projects[1] or nil
local cwd = config_utils.get_cwd(project_config) or vim.fn.getcwd()
require("flutter-tools.runners.debugger_runner").register_defaults(
paths,
config_utils.has_flutter_dependency_in_pubspec(cwd),
project_config,
cwd
)
end)
end
end
Expand Down
128 changes: 99 additions & 29 deletions lua/flutter-tools/runners/debugger_runner.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ end
---@param paths table<string, string>
---@param is_flutter_project boolean
---@param project_config flutter.ProjectConfig?
local function register_default_configurations(paths, is_flutter_project, project_config)
---@param cwd string?
local function register_default_configurations(paths, is_flutter_project, project_config, cwd)
local program
if is_flutter_project then
if project_config and project_config.target then
Expand All @@ -78,6 +79,7 @@ local function register_default_configurations(paths, is_flutter_project, projec
dartSdkPath = paths.dart_sdk,
flutterSdkPath = paths.flutter_sdk,
program = program,
cwd = cwd,
},
{
type = "dart",
Expand All @@ -86,6 +88,7 @@ local function register_default_configurations(paths, is_flutter_project, projec
dartSdkPath = paths.dart_sdk,
flutterSdkPath = paths.flutter_sdk,
program = program,
cwd = cwd,
},
}
else
Expand All @@ -102,6 +105,7 @@ local function register_default_configurations(paths, is_flutter_project, projec
name = "Launch dart",
dartSdkPath = paths.dart_sdk,
program = program,
cwd = cwd,
},
}
end
Expand Down Expand Up @@ -163,11 +167,34 @@ local function handle_inspect_event(isolate_id)
end)
end

local listened_events = {
{ "after", "event_output" },
{ "before", "event_exited" },
{ "before", "event_terminated" },
{ "before", "event_app.started" },
{ "before", "event_dart.debuggerUris" },
{ "before", "event_dart.serviceExtensionAdded" },
{ "before", "event_flutter.serviceExtensionStateChanged" },
}

local function unregister_dap_listeners()
for _, event in ipairs(listened_events) do
dap.listeners[event[1]][event[2]][plugin_identifier] = nil
end
end

---@param on_run_data fun(is_err: boolean, line: string)
---@param on_run_exit fun(before_start_logs: string[])
local function register_dap_listeners(on_run_data, on_run_exit)
vm_service_extensions.reset()
local started = false
local before_start_logs = {}
dap.listeners.after["event_output"][plugin_identifier] = function(_, body)
on_run_data(started, before_start_logs, body)
if not body or not body.output then return end
for line in body.output:gmatch("[^\r\n]+") do
if not started then table.insert(before_start_logs, line) end
on_run_data(body.category == "stderr", line)
end
end

local handle_termination = function()
Expand Down Expand Up @@ -219,6 +246,60 @@ local function register_dap_listeners(on_run_data, on_run_exit)
end
end

---@class flutter.DebuggerListeners
---@field on_run_data fun(is_err: boolean, line: string)
---@field on_run_exit fun(before_start_logs: string[])

---Listeners for the session the next `dap.run` call starts
---@type flutter.DebuggerListeners?
local pending_listeners = nil

---@type fun(): flutter.DebuggerListeners?
local get_untracked_session_listeners = function() end

local tracked_session_id = nil

---@param launch_config dap.Configuration
---@param listeners flutter.DebuggerListeners
local function start_session(launch_config, listeners)
pending_listeners = listeners
dap.run(launch_config)
end

dap.listeners.on_session[plugin_identifier] = function(_, session)
if not session or session.config.type ~= "dart" or session.id == tracked_session_id then
return
end
tracked_session_id = session.id
local listeners = pending_listeners or get_untracked_session_listeners()
pending_listeners = nil
if listeners then
register_dap_listeners(listeners.on_run_data, listeners.on_run_exit)
else
unregister_dap_listeners()
end
end

---Set the listeners for dart sessions started outside flutter-tools, e.g. via `dap.continue()`
---@param get_listeners fun(): flutter.DebuggerListeners?
function DebuggerRunner.on_untracked_session(get_listeners)
get_untracked_session_listeners = get_listeners
end

---Register the adapter and launch configurations up front so `dap.continue()` works before any
---flutter-tools command has run. Adapters and configurations the user defined are left in place.
---@param paths table<string, string>
---@param is_flutter_project boolean
---@param project_config flutter.ProjectConfig?
---@param cwd string?
function DebuggerRunner.register_defaults(paths, is_flutter_project, project_config, cwd)
if not dap.adapters.dart then register_debug_adapter(paths, is_flutter_project) end
if not dap.configurations.dart then
register_default_configurations(paths, is_flutter_project, project_config, cwd)
end
if config.debugger.register_configurations then config.debugger.register_configurations(paths) end
end

function DebuggerRunner:run(
opts,
paths,
Expand All @@ -230,32 +311,25 @@ function DebuggerRunner:run(
project_config,
last_launch_config
)
vm_service_extensions.reset()
---@type dap.Configuration
local selected_launch_config = nil

register_dap_listeners(
function(started, before_start_logs, body)
if body and body.output then
for line in body.output:gmatch("[^\r\n]+") do
if not started then table.insert(before_start_logs, line) end
on_run_data(body.category == "stderr", line)
end
end
end,
function(before_start_logs)
---@type flutter.DebuggerListeners
local listeners = {
on_run_data = on_run_data,
on_run_exit = function(before_start_logs)
on_run_exit(before_start_logs, args, opts, project_config, selected_launch_config)
end
)
end,
}

register_debug_adapter(paths, is_flutter_project)
local launch_configurations = {}
local launch_configuration_count = 0
if last_launch_config then
dap.run(last_launch_config)
start_session(last_launch_config, listeners)
return
else
register_default_configurations(paths, is_flutter_project, project_config)
register_default_configurations(paths, is_flutter_project, project_config, cwd)
if config.debugger.register_configurations then
config.debugger.register_configurations(paths)
end
Expand Down Expand Up @@ -293,27 +367,23 @@ function DebuggerRunner:run(
launch_config.evaluateToStringInDebugViews = true
end
selected_launch_config = launch_config
dap.run(launch_config)
start_session(launch_config, listeners)
end
)
end
end

function DebuggerRunner:attach(paths, args, cwd, on_run_data, on_run_exit)
vm_service_extensions.reset()
register_dap_listeners(function(started, before_start_logs, body)
if body and body.output then
for line in body.output:gmatch("[^\r\n]+") do
if not started then table.insert(before_start_logs, line) end
on_run_data(body.category == "stderr", line)
end
end
end, function(before_start_logs) on_run_exit(before_start_logs, args) end)
---@type flutter.DebuggerListeners
local listeners = {
on_run_data = on_run_data,
on_run_exit = function(before_start_logs) on_run_exit(before_start_logs, args) end,
}

register_debug_adapter(paths, true)
local launch_configurations = {}
local launch_configuration_count = 0
register_default_configurations(paths, true)
register_default_configurations(paths, true, nil, cwd)
if config.debugger.register_configurations then config.debugger.register_configurations(paths) end
local all_configurations = require("dap").configurations.dart
if not all_configurations then
Expand Down Expand Up @@ -347,7 +417,7 @@ function DebuggerRunner:attach(paths, args, cwd, on_run_data, on_run_exit)
if config.debugger.evaluate_to_string_in_debug_views then
launch_config.evaluateToStringInDebugViews = true
end
dap.run(launch_config)
start_session(launch_config, listeners)
end
)
end
Expand Down
40 changes: 40 additions & 0 deletions lua/flutter-tools/utils/config_utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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"
local lsp = lazy.require("flutter-tools.lsp") ---@module "flutter-tools.utils"
local parser = lazy.require("flutter-tools.utils.yaml_parser")

--- Gets the appropriate cwd
---@param project_conf flutter.ProjectConfig?
Expand All @@ -19,4 +20,43 @@ function M.get_cwd(project_conf)
return lsp.get_project_root_dir()
end

--@return table?
local function parse_yaml(str)
local ok, yaml = pcall(parser.parse, str)
if not ok then return nil end
return yaml
end

---@param cwd string
function M.has_flutter_dependency_in_pubspec(cwd)
-- As this plugin is tailored for flutter projects,
-- we assume that the project is a flutter project.
local default_has_flutter_dependency = true
local pubspec_path = vim.fn.glob(path.join(cwd, "pubspec.yaml"))
if pubspec_path == "" then return default_has_flutter_dependency end
local pubspec_content = vim.fn.readfile(pubspec_path)
local joined_content = table.concat(pubspec_content, "\n")
local pubspec = parse_yaml(joined_content)
if not pubspec then return default_has_flutter_dependency end
--https://github.com/Dart-Code/Dart-Code/blob/43914cd2709d77668e19a4edf3500f996d5c307b/src/shared/utils/fs.ts#L183
return (
pubspec.dependencies
and (
pubspec.dependencies.flutter
or pubspec.dependencies.flutter_test
or pubspec.dependencies.sky_engine
or pubspec.dependencies.flutter_goldens
)
)
or (
pubspec.devDependencies
and (
pubspec.devDependencies.flutter
or pubspec.devDependencies.flutter_test
or pubspec.devDependencies.sky_engine
or pubspec.devDependencies.flutter_goldens
)
)
end

return M
Loading
Loading