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
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ Dmitry Kandalov <dmitry.kandalov@gmail.com>
Kazuya Chikamatsu <kazu.chika.shima@gmail.com>
Dustin Feucht <code.nopjar@gmail.com>
Nico Mexis <nicomexis.nm@gmail.com>
Luke Memet <lukememet@gmail.com>
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### Removed

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

## 92.0.0

Expand Down
3 changes: 2 additions & 1 deletion src/io/flutter/run/AttachState.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
// Cache for use in console configuration, and for updating registered extensionRPCs.
FlutterApp.addToEnvironment(env, app);
final ExecutionResult result = setUpConsoleAndActions(app);
return createDebugSession(env, app, result);
final String nameWithDeviceName = device.withRunConfigurationName(env.getRunProfile().getName());
return createDebugSession(env, app, result, nameWithDeviceName);
}
}
194 changes: 154 additions & 40 deletions src/io/flutter/run/FlutterDebugSessionUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.intellij.xdebugger.XDebugSession;
import com.intellij.xdebugger.XDebuggerManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

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

private static final Method newSessionBuilderMethod;
private static final Method environmentMethod;
private static final Method startSessionMethod;
private static final @Nullable BuilderHooks builderHooks = findBuilderHooks();

static {
Method nsb = null;
Method env = null;
Method ss = null;
try {
nsb = XDebuggerManager.class.getMethod("newSessionBuilder", XDebugProcessStarter.class);
Class<?> builderClass = nsb.getReturnType();
env = builderClass.getMethod("environment", ExecutionEnvironment.class);
ss = builderClass.getMethod("startSession");
} catch (NoSuchMethodException e) {
// Fallback for older platforms
}
newSessionBuilderMethod = nsb;
environmentMethod = env;
startSessionMethod = ss;
public static @NotNull RunContentDescriptor startSessionAndGetDescriptor(
@NotNull XDebuggerManager manager,
@NotNull ExecutionEnvironment env,
@NotNull XDebugProcessStarter starter,
boolean muteBreakpoints) throws ExecutionException {
return startSessionAndGetDescriptor(manager, env, starter, env.getRunProfile().getName(), muteBreakpoints);
}

public static @NotNull RunContentDescriptor startSessionAndGetDescriptor(
@NotNull XDebuggerManager manager,
@NotNull ExecutionEnvironment env,
@NotNull XDebugProcessStarter starter,
@NotNull String sessionName,
boolean muteBreakpoints) throws ExecutionException {
try {
if (newSessionBuilderMethod == null) {
throw new NoSuchMethodException("newSessionBuilder is not available");
}
Object builder = newSessionBuilderMethod.invoke(manager, starter);
builder = environmentMethod.invoke(builder, env);
Object sessionResult = startSessionMethod.invoke(builder);

if (muteBreakpoints) {
Method getSessionMethod = sessionResult.getClass().getMethod("getSession");
XDebugSession session = (XDebugSession) getSessionMethod.invoke(sessionResult);
session.setBreakpointMuted(true);
}
if (builderHooks == null) {
return startLegacySessionAndGetDescriptor(manager, env, starter, muteBreakpoints);
}

Method getDescriptorMethod = sessionResult.getClass().getMethod("getRunContentDescriptor");
return (RunContentDescriptor) getDescriptorMethod.invoke(sessionResult);
return startSessionAndGetDescriptor(builderHooks, manager, env, starter, sessionName, muteBreakpoints);
}

} catch (NoSuchMethodException e) {
// Fallback to old API for 2025.1 and older
XDebugSession session = manager.startSession(env, starter);
if (muteBreakpoints) {
session.setBreakpointMuted(true);
}
return session.getRunContentDescriptor();
@VisibleForTesting
static @NotNull RunContentDescriptor startSessionAndGetDescriptor(
@NotNull BuilderHooks hooks,
@NotNull Object manager,
@NotNull ExecutionEnvironment env,
@NotNull XDebugProcessStarter starter,
@NotNull String sessionName,
boolean muteBreakpoints) throws ExecutionException {
try {
final Object sessionResult = startBuilderSession(hooks, manager, env, starter, sessionName);
muteBreakpointsIfNeeded(sessionResult, muteBreakpoints);
return getDescriptor(sessionResult);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof ExecutionException) {
Expand All @@ -81,4 +68,131 @@ public class FlutterDebugSessionUtils {
throw new ExecutionException("Failed with unexpected reflection error", e);
}
}

private static @Nullable BuilderHooks findBuilderHooks() {
Method sn = null;
Method ctr = null;
Method st = null;
try {
final Method nsb = XDebuggerManager.class.getMethod("newSessionBuilder", XDebugProcessStarter.class);
final Class<?> builderClass = nsb.getReturnType();
final Method env = builderClass.getMethod("environment", ExecutionEnvironment.class);
final Method ss = builderClass.getMethod("startSession");
try {
sn = builderClass.getMethod("sessionName", String.class);
ctr = builderClass.getMethod("contentToReuse", RunContentDescriptor.class);
st = builderClass.getMethod("showTab", boolean.class);
} catch (NoSuchMethodException e) {
// Some newer SDKs may expose the builder without all of the tab configuration hooks.
}
return new BuilderHooks(nsb, env, sn, ctr, st, ss);
} catch (NoSuchMethodException e) {
// Fallback for older platforms
return null;
}
}

private static @NotNull RunContentDescriptor startLegacySessionAndGetDescriptor(
@NotNull XDebuggerManager manager,
@NotNull ExecutionEnvironment env,
@NotNull XDebugProcessStarter starter,
boolean muteBreakpoints) throws ExecutionException {
final XDebugSession session = manager.startSession(env, starter);
if (muteBreakpoints) {
session.setBreakpointMuted(true);
}
return session.getRunContentDescriptor();
}

private static @NotNull Object startBuilderSession(
@NotNull BuilderHooks hooks,
@NotNull Object manager,
@NotNull ExecutionEnvironment env,
@NotNull XDebugProcessStarter starter,
@NotNull String sessionName) throws Exception {
Object builder = hooks.newSessionBuilderMethod.invoke(manager, starter);
builder = hooks.environmentMethod.invoke(builder, env);
builder = configureNamedTab(hooks, builder, env, sessionName);
return hooks.startSessionMethod.invoke(builder);
}

private static @NotNull Object configureNamedTab(
@NotNull BuilderHooks hooks,
@NotNull Object builder,
@NotNull ExecutionEnvironment env,
@NotNull String sessionName) throws Exception {
// Split debugger builds derive the visible tab title from the builder configuration rather than later descriptor mutation.
if (hooks.contentToReuseMethod != null) {
builder = hooks.contentToReuseMethod.invoke(builder, env.getContentToReuse());
}
if (hooks.sessionNameMethod != null) {
builder = hooks.sessionNameMethod.invoke(builder, sessionName);
}
if (hooks.showTabMethod != null) {
builder = hooks.showTabMethod.invoke(builder, true);
}
return builder;
}

private static void muteBreakpointsIfNeeded(@NotNull Object sessionResult, boolean muteBreakpoints) throws Exception {
if (!muteBreakpoints) {
return;
}
final Method getSessionMethod = sessionResult.getClass().getMethod("getSession");
final XDebugSession session = (XDebugSession) getSessionMethod.invoke(sessionResult);
session.setBreakpointMuted(true);
}

private static @NotNull RunContentDescriptor getDescriptor(@NotNull Object sessionResult) throws Exception {
final Method getDescriptorMethod = sessionResult.getClass().getMethod("getRunContentDescriptor");
return (RunContentDescriptor) getDescriptorMethod.invoke(sessionResult);
}

/**
* Returns null when the current SDK exposes the reflective builder hooks needed to label split debug tabs.
*/
@VisibleForTesting
static @Nullable String getNamedTabSupportError() {
if (builderHooks == null) {
return null;
}
if (builderHooks.environmentMethod == null) {
return "environment not found on XDebugSessionBuilder";
}
if (builderHooks.startSessionMethod == null) {
return "startSession not found on XDebugSessionBuilder";
}
if (builderHooks.sessionNameMethod == null) {
return "sessionName not found on XDebugSessionBuilder";
}
if (builderHooks.showTabMethod == null) {
return "showTab not found on XDebugSessionBuilder";
}
return null;
}

@VisibleForTesting
static final class BuilderHooks {
final Method newSessionBuilderMethod;
final Method environmentMethod;
final @Nullable Method sessionNameMethod;
final @Nullable Method contentToReuseMethod;
final @Nullable Method showTabMethod;
final Method startSessionMethod;

BuilderHooks(
@NotNull Method newSessionBuilderMethod,
@NotNull Method environmentMethod,
@Nullable Method sessionNameMethod,
@Nullable Method contentToReuseMethod,
@Nullable Method showTabMethod,
@NotNull Method startSessionMethod) {
this.newSessionBuilderMethod = newSessionBuilderMethod;
this.environmentMethod = environmentMethod;
this.sessionNameMethod = sessionNameMethod;
this.contentToReuseMethod = contentToReuseMethod;
this.showTabMethod = showTabMethod;
this.startSessionMethod = startSessionMethod;
}
}
}
5 changes: 5 additions & 0 deletions src/io/flutter/run/FlutterDevice.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ public String deviceName() {
return myDeviceName;
}

@NotNull
public String withRunConfigurationName(@NotNull String runConfigurationName) {
return runConfigurationName + " (" + deviceName() + ")";
}

@Nullable
public String platform() {
return myPlatform;
Expand Down
9 changes: 5 additions & 4 deletions src/io/flutter/run/LaunchState.java
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,12 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
}
}

final String nameWithDeviceName = device.withRunConfigurationName(env.getRunProfile().getName());
final FlutterLaunchMode launchMode = FlutterLaunchMode.fromEnv(env);
final RunContentDescriptor descriptor;
if (launchMode.supportsDebugConnection()) {
ToolWindowBadgeUpdater.updateBadgedIcon(app, project);
descriptor = createDebugSession(env, app, result);
descriptor = createDebugSession(env, app, result, nameWithDeviceName);
}
else {
descriptor = new RunContentBuilder(result, env).showRunContent(env.getContentToReuse());
Expand All @@ -169,7 +170,6 @@ protected RunContentDescriptor launch(@NotNull ExecutionEnvironment env) throws
// Add the device name for the run descriptor.
// The descriptor shows the run configuration name (e.g., `main.dart`) by default;
// adding the device name will help users identify the instance when trying to operate a specific one.
final String nameWithDeviceName = descriptor.getDisplayName() + " (" + device.deviceName() + ")";
final BiConsumer<RunContentDescriptor, String> setter = getDisplaySetter();
if (setter != null) {
setter.accept(descriptor, nameWithDeviceName);
Expand Down Expand Up @@ -226,7 +226,8 @@ protected void showNoDeviceConnectedMessage(Project project) {
@NotNull
protected RunContentDescriptor createDebugSession(@NotNull final ExecutionEnvironment env,
@NotNull final FlutterApp app,
@NotNull final ExecutionResult executionResult)
@NotNull final ExecutionResult executionResult,
@NotNull final String sessionName)
throws ExecutionException {

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

@NotNull
Expand Down
Loading
Loading