дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(debugger): debug Flutter apps through the Flutter debug adapter (#1474)
Dart projects that depend on the Flutter SDK were debugged through `dart debug_adapter`, which cannot start a program importing dart:ui and has no hot reload. A session now uses `flutter debug_adapter` when the program's or the working directory's pubspec.yaml depends on the Flutter SDK, or when an attach configuration asks for it (add-to-app: vm-service-uri and device map to the adapter's attach arguments). Hot Reload and Hot Restart are sent over DAP and appear in the toolbar of Dart sessions. Dart files also get a debug configuration. Co-authored-by: payailk <[email protected]>
Цей коміт міститься в:
1 джерело
8869f1ce1c
коміт
566190e94b
16 files changed
+187
-2
No files matched your search
@@ -724,6 +724,8 @@ export const commands = {
|
||||
debugCompletions: (session: DebugSessionId, frameId: number | null, text: string, line: number, column: number) => typedError<DebugCompletion[], IdeError>(__TAURI_INVOKE("debug_completions", { session, frameId, text, line, column })),
|
||||
debugResume: (session: DebugSessionId, threadId: number) => typedError<null, IdeError>(__TAURI_INVOKE("debug_resume", { session, threadId })),
|
||||
debugPause: (session: DebugSessionId, threadId: number) => typedError<null, IdeError>(__TAURI_INVOKE("debug_pause", { session, threadId })),
|
||||
debugHotReload: (session: DebugSessionId) => typedError<null, IdeError>(__TAURI_INVOKE("debug_hot_reload", { session })),
|
||||
debugHotRestart: (session: DebugSessionId) => typedError<null, IdeError>(__TAURI_INVOKE("debug_hot_restart", { session })),
|
||||
debugStep: (session: DebugSessionId, threadId: number, kind: StepKind) => typedError<null, IdeError>(__TAURI_INVOKE("debug_step", { session, threadId, kind })),
|
||||
debugRunTo: (session: DebugSessionId, threadId: number, path: string, line: number, force: boolean) => typedError<null, IdeError>(__TAURI_INVOKE("debug_run_to", { session, threadId, path, line, force })),
|
||||
debugStop: (session: DebugSessionId) => typedError<null, IdeError>(__TAURI_INVOKE("debug_stop", { session })),
|
||||
|
||||
@@ -52,6 +52,17 @@ const session: DebugSession = {
|
||||
};
|
||||
|
||||
describe("debug tool window", () => {
|
||||
it("offers Hot Reload and Hot Restart for Dart and Flutter sessions only", () => {
|
||||
const hot = () => ["debug.hotReload", "debug.hotRestart"].map((name) => !!screen.queryByRole("button", { name }));
|
||||
useDebug.setState({ sessions: { 1: { ...session, status: "running" } }, active: 1 });
|
||||
const { unmount } = render(<DebugToolWindow />);
|
||||
expect(hot()).toEqual([false, false]);
|
||||
unmount();
|
||||
useDebug.setState({ sessions: { 1: { ...session, adapter: "dart", status: "running" } }, active: 1 });
|
||||
render(<DebugToolWindow />);
|
||||
expect(hot()).toEqual([true, true]);
|
||||
});
|
||||
|
||||
it("shows preparation output, preserves a chosen tab and opens variables on pause", () => {
|
||||
useDebug.setState({ sessions: { 1: { ...session, status: "starting" } }, active: 1 });
|
||||
render(<DebugToolWindow />);
|
||||
|
||||
@@ -436,6 +436,30 @@ export function registerDebugActions(): () => void {
|
||||
await report(tx("Pause failed"), call(commands.debugPause(s.id, thread)));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "debug.hotReload",
|
||||
title: "Hot Reload",
|
||||
category: "Debug",
|
||||
icon: "hot-reload",
|
||||
description: "Flutter: inject the changed sources into the running app",
|
||||
when: "debugSessionRunning && debugSessionDart",
|
||||
run: () => {
|
||||
const s = activeSession();
|
||||
if (s) void report("Hot Reload failed", call(commands.debugHotReload(s.id)));
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "debug.hotRestart",
|
||||
title: "Hot Restart",
|
||||
category: "Debug",
|
||||
icon: "refresh",
|
||||
description: "Flutter: restart the app, discarding its state",
|
||||
when: "debugSessionRunning && debugSessionDart",
|
||||
run: () => {
|
||||
const s = activeSession();
|
||||
if (s) void report("Hot Restart failed", call(commands.debugHotRestart(s.id)));
|
||||
},
|
||||
},
|
||||
step("debug.stepOver", "Step Over", "over", "step-over"),
|
||||
step("debug.stepInto", "Step Into", "into", "step-into"),
|
||||
step("debug.stepOut", "Step Out", "out", "step-out"),
|
||||
|
||||
@@ -313,6 +313,49 @@ function nativeLaunch(rc: RunConfig, cwd: string, adapter: string): LaunchConfig
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flutter: `flutter debug_adapter` starts the tool itself, so `program` is the
|
||||
* entry point (`lib/main.dart`) — never the interpreter named by `command`.
|
||||
*/
|
||||
function flutterLaunch(rc: RunConfig, cwd: string, adapter: string): LaunchConfig {
|
||||
// A file-scoped config carries the entry point as its last `.dart` argument
|
||||
// (`dart run lib/main.dart`); a user config may name none, which is the
|
||||
// conventional Flutter entry point.
|
||||
const entry = [...rc.args].reverse().find((a) => a.endsWith(".dart")) ?? "lib/main.dart";
|
||||
return {
|
||||
...base(rc, adapter, cwd),
|
||||
program: join(cwd, entry),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Flutter add-to-app: the host app is built and installed by its own Gradle
|
||||
* build, so the debugger attaches to the already running app instead of
|
||||
* launching `lib/main.dart` (which would build the module on its own).
|
||||
*
|
||||
* `params.vm-service-uri` pins the VM Service endpoint (`flutter attach
|
||||
* --debug-uri`); without it `flutter attach` discovers the running app on the
|
||||
* device. `params.device` becomes `-d <device>`.
|
||||
*/
|
||||
function flutterAttach(rc: RunConfig, cwd: string, adapter: string): LaunchConfig {
|
||||
const p = rc.params ?? {};
|
||||
const uri = p["vm-service-uri"] || p.vmServiceUri;
|
||||
const device = p.device;
|
||||
// `__flutter` tells the adapter launcher to use `flutter debug_adapter`
|
||||
// whatever the working directory holds (an add-to-app module lives below a
|
||||
// workspace root that has no Flutter dependency of its own). It is removed
|
||||
// before the request reaches the adapter.
|
||||
const extra: Record<string, unknown> = { __flutter: true };
|
||||
if (uri) extra.vmServiceUri = uri;
|
||||
if (device) extra.toolArgs = ["-d", device];
|
||||
return {
|
||||
...base(rc, adapter, cwd),
|
||||
request: "attach",
|
||||
program: null,
|
||||
extraJson: JSON.stringify(extra),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The launch configuration that debugs `rc`, or `null` when it has no adapter.
|
||||
* `root` is the project root (default working directory).
|
||||
@@ -321,6 +364,9 @@ export function launchConfigFor(rc: RunConfig, root: string): LaunchConfig | nul
|
||||
const adapter = rc.debugAdapter;
|
||||
if (!adapter || debugUnsupportedReason(rc)) return null;
|
||||
const cwd = rc.cwd ? join(root, rc.cwd) : root;
|
||||
if (adapter === "dart") {
|
||||
return rc.params?.request === "attach" ? flutterAttach(rc, cwd, adapter) : flutterLaunch(rc, cwd, adapter);
|
||||
}
|
||||
if (adapter === "codelldb" || adapter === "lldb-dap") {
|
||||
return rc.kind === "cargo" ? cargoLaunch(rc, cwd, adapter) : nativeLaunch(rc, cwd, adapter);
|
||||
}
|
||||
|
||||
@@ -328,6 +328,8 @@ export function publishContext() {
|
||||
setContext("debugSessionActive", anyLive);
|
||||
setContext("debugSessionRunning", s?.status === "running");
|
||||
setContext("debugSessionPaused", s?.status === "paused");
|
||||
// Dart and Flutter sessions: Hot Reload / Hot Restart.
|
||||
setContext("debugSessionDart", !!s && rootOf(s).adapter === "dart");
|
||||
setContext("debugCanRerun", !!s && !!(rootOf(s).runConfig || rootOf(s).launch));
|
||||
setContext(
|
||||
"debugCanHotSwap",
|
||||
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
selectSession,
|
||||
selectThread,
|
||||
revealFrame,
|
||||
rootOf,
|
||||
sessionSupports,
|
||||
useDebug,
|
||||
type DebugSession,
|
||||
@@ -200,6 +201,13 @@ export function DebugToolWindow() {
|
||||
<ToolButton action="debug.stepInto" icon="step-into" />
|
||||
<ToolButton action="debug.stepOut" icon="step-out" />
|
||||
<span className={styles.sep} aria-hidden />
|
||||
{rootOf(active).adapter === "dart" && (
|
||||
<>
|
||||
<ToolButton action="debug.hotReload" icon="hot-reload" />
|
||||
<ToolButton action="debug.hotRestart" icon="refresh" />
|
||||
<span className={styles.sep} aria-hidden />
|
||||
</>
|
||||
)}
|
||||
<IconButton
|
||||
icon="view-breakpoints"
|
||||
action="debug.viewBreakpoints"
|
||||
|
||||
@@ -807,7 +807,7 @@ export function produceFile(input: ContextInput, env: ToolEnv): Produced | null
|
||||
case "r":
|
||||
return simple(lang, s, env, "r", () => [s.arg]);
|
||||
case "dart":
|
||||
return simple(lang, s, env, "dart", () => ["run", s.arg]);
|
||||
return simple(lang, s, env, "dart", () => ["run", s.arg], "dart");
|
||||
case "scala":
|
||||
return simple(lang, s, env, "scala-cli", () => ["run", s.arg]);
|
||||
case "kotlin":
|
||||
|
||||
@@ -162,6 +162,10 @@
|
||||
"Fixup…": "修补提交…",
|
||||
"Step Over": "单步跳过",
|
||||
"Step Out": "单步跳出",
|
||||
"Hot Reload": "热重载",
|
||||
"Hot Restart": "热重启",
|
||||
"Flutter: inject the changed sources into the running app": "Flutter:将改动后的源码注入正在运行的应用",
|
||||
"Flutter: restart the app, discarding its state": "Flutter:重启应用,丢弃当前状态",
|
||||
"Zoom In": "放大",
|
||||
"Zoom Out": "缩小",
|
||||
"Actual Size": "实际大小",
|
||||
|
||||
@@ -361,6 +361,7 @@ const ICONS = {
|
||||
"gutter-overriding": "gutter/overridingMethod",
|
||||
stop: "run/stop",
|
||||
rerun: "run/rerun",
|
||||
"hot-reload": "openidea/general/hotReload",
|
||||
"rerun-filled": "run/rerun_stroke",
|
||||
"restart-debug": "run/restartDebug",
|
||||
"restart-debug-filled": "run/restartDebug_stroke",
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<!-- OpenIDEA: hot reload (a lightning bolt) in the expui style. -->
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.5 1.5L3.5 9H7.5L6.5 14.5L12.5 7H8.5L9.5 1.5Z" stroke="#6C707E" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
Після Ширина: | Висота: | Розмір: 273 B |
Reference in new issue
Заблокувати користувача