Skip to content

Commit 178e42b

Browse files
authored
Fix IntelliJ split-debug tab labels regression (#8908)
## Summary - restore device-qualified Run/Debug tab titles on the newer IntelliJ split-debugger path - keep the existing descriptor mutation as a fallback for the older path - add a regression test that fails loudly if the reflective builder hooks disappear again Fixes #8907. ## Problem #8796 fixed the original `RunContentDescriptor.setDisplayName()` regression, but Blasko reported that plain IntelliJ still lost device labels while Android Studio worked: - #8795 (comment) The later split-debugger helper from #8878 moved newer IntelliJ builds onto `XDebuggerManager.newSessionBuilder()`, but that path never seeded the builder with the device-qualified session title. On split-debugger builds the visible tab title is derived from the builder configuration rather than the later descriptor mutation alone, so IntelliJ could regress while Android Studio stayed fine. ## Fix Pass the device-qualified title through `LaunchState` and `AttachState` into `FlutterDebugSessionUtils`, then configure the builder with: - `sessionName(...)` - `showTab(true)` - `contentToReuse(...)` before `startSession()` runs. The existing `RunContentDescriptor.setDisplayName()` reflection stays in place as a fallback for the older path. ## Tests - `./gradlew compileJava --console=plain` - `./gradlew test --tests io.flutter.run.LaunchStateTest --tests io.flutter.run.FlutterDebugSessionUtilsTest --console=plain` Manually tested and did indeed verify, the labels are back once again with this fix.
1 parent dc90f5d commit 178e42b

8 files changed

Lines changed: 336 additions & 45 deletions

File tree

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,4 @@ Dmitry Kandalov <dmitry.kandalov@gmail.com>
2929
Kazuya Chikamatsu <kazu.chika.shima@gmail.com>
3030
Dustin Feucht <code.nopjar@gmail.com>
3131
Nico Mexis <nicomexis.nm@gmail.com>
32+
Luke Memet <lukememet@gmail.com>

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
### Removed
1010

1111
### Fixed
12+
- Restored device labels on split-debugger run tabs in IntelliJ IDEA 2025.3+. (#8908)
1213

1314
## 92.0.0
1415

src/io/flutter/run/AttachState.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
4747
// Cache for use in console configuration, and for updating registered extensionRPCs.
4848
FlutterApp.addToEnvironment(env, app);
4949
final ExecutionResult result = setUpConsoleAndActions(app);
50-
return createDebugSession(env, app, result);
50+
final String nameWithDeviceName = device.withRunConfigurationName(env.getRunProfile().getName());
51+
return createDebugSession(env, app, result, nameWithDeviceName);
5152
}
5253
}

src/io/flutter/run/FlutterDebugSessionUtils.java

Lines changed: 154 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
import com.intellij.xdebugger.XDebugSession;
1313
import com.intellij.xdebugger.XDebuggerManager;
1414
import org.jetbrains.annotations.NotNull;
15+
import org.jetbrains.annotations.Nullable;
16+
import org.jetbrains.annotations.VisibleForTesting;
1517

1618
import java.lang.reflect.InvocationTargetException;
1719
import java.lang.reflect.Method;
@@ -21,56 +23,41 @@
2123
// See https://github.com/flutter/flutter-intellij/issues/8879.
2224
public class FlutterDebugSessionUtils {
2325

24-
private static final Method newSessionBuilderMethod;
25-
private static final Method environmentMethod;
26-
private static final Method startSessionMethod;
26+
private static final @Nullable BuilderHooks builderHooks = findBuilderHooks();
2727

28-
static {
29-
Method nsb = null;
30-
Method env = null;
31-
Method ss = null;
32-
try {
33-
nsb = XDebuggerManager.class.getMethod("newSessionBuilder", XDebugProcessStarter.class);
34-
Class<?> builderClass = nsb.getReturnType();
35-
env = builderClass.getMethod("environment", ExecutionEnvironment.class);
36-
ss = builderClass.getMethod("startSession");
37-
} catch (NoSuchMethodException e) {
38-
// Fallback for older platforms
39-
}
40-
newSessionBuilderMethod = nsb;
41-
environmentMethod = env;
42-
startSessionMethod = ss;
28+
public static @NotNull RunContentDescriptor startSessionAndGetDescriptor(
29+
@NotNull XDebuggerManager manager,
30+
@NotNull ExecutionEnvironment env,
31+
@NotNull XDebugProcessStarter starter,
32+
boolean muteBreakpoints) throws ExecutionException {
33+
return startSessionAndGetDescriptor(manager, env, starter, env.getRunProfile().getName(), muteBreakpoints);
4334
}
4435

4536
public static @NotNull RunContentDescriptor startSessionAndGetDescriptor(
4637
@NotNull XDebuggerManager manager,
4738
@NotNull ExecutionEnvironment env,
4839
@NotNull XDebugProcessStarter starter,
40+
@NotNull String sessionName,
4941
boolean muteBreakpoints) throws ExecutionException {
50-
try {
51-
if (newSessionBuilderMethod == null) {
52-
throw new NoSuchMethodException("newSessionBuilder is not available");
53-
}
54-
Object builder = newSessionBuilderMethod.invoke(manager, starter);
55-
builder = environmentMethod.invoke(builder, env);
56-
Object sessionResult = startSessionMethod.invoke(builder);
57-
58-
if (muteBreakpoints) {
59-
Method getSessionMethod = sessionResult.getClass().getMethod("getSession");
60-
XDebugSession session = (XDebugSession) getSessionMethod.invoke(sessionResult);
61-
session.setBreakpointMuted(true);
62-
}
42+
if (builderHooks == null) {
43+
return startLegacySessionAndGetDescriptor(manager, env, starter, muteBreakpoints);
44+
}
6345

64-
Method getDescriptorMethod = sessionResult.getClass().getMethod("getRunContentDescriptor");
65-
return (RunContentDescriptor) getDescriptorMethod.invoke(sessionResult);
46+
return startSessionAndGetDescriptor(builderHooks, manager, env, starter, sessionName, muteBreakpoints);
47+
}
6648

67-
} catch (NoSuchMethodException e) {
68-
// Fallback to old API for 2025.1 and older
69-
XDebugSession session = manager.startSession(env, starter);
70-
if (muteBreakpoints) {
71-
session.setBreakpointMuted(true);
72-
}
73-
return session.getRunContentDescriptor();
49+
@VisibleForTesting
50+
static @NotNull RunContentDescriptor startSessionAndGetDescriptor(
51+
@NotNull BuilderHooks hooks,
52+
@NotNull Object manager,
53+
@NotNull ExecutionEnvironment env,
54+
@NotNull XDebugProcessStarter starter,
55+
@NotNull String sessionName,
56+
boolean muteBreakpoints) throws ExecutionException {
57+
try {
58+
final Object sessionResult = startBuilderSession(hooks, manager, env, starter, sessionName);
59+
muteBreakpointsIfNeeded(sessionResult, muteBreakpoints);
60+
return getDescriptor(sessionResult);
7461
} catch (InvocationTargetException e) {
7562
Throwable cause = e.getCause();
7663
if (cause instanceof ExecutionException) {
@@ -81,4 +68,131 @@ public class FlutterDebugSessionUtils {
8168
throw new ExecutionException("Failed with unexpected reflection error", e);
8269
}
8370
}
71+
72+
private static @Nullable BuilderHooks findBuilderHooks() {
73+
Method sn = null;
74+
Method ctr = null;
75+
Method st = null;
76+
try {
77+
final Method nsb = XDebuggerManager.class.getMethod("newSessionBuilder", XDebugProcessStarter.class);
78+
final Class<?> builderClass = nsb.getReturnType();
79+
final Method env = builderClass.getMethod("environment", ExecutionEnvironment.class);
80+
final Method ss = builderClass.getMethod("startSession");
81+
try {
82+
sn = builderClass.getMethod("sessionName", String.class);
83+
ctr = builderClass.getMethod("contentToReuse", RunContentDescriptor.class);
84+
st = builderClass.getMethod("showTab", boolean.class);
85+
} catch (NoSuchMethodException e) {
86+
// Some newer SDKs may expose the builder without all of the tab configuration hooks.
87+
}
88+
return new BuilderHooks(nsb, env, sn, ctr, st, ss);
89+
} catch (NoSuchMethodException e) {
90+
// Fallback for older platforms
91+
return null;
92+
}
93+
}
94+
95+
private static @NotNull RunContentDescriptor startLegacySessionAndGetDescriptor(
96+
@NotNull XDebuggerManager manager,
97+
@NotNull ExecutionEnvironment env,
98+
@NotNull XDebugProcessStarter starter,
99+
boolean muteBreakpoints) throws ExecutionException {
100+
final XDebugSession session = manager.startSession(env, starter);
101+
if (muteBreakpoints) {
102+
session.setBreakpointMuted(true);
103+
}
104+
return session.getRunContentDescriptor();
105+
}
106+
107+
private static @NotNull Object startBuilderSession(
108+
@NotNull BuilderHooks hooks,
109+
@NotNull Object manager,
110+
@NotNull ExecutionEnvironment env,
111+
@NotNull XDebugProcessStarter starter,
112+
@NotNull String sessionName) throws Exception {
113+
Object builder = hooks.newSessionBuilderMethod.invoke(manager, starter);
114+
builder = hooks.environmentMethod.invoke(builder, env);
115+
builder = configureNamedTab(hooks, builder, env, sessionName);
116+
return hooks.startSessionMethod.invoke(builder);
117+
}
118+
119+
private static @NotNull Object configureNamedTab(
120+
@NotNull BuilderHooks hooks,
121+
@NotNull Object builder,
122+
@NotNull ExecutionEnvironment env,
123+
@NotNull String sessionName) throws Exception {
124+
// Split debugger builds derive the visible tab title from the builder configuration rather than later descriptor mutation.
125+
if (hooks.contentToReuseMethod != null) {
126+
builder = hooks.contentToReuseMethod.invoke(builder, env.getContentToReuse());
127+
}
128+
if (hooks.sessionNameMethod != null) {
129+
builder = hooks.sessionNameMethod.invoke(builder, sessionName);
130+
}
131+
if (hooks.showTabMethod != null) {
132+
builder = hooks.showTabMethod.invoke(builder, true);
133+
}
134+
return builder;
135+
}
136+
137+
private static void muteBreakpointsIfNeeded(@NotNull Object sessionResult, boolean muteBreakpoints) throws Exception {
138+
if (!muteBreakpoints) {
139+
return;
140+
}
141+
final Method getSessionMethod = sessionResult.getClass().getMethod("getSession");
142+
final XDebugSession session = (XDebugSession) getSessionMethod.invoke(sessionResult);
143+
session.setBreakpointMuted(true);
144+
}
145+
146+
private static @NotNull RunContentDescriptor getDescriptor(@NotNull Object sessionResult) throws Exception {
147+
final Method getDescriptorMethod = sessionResult.getClass().getMethod("getRunContentDescriptor");
148+
return (RunContentDescriptor) getDescriptorMethod.invoke(sessionResult);
149+
}
150+
151+
/**
152+
* Returns null when the current SDK exposes the reflective builder hooks needed to label split debug tabs.
153+
*/
154+
@VisibleForTesting
155+
static @Nullable String getNamedTabSupportError() {
156+
if (builderHooks == null) {
157+
return null;
158+
}
159+
if (builderHooks.environmentMethod == null) {
160+
return "environment not found on XDebugSessionBuilder";
161+
}
162+
if (builderHooks.startSessionMethod == null) {
163+
return "startSession not found on XDebugSessionBuilder";
164+
}
165+
if (builderHooks.sessionNameMethod == null) {
166+
return "sessionName not found on XDebugSessionBuilder";
167+
}
168+
if (builderHooks.showTabMethod == null) {
169+
return "showTab not found on XDebugSessionBuilder";
170+
}
171+
return null;
172+
}
173+
174+
@VisibleForTesting
175+
static final class BuilderHooks {
176+
final Method newSessionBuilderMethod;
177+
final Method environmentMethod;
178+
final @Nullable Method sessionNameMethod;
179+
final @Nullable Method contentToReuseMethod;
180+
final @Nullable Method showTabMethod;
181+
final Method startSessionMethod;
182+
183+
BuilderHooks(
184+
@NotNull Method newSessionBuilderMethod,
185+
@NotNull Method environmentMethod,
186+
@Nullable Method sessionNameMethod,
187+
@Nullable Method contentToReuseMethod,
188+
@Nullable Method showTabMethod,
189+
@NotNull Method startSessionMethod) {
190+
this.newSessionBuilderMethod = newSessionBuilderMethod;
191+
this.environmentMethod = environmentMethod;
192+
this.sessionNameMethod = sessionNameMethod;
193+
this.contentToReuseMethod = contentToReuseMethod;
194+
this.showTabMethod = showTabMethod;
195+
this.startSessionMethod = startSessionMethod;
196+
}
197+
}
84198
}

src/io/flutter/run/FlutterDevice.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public String deviceName() {
5252
return myDeviceName;
5353
}
5454

55+
@NotNull
56+
public String withRunConfigurationName(@NotNull String runConfigurationName) {
57+
return runConfigurationName + " (" + deviceName() + ")";
58+
}
59+
5560
@Nullable
5661
public String platform() {
5762
return myPlatform;

src/io/flutter/run/LaunchState.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,12 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
156156
}
157157
}
158158

159+
final String nameWithDeviceName = device.withRunConfigurationName(env.getRunProfile().getName());
159160
final FlutterLaunchMode launchMode = FlutterLaunchMode.fromEnv(env);
160161
final RunContentDescriptor descriptor;
161162
if (launchMode.supportsDebugConnection()) {
162163
ToolWindowBadgeUpdater.updateBadgedIcon(app, project);
163-
descriptor = createDebugSession(env, app, result);
164+
descriptor = createDebugSession(env, app, result, nameWithDeviceName);
164165
}
165166
else {
166167
descriptor = new RunContentBuilder(result, env).showRunContent(env.getContentToReuse());
@@ -169,7 +170,6 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
169170
// Add the device name for the run descriptor.
170171
// The descriptor shows the run configuration name (e.g., `main.dart`) by default;
171172
// adding the device name will help users identify the instance when trying to operate a specific one.
172-
final String nameWithDeviceName = descriptor.getDisplayName() + " (" + device.deviceName() + ")";
173173
final BiConsumer<RunContentDescriptor, String> setter = getDisplaySetter();
174174
if (setter != null) {
175175
setter.accept(descriptor, nameWithDeviceName);
@@ -226,7 +226,8 @@ protected void showNoDeviceConnectedMessage(Project project) {
226226
@NotNull
227227
protected RunContentDescriptor createDebugSession(@NotNull final ExecutionEnvironment env,
228228
@NotNull final FlutterApp app,
229-
@NotNull final ExecutionResult executionResult)
229+
@NotNull final ExecutionResult executionResult,
230+
@NotNull final String sessionName)
230231
throws ExecutionException {
231232

232233
final DartUrlResolver resolver = DartUrlResolver.getInstance(env.getProject(), sourceLocation);
@@ -239,7 +240,7 @@ protected RunContentDescriptor createDebugSession(@NotNull final ExecutionEnviro
239240
public XDebugProcess start(@NotNull final XDebugSession session) {
240241
return new FlutterDebugProcess(app, env, session, executionResult, resolver, mapper);
241242
}
242-
}, app.getMode() != RunMode.DEBUG);
243+
}, sessionName, app.getMode() != RunMode.DEBUG);
243244
}
244245

245246
@NotNull

0 commit comments

Comments
 (0)