Skip to content

Commit 12fdb56

Browse files
committed
Handle deep link notifications from new Flutter DAP
This is work towards #6134. It also requires a Flutter change (flutter/flutter#190455) and to track down why the DevTools server URL + VM Service URL in the Flutter code are `null` (which prevents the error not being included).
1 parent ed823c2 commit 12fdb56

2 files changed

Lines changed: 60 additions & 4 deletions

File tree

‎src/extension/commands/debug.ts‎

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { FlutterCapabilities } from "../../shared/capabilities/flutter";
55
import { CommandSource, debugLaunchProgressId, debugTerminatingProgressId, devToolsPages, doNotAskAgainAction, doNotShowAgainAction, moreInfoAction, widgetInspectorPage } from "../../shared/constants";
66
import { isInDartDebugSessionContext, isInFlutterDebugModeDebugSessionContext, isInFlutterProfileModeDebugSessionContext, isInFlutterReleaseModeDebugSessionContext } from "../../shared/constants.contexts";
77
import { DebugOption, DebuggerType, LogCategory, LogSeverity, VmService, VmServiceExtension, debugOptionNames } from "../../shared/enums";
8-
import { DartWorkspaceContext, IAmDisposable, LogMessage, Logger, WidgetErrorInspectData } from "../../shared/interfaces";
8+
import { DartWorkspaceContext, DevToolsDeepLinkData, IAmDisposable, LegacyWidgetErrorInspectData, LogMessage, Logger } from "../../shared/interfaces";
99
import { PromiseCompleter, disposeAll } from "../../shared/utils";
1010
import { fsPath, isFlutterProjectFolder, isWithinPath } from "../../shared/utils/fs";
1111
import { ANALYSIS_FILTERS } from "../../shared/vscode/constants";
@@ -964,11 +964,11 @@ export class DebugCommands implements IAmDisposable {
964964
}
965965
progress.complete();
966966
}
967-
} else if (event === "dart.flutter.widgetErrorInspectData") {
967+
} else if (event === "dart.flutter.widgetErrorInspectData") { // Legacy DAP deep links
968968
if (this.suppressFlutterWidgetErrors || !config.showInspectorNotificationsForWidgetErrors)
969969
return;
970970

971-
const data = e.body as WidgetErrorInspectData;
971+
const data = e.body as LegacyWidgetErrorInspectData;
972972
if (data.devToolsUrl !== (await this.devTools.devtoolsUrl))
973973
return;
974974

@@ -993,6 +993,41 @@ export class DebugCommands implements IAmDisposable {
993993
}
994994
clearTimeout(timer);
995995
this.suppressFlutterWidgetErrors = false;
996+
} else if (event === "dart.flutter.devToolsDeepLink") { // New DAP deep links
997+
if (this.suppressFlutterWidgetErrors || !config.showInspectorNotificationsForWidgetErrors)
998+
return;
999+
1000+
// "event":"dart.flutter.devToolsDeepLink",
1001+
// "body":{
1002+
// "summary":"A RenderFlex overflowed by 266 pixels on the right.",
1003+
// "deepLinkUrl":"http://example.org/#/inspector?uri&inspectorRef=inspector-0"
1004+
// }
1005+
const data = e.body as DevToolsDeepLinkData;
1006+
1007+
// To avoid spam, when we show this dialog we will set a flag that prevents any more
1008+
// of these types of dialogs until it is dismissed or 5 seconds have passed.
1009+
this.suppressFlutterWidgetErrors = true;
1010+
const timer = setTimeout(() => this.suppressFlutterWidgetErrors = false, 5000);
1011+
1012+
const inspectAction = `Inspect Widget`;
1013+
const choice = await vs.window.showWarningMessage(data.summary, inspectAction, doNotAskAgainAction);
1014+
if (choice === inspectAction && session.vmServiceUri) {
1015+
const link = this.parseDeepLinkUri(data.deepLinkUrl);
1016+
if (link !== null) {
1017+
void this.devTools.spawn(
1018+
session as DartDebugSessionInformation & { vmServiceUri: string },
1019+
{
1020+
inspectorRef: link?.inspectorRef,
1021+
pageId: link?.pageId,
1022+
},
1023+
true,
1024+
);
1025+
}
1026+
} else if (choice === doNotAskAgainAction) {
1027+
void config.setShowInspectorNotificationsForWidgetErrors(false);
1028+
}
1029+
clearTimeout(timer);
1030+
this.suppressFlutterWidgetErrors = false;
9961031
} else if (event === "flutter.appStarted") {
9971032
session.hasStarted = true;
9981033
debugSessionChangedEmitter.fire(session);
@@ -1010,6 +1045,22 @@ export class DebugCommands implements IAmDisposable {
10101045
}
10111046
}
10121047

1048+
private parseDeepLinkUri(deepLinkUrl: string): { pageId: string, inspectorRef: string | undefined } | undefined {
1049+
try {
1050+
// http://example.org/#/inspector?uri&inspectorRef=inspector-0
1051+
const url = new URL(deepLinkUrl.replace("#/", ""));
1052+
const pageId = url.pathname.split("/")[1];
1053+
const inspectorRef = url.searchParams.get("inspectorRef") ?? undefined;
1054+
if (pageId !== "") {
1055+
return { pageId, inspectorRef };
1056+
}
1057+
return undefined;
1058+
} catch {
1059+
this.logger.error(`Failed to parse DevTools deep link URL: ${deepLinkUrl}`);
1060+
return undefined;
1061+
}
1062+
}
1063+
10131064
private toggleDebugOptions() {
10141065
// -1 is because we skip the last combination when toggling since it seems uncommon.
10151066
this.currentDebugOption = (this.currentDebugOption + 1) % (debugOptionNames.length - 1);

‎src/shared/interfaces.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,17 @@ export interface DevToolsPage {
251251
isStaticTool?: boolean;
252252
}
253253

254-
export interface WidgetErrorInspectData {
254+
export interface LegacyWidgetErrorInspectData {
255255
errorDescription: string;
256256
devToolsUrl: string;
257257
inspectorReference: string;
258258
}
259259

260+
export interface DevToolsDeepLinkData {
261+
summary: string;
262+
deepLinkUrl: string;
263+
}
264+
260265
export interface Range {
261266
start: Position;
262267
end: Position;

0 commit comments

Comments
 (0)