Skip to content

Commit 4141851

Browse files
authored
Fix long string overlay in debugger Variables panel (#7112) (#9745)
this PR fixes issue #7112 In the debugger Variables panel, when a variable's value is a long string (e.g. a URL), it was wrapping to the next line and visually overlaying the next item's controls. The TreeView uses a fixed row height of 20px via itemExtent, but `Text.rich` widgets inside `DisplayProvider` and `DapDisplayProvider` had no single-line constraint. now this issue is fixed by adding `maxLines: 1`, `softWrap: false`, `overflow: TextOverflow.ellipsis` to all relevant `Text.rich` widgets. Literal newlines are replaced with `\\n` for visibility and values are wrapped in DevToolsTooltip so the full value is accessible on hover. A widget test was added in `test/screens/debugger/debugger_screen_dap_variables_test.dart` that pumps a DAP variable node with a 1000-character value and asserts single-line truncation properties and the tooltip carrying the full value. before this fix Long URL wraps to next line, overlaying next item's controls <img width="472" height="124" alt="image" src="https://github.com/user-attachments/assets/abada05b-ccb0-458c-a6b0-26f12c9175ad" /> after this fix Long URL truncated with ... on a single line, hover shows full value screenshot for reference <img width="538" height="590" alt="image" src="https://github.com/user-attachments/assets/c6c3f2ee-efe9-4d7c-afc5-9d7894ca407f" /> ### General checklist ### Issues checklist ### Tests checklist ### AI-tooling checklist * [ ] I read the [AI contributions guidelines] and agree to follow them. * [ ] I reviewed all AI-generated code before opening this PR. * [ ] I understand and am able to discuss the code in this PR. * [ ] I have verifed the accuracy of any AI-generated text included in the PR description. * [ ] I commit to verifying the accuracy of any AI-generated code or text that I upload in response to review comments. ### Feature-change checklist * [ ] I added the `release-notes-not-required` label or left a comment requesting the label be added. * [ ] I added an entry to `packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md`. * [ ] I included before/after screenshots and/or a GIF demo of the new UI to my PR description. * [ ] I ran the DevTools app locally to manually verify my changes. ![build.yaml badge] If you need help, consider asking for help on [Discord]. [`contributions-welcome`]: https://github.com/flutter/devtools/issues?q=state%3Aopen%20label%3Acontributions-welcome [`good-first-issue`]: https://github.com/flutter/devtools/issues?q=state%3Aopen%20label%3Agood-first-issue [build.yaml badge]: https://github.com/flutter/devtools/actions/workflows/build.yaml/badge.svg
1 parent 7d3d1ec commit 4141851

3 files changed

Lines changed: 97 additions & 41 deletions

File tree

packages/devtools_app/lib/src/shared/console/widgets/display_provider.dart

Lines changed: 61 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,30 @@ class DisplayProvider extends StatefulWidget {
3636
State<DisplayProvider> createState() => _DisplayProviderState();
3737
}
3838

39+
class OverflowingText extends StatelessWidget {
40+
const OverflowingText({
41+
super.key,
42+
required this.textSpan,
43+
this.tooltipMessage,
44+
});
45+
46+
final InlineSpan textSpan;
47+
final String? tooltipMessage;
48+
49+
@override
50+
Widget build(BuildContext context) {
51+
final textWidget = Text.rich(
52+
textSpan,
53+
maxLines: 1,
54+
softWrap: false,
55+
overflow: TextOverflow.ellipsis,
56+
);
57+
final message = tooltipMessage;
58+
if (message == null) return textWidget;
59+
return DevToolsTooltip(message: message, child: textWidget);
60+
}
61+
}
62+
3963
class _DisplayProviderState extends State<DisplayProvider> {
4064
bool isHovered = false;
4165
@override
@@ -46,8 +70,8 @@ class _DisplayProviderState extends State<DisplayProvider> {
4670
return InteractivityWrapper(
4771
onTap: widget.onTap,
4872
menuButtons: _getMenuButtons(context),
49-
child: Text.rich(
50-
TextSpan(
73+
child: OverflowingText(
74+
textSpan: TextSpan(
5175
children: textSpansFromAnsi(
5276
widget.variable.text!,
5377
theme.subtleFixedFontStyle,
@@ -79,44 +103,40 @@ class _DisplayProviderState extends State<DisplayProvider> {
79103
final contents = InteractivityWrapper(
80104
onTap: widget.onTap,
81105
menuButtons: _getMenuButtons(context),
82-
child: DevToolsTooltip(
83-
message: originalDisplayValue,
84-
child: Container(
85-
color: isHovered ? Theme.of(context).highlightColor : null,
86-
child: Row(
87-
children: [
88-
Expanded(
89-
child: Text.rich(
90-
maxLines: 1,
91-
overflow: TextOverflow.ellipsis,
92-
TextSpan(
93-
text: hasName ? widget.variable.name : null,
94-
style: widget.variable.artificialName
95-
? theme.subtleFixedFontStyle
96-
: theme.fixedFontStyle.apply(
97-
color: theme.colorScheme.controlFlowSyntaxColor,
98-
),
99-
children: [
100-
if (hasName)
101-
TextSpan(text: ': ', style: theme.fixedFontStyle),
102-
TextSpan(
103-
text: displayValue,
104-
style: widget.variable.artificialValue
105-
? theme.subtleFixedFontStyle
106-
: _variableDisplayStyle(theme, widget.variable),
107-
),
108-
],
109-
),
106+
child: Container(
107+
color: isHovered ? Theme.of(context).highlightColor : null,
108+
child: Row(
109+
children: [
110+
Expanded(
111+
child: OverflowingText(
112+
tooltipMessage: originalDisplayValue,
113+
textSpan: TextSpan(
114+
text: hasName ? widget.variable.name : null,
115+
style: widget.variable.artificialName
116+
? theme.subtleFixedFontStyle
117+
: theme.fixedFontStyle.apply(
118+
color: theme.colorScheme.controlFlowSyntaxColor,
119+
),
120+
children: [
121+
if (hasName)
122+
TextSpan(text: ': ', style: theme.fixedFontStyle),
123+
TextSpan(
124+
text: displayValue,
125+
style: widget.variable.artificialValue
126+
? theme.subtleFixedFontStyle
127+
: _variableDisplayStyle(theme, widget.variable),
128+
),
129+
],
110130
),
111131
),
112-
if (isHovered && widget.onCopy != null)
113-
DevToolsButton(
114-
icon: Icons.copy_outlined,
115-
outlined: false,
116-
onPressed: () => widget.onCopy!.call(widget.variable),
117-
),
118-
],
119-
),
132+
),
133+
if (isHovered && widget.onCopy != null)
134+
DevToolsButton(
135+
icon: Icons.copy_outlined,
136+
outlined: false,
137+
onPressed: () => widget.onCopy!.call(widget.variable),
138+
),
139+
],
120140
),
121141
),
122142
);
@@ -248,8 +268,9 @@ class DapDisplayProvider extends StatelessWidget {
248268
// TODO(https://github.com/flutter/devtools/issues/6056): Wrap in
249269
// interactivity wrapper to provide inspect and re-root functionality. Add
250270
// tooltip on hover to provide type information.
251-
return Text.rich(
252-
TextSpan(
271+
return OverflowingText(
272+
tooltipMessage: value,
273+
textSpan: TextSpan(
253274
text: name,
254275
style: theme.fixedFontStyle.apply(
255276
color: theme.colorScheme.controlFlowSyntaxColor,

packages/devtools_app/release_notes/NEXT_RELEASE_NOTES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ TODO: Remove this section if there are not any updates.
3636

3737
## Debugger updates
3838

39-
TODO: Remove this section if there are not any updates.
39+
- Fixed an issue where long string values in the console/variables view would overflow and overlap with other elements. [#7112](https://github.com/flutter/devtools/issues/7112)
4040

4141
## Network profiler updates
4242

packages/devtools_app/test/screens/debugger/debugger_screen_dap_variables_test.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,39 @@ void main() {
126126
// String is now visible.
127127
expect(stringFinder, findsOneWidget);
128128
});
129+
testWidgetsWithWindowSize(
130+
'Truncates long variables to a single line with tooltip',
131+
windowSize,
132+
(WidgetTester tester) async {
133+
final longString = 'a' * 1000;
134+
final node = DapObjectNode(
135+
service: vmService,
136+
variable: dap.Variable(
137+
name: 'longVar',
138+
value: longString,
139+
variablesReference: 0,
140+
),
141+
);
142+
143+
fakeServiceConnection.appState.setDapVariables([node]);
144+
await pumpDebuggerScreen(tester, debuggerController);
145+
146+
final tooltipFinder = find.byWidgetPredicate(
147+
(widget) => widget is DevToolsTooltip && widget.message == longString,
148+
);
149+
expect(tooltipFinder, findsOneWidget);
150+
151+
final finder = find.byType(RichText);
152+
bool foundTruncated = false;
153+
for (final widget in tester.widgetList<RichText>(finder)) {
154+
if (widget.text.toPlainText().contains('longVar: $longString')) {
155+
expect(widget.maxLines, 1);
156+
expect(widget.softWrap, false);
157+
expect(widget.overflow, TextOverflow.ellipsis);
158+
foundTruncated = true;
159+
}
160+
}
161+
expect(foundTruncated, isTrue);
162+
},
163+
);
129164
}

0 commit comments

Comments
 (0)