Skip to content

Commit 631b05e

Browse files
committed
Show notifications for widget errors that can launch the inspector
Fixes #3088.
1 parent c62be7f commit 631b05e

11 files changed

Lines changed: 98 additions & 15 deletions

File tree

‎package.json‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,6 +1523,12 @@
15231523
"type": "string"
15241524
}
15251525
},
1526+
"dart.showInspectorNotificationsForWidgetErrors": {
1527+
"type": "boolean",
1528+
"default": true,
1529+
"markdownDescription": "Whether to show notifications for widget errors that offer Inspect Widget links. This requires that the `dart.shareDevToolsWithFlutter` setting is also enabled.",
1530+
"scope": "window"
1531+
},
15261532
"dart.hotReloadProgress": {
15271533
"enum": [
15281534
"notification",

‎src/debug/dart_debug_impl.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export class DartDebugSession extends DebugSession {
7373
public debugExternalLibraries = false;
7474
public showDartDeveloperLogs = true;
7575
public useFlutterStructuredErrors = false;
76+
public useInspectorNotificationsForWidgetErrors = false;
7677
public evaluateGettersInDebugViews = false;
7778
protected evaluateToStringInDebugViews = false;
7879
protected readonly dartCapabilities = DartCapabilities.empty;
@@ -275,7 +276,8 @@ export class DartDebugSession extends DebugSession {
275276
this.sendLogsToClient = !!args.sendLogsToClient;
276277
this.showDartDeveloperLogs = args.showDartDeveloperLogs;
277278
this.toolEnv = args.toolEnv;
278-
this.useFlutterStructuredErrors = args.useFlutterStructuredErrors;
279+
this.useFlutterStructuredErrors = !!args.useFlutterStructuredErrors;
280+
this.useInspectorNotificationsForWidgetErrors = !!args.useInspectorNotificationsForWidgetErrors;
279281
}
280282

281283
protected async attachRequest(response: DebugProtocol.AttachResponse, args: DartAttachRequestArguments): Promise<void> {

‎src/debug/flutter_debug_impl.ts‎

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { FlutterAttachRequestArguments, FlutterLaunchRequestArguments } from "..
66
import { LogCategory, VmService, VmServiceExtension } from "../shared/enums";
77
import { AppProgress } from "../shared/flutter/daemon_interfaces";
88
import { DiagnosticsNode, DiagnosticsNodeLevel, DiagnosticsNodeStyle, DiagnosticsNodeType, FlutterErrorData } from "../shared/flutter/structured_errors";
9-
import { Logger } from "../shared/interfaces";
9+
import { Logger, WidgetErrorInspectData } from "../shared/interfaces";
1010
import { grey, grey2 } from "../shared/utils/colors";
1111
import { getSdkVersion } from "../shared/utils/fs";
1212
import { DartDebugSession } from "./dart_debug_impl";
@@ -408,6 +408,9 @@ export class FlutterDebugSession extends DartDebugSession {
408408
private handleFlutterErrorEvent(event: VMEvent) {
409409
const error = event.extensionData as FlutterErrorData;
410410
this.logFlutterErrorToUser(error);
411+
412+
if (this.useInspectorNotificationsForWidgetErrors)
413+
this.tryParseDevToolsInspectLink(error);
411414
}
412415

413416
private logFlutterErrorToUser(error: FlutterErrorData) {
@@ -437,8 +440,12 @@ export class FlutterDebugSession extends DartDebugSession {
437440
if (node.showSeparator !== false && node.description)
438441
line += ": ";
439442
}
440-
if (node.description)
441-
line += node.description;
443+
if (node.description) {
444+
if (this.useInspectorNotificationsForWidgetErrors && node.type === DiagnosticsNodeType.DevToolsDeepLinkProperty)
445+
line += "You can inspect this widget using the 'Inspect Widget' button in the VS Code notification.";
446+
else
447+
line += node.description;
448+
}
442449
line = line.trimRight();
443450

444451
// For text that is not part of a stack trace and is not an Error or Summary we
@@ -489,6 +496,29 @@ export class FlutterDebugSession extends DartDebugSession {
489496
node.children.forEach((child) => this.logDiagnosticNodeToUser(child, { parent: node, level }));
490497
}
491498

499+
static flutterErrorDevToolsUrlPattern = new RegExp("(https?://[^/]+/)[^ ]+&inspectorRef=([^ &\\n]+)");
500+
private tryParseDevToolsInspectLink(error: FlutterErrorData) {
501+
try {
502+
const errorSummaryNode = error.properties?.find((p) => p.type === DiagnosticsNodeType.ErrorSummary);
503+
const devToolsLinkNode = error.properties?.find((p) => p.type === DiagnosticsNodeType.DevToolsDeepLinkProperty);
504+
505+
// "A RenderFlex overflowed by 5551 pixels on the right."
506+
const errorDescription = errorSummaryNode?.description;
507+
508+
// "http://127.0.0.1:9100/#/inspector?uri=http%3A%2F%2F127.0.0.1%3A49905%2FC-UKCEA9hEQ%3D%2F&inspectorRef=inspector-0"
509+
const devToolsInspectWidgetUrl = devToolsLinkNode?.value;
510+
const devToolsInspectWidgetUrlMatch = devToolsInspectWidgetUrl ? FlutterDebugSession.flutterErrorDevToolsUrlPattern.exec(devToolsInspectWidgetUrl) : undefined;
511+
const devToolsUrl = devToolsInspectWidgetUrlMatch?.length ? devToolsInspectWidgetUrlMatch[1] : undefined;
512+
const inspectorReference = devToolsInspectWidgetUrlMatch?.length ? devToolsInspectWidgetUrlMatch[2] : undefined;
513+
514+
if (errorDescription && devToolsUrl && inspectorReference) {
515+
this.sendEvent(new Event("dart.flutter.widgetErrorInspectData", { errorDescription, devToolsUrl, inspectorReference } as WidgetErrorInspectData));
516+
}
517+
} catch (e) {
518+
this.logger.error(`Error trying to parse widget inspect data from structured error`);
519+
}
520+
}
521+
492522
// Extension
493523
public async handleExtensionEvent(event: VMEvent) {
494524
// Don't process any events while the debugger is still running init code.

‎src/extension/commands/debug.ts‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as fs from "fs";
22
import * as path from "path";
33
import * as vs from "vscode";
4-
import { devToolsPages, doNotAskAgainAction, isInFlutterDebugModeDebugSessionContext, isInFlutterProfileModeDebugSessionContext } from "../../shared/constants";
4+
import { devToolsPages, doNotAskAgainAction, isInFlutterDebugModeDebugSessionContext, isInFlutterProfileModeDebugSessionContext, widgetInspectorPage } from "../../shared/constants";
55
import { DebuggerType, DebugOption, debugOptionNames, LogSeverity, VmServiceExtension } from "../../shared/enums";
6-
import { DartWorkspaceContext, DevToolsPage, Logger, LogMessage } from "../../shared/interfaces";
6+
import { DartWorkspaceContext, DevToolsPage, Logger, LogMessage, WidgetErrorInspectData } from "../../shared/interfaces";
77
import { flatMap, PromiseCompleter } from "../../shared/utils";
88
import { findProjectFolders, fsPath } from "../../shared/utils/fs";
99
import { showDevToolsNotificationIfAppropriate } from "../../shared/vscode/user_prompts";
@@ -54,6 +54,7 @@ export class DebugCommands {
5454
public readonly onDebugSessionVmServiceAvailable = this.onDebugSessionVmServiceAvailableEmitter.event;
5555
public readonly vmServices: VmServiceExtensions;
5656
public readonly devTools: DevToolsManager;
57+
private suppressFlutterWidgetErrors = false;
5758

5859
constructor(private readonly logger: Logger, private context: Context, workspaceContext: DartWorkspaceContext, private readonly analytics: Analytics, pubGlobal: PubGlobal) {
5960
this.vmServices = new VmServiceExtensions(logger, this.sendServiceSetting);
@@ -121,11 +122,10 @@ export class DebugCommands {
121122

122123
// Only show a notification if we were not triggered automatically.
123124
const notify = !options || options.triggeredAutomatically !== true;
124-
const reuseWindows = config.devToolsReuseWindows;
125125
const page = options?.page;
126126

127127
if (session.vmServiceUri) {
128-
return this.devTools.spawnForSession(session as DartDebugSessionInformation & { vmServiceUri: string }, { embed: config.embedDevTools, reuseWindows, notify, page });
128+
return this.devTools.spawnForSession(session as DartDebugSessionInformation & { vmServiceUri: string }, { notify, page });
129129
} else if (session.session.configuration.noDebug) {
130130
vs.window.showInformationMessage("You must start your app with debugging in order to use DevTools.");
131131
} else {
@@ -549,6 +549,34 @@ export class DebugCommands {
549549
await new Promise((resolve) => setTimeout(resolve, 400));
550550
}
551551
session.progress[e.body.progressID]?.complete();
552+
} else if (e.event === "dart.flutter.widgetErrorInspectData") {
553+
if (this.suppressFlutterWidgetErrors || !config.showInspectorNotificationsForWidgetErrors)
554+
return;
555+
556+
const data = e.body as WidgetErrorInspectData;
557+
if (data.devToolsUrl !== (await this.devTools.devtoolsUrl))
558+
return;
559+
560+
// To avoid spam, when we show this dialog we will set a flag that prevents any more
561+
// of these types of dialogs until it is dismissed or 5 seconds have passed.
562+
this.suppressFlutterWidgetErrors = true;
563+
const timer = setTimeout(() => this.suppressFlutterWidgetErrors = false, 5000);
564+
565+
const inspectAction = `Inspect Widget`;
566+
const choice = await vs.window.showWarningMessage(data.errorDescription, inspectAction, doNotAskAgainAction);
567+
if (choice === inspectAction && session.vmServiceUri) {
568+
this.devTools.spawnForSession(
569+
session as DartDebugSessionInformation & { vmServiceUri: string },
570+
{
571+
inspectorRef: data.inspectorReference,
572+
page: widgetInspectorPage,
573+
},
574+
);
575+
} else if (choice === doNotAskAgainAction) {
576+
config.setShowInspectorNotificationsForWidgetErrors(false);
577+
}
578+
clearTimeout(timer);
579+
this.suppressFlutterWidgetErrors = false;
552580
}
553581
}
554582

‎src/extension/config.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ class Config {
104104
get shareDevToolsWithFlutter(): boolean { return this.getConfig<boolean>("shareDevToolsWithFlutter", true); }
105105
get showDartPadSampleCodeLens(): boolean { return this.getConfig<boolean>("showDartPadSampleCodeLens", true); }
106106
get showDevToolsDebugToolBarButtons(): boolean { return this.getConfig<boolean>("showDevToolsDebugToolBarButtons", true); }
107+
get showInspectorNotificationsForWidgetErrors(): boolean { return this.getConfig<boolean>("showInspectorNotificationsForWidgetErrors", true); }
107108
get showIgnoreQuickFixes(): boolean { return this.getConfig<boolean>("showIgnoreQuickFixes", true); }
108109
get showMainCodeLens(): boolean { return this.getConfig<boolean>("showMainCodeLens", true); }
109110
get showSkippedTests(): boolean { return this.getConfig<boolean>("showSkippedTests", true); }
@@ -134,6 +135,7 @@ class Config {
134135
public setGlobalFlutterSdkPath(value: string): Thenable<void> { return this.setConfig("flutterSdkPath", value, ConfigurationTarget.Global); }
135136
public setPreviewLsp(value: boolean): Thenable<void> { return this.setConfig("previewLsp", value, ConfigurationTarget.Global); }
136137
public setOpenDevTools(value: "never" | "flutter" | "always" | undefined): Thenable<void> { return this.setConfig("openDevTools", value, ConfigurationTarget.Global); }
138+
public setShowInspectorNotificationsForWidgetErrors(value: boolean): Thenable<void> { return this.setConfig("showInspectorNotificationsForWidgetErrors", value, ConfigurationTarget.Global); }
137139
public setShowSkippedTests(value: boolean): Thenable<void> { return this.setConfig("showSkippedTests", value, ConfigurationTarget.Global); }
138140
public setSdkPath(value: string | undefined): Thenable<void> { return this.setConfig("sdkPath", value, ConfigurationTarget.Workspace); }
139141
public setWarnWhenEditingFilesOutsideWorkspace(value: boolean): Thenable<void> { return this.setConfig("warnWhenEditingFilesOutsideWorkspace", value, ConfigurationTarget.Global); }

‎src/extension/providers/debug_config_provider.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,7 @@ export class DebugConfigProvider implements DebugConfigurationProvider {
463463
debugConfig.flutterSdkPath = this.wsContext.sdks.flutter;
464464
debugConfig.globalFlutterArgs = getGlobalFlutterArgs();
465465
debugConfig.useFlutterStructuredErrors = conf.flutterStructuredErrors;
466+
debugConfig.useInspectorNotificationsForWidgetErrors = config.showInspectorNotificationsForWidgetErrors;
466467
debugConfig.debugExtensionBackendProtocol = config.debugExtensionBackendProtocol;
467468

468469
const additionalArgs = isTest

‎src/extension/sdk/dev_tools/manager.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,10 @@ export class DevToolsManager implements vs.Disposable {
154154
if (!url)
155155
return;
156156

157-
// If the launched versin of DevTools doesn't support embedding, remove the flag.
158-
if (!this.capabilities.supportsEmbedFlag)
159-
options.embed = false;
157+
if (options.embed === undefined)
158+
options.embed = this.capabilities.supportsEmbedFlag && config.embedDevTools;
159+
if (options.reuseWindows === undefined)
160+
options.reuseWindows = config.devToolsReuseWindows;
160161

161162
// When we're running embedded and were asked to open without a page, we should prompt for a page (plus give an option
162163
// to open non-embedded view).
@@ -246,6 +247,7 @@ export class DevToolsManager implements vs.Disposable {
246247
const queryParams: { [key: string]: string | undefined } = {
247248
hide: "debugger",
248249
ide: "VSCode",
250+
inspectorRef: options.inspectorRef,
249251
theme: config.useDevToolsDarkTheme && !options.embed ? "dark" : undefined,
250252
};
251253

@@ -260,7 +262,7 @@ export class DevToolsManager implements vs.Disposable {
260262
queryParams.embed = "true";
261263
const fullUrl = await this.buildDevToolsUrl(queryParams, session, url);
262264
if (options.embed)
263-
// TODO: What should really we do it we don't have a page?
265+
// TODO: What should we do if we don't have a page?
264266
this.launchInEmbeddedWebView(fullUrl, session, options.page ?? devToolsPages[0]);
265267
else
266268
await envUtils.openInBrowser(fullUrl.toString(), this.logger);
@@ -476,8 +478,9 @@ export interface ServerStartedNotification {
476478
}
477479

478480
interface DevToolsOptions {
479-
embed: boolean;
481+
embed?: boolean;
480482
reuseWindows?: boolean;
481483
notify?: boolean;
482484
page?: DevToolsPage;
485+
inspectorRef?: string;
483486
}

‎src/shared/constants.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ export const runFlutterCreateDotPrompt = (platformType: string) => `You must run
136136
export const runFlutterCreateDotAction = "Run 'flutter create .'";
137137
export const cancelAction = "Cancel";
138138

139+
export const widgetInspectorPage: DevToolsPage = { pageId: "inspector", commandId: "dart.openDevToolsInspector", title: "Widget Inspector" };
139140
export const devToolsPages: DevToolsPage[] = [
140141
// First entry is the default page.
141-
{ pageId: "inspector", commandId: "dart.openDevToolsInspector", title: "Widget Inspector" },
142+
widgetInspectorPage,
142143
{ pageId: "cpu-profiler", commandId: "dart.openDevToolsCpuProfiler", legacyPageId: "performance", title: "CPU Profiler" },
143144
{ pageId: "memory", commandId: "dart.openDevToolsMemory", title: "Memory" },
144145
{ pageId: "performance", commandId: "dart.openDevToolsPerformance", legacyPageId: "timeline", title: "Performance" },

‎src/shared/debug/interfaces.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ export interface DartSharedArgs {
1111
sendLogsToClient?: boolean;
1212
showDartDeveloperLogs: boolean;
1313
toolEnv?: { [key: string]: string | undefined };
14-
useFlutterStructuredErrors: boolean;
14+
useFlutterStructuredErrors?: boolean;
15+
useInspectorNotificationsForWidgetErrors?: boolean;
1516
debugExtensionBackendProtocol: "sse" | "ws";
1617
}
1718

‎src/shared/flutter/structured_errors.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface DiagnosticsNode {
1212
properties: DiagnosticsNode[];
1313
children: DiagnosticsNode[];
1414
style: DiagnosticsNodeStyle;
15+
value?: string;
1516
}
1617

1718
// If we have per-type properties, handle them like this.
@@ -21,8 +22,10 @@ export interface DiagnosticsNode {
2122

2223
export enum DiagnosticsNodeType {
2324
ErrorDescription = "ErrorDescription",
25+
ErrorSummary = "ErrorSummary",
2426
ErrorSpacer = "ErrorSpacer",
2527
DiagnosticsStackTrace = "DiagnosticsStackTrace",
28+
DevToolsDeepLinkProperty = "DevToolsDeepLinkProperty",
2629
}
2730

2831
export enum DiagnosticsNodeLevel {

0 commit comments

Comments
 (0)