fix(editor): drop misaligned gutter error pin, add bottom diagnostic strip

Two glitches visible in 0.15.1:

  1. The flutter_code_editor gutter renders an Icons.cancel pin
     for every analyzer issue, hard-positioned inside a 16-px
     slot. With a single-digit indent the icon centres look off;
     with double-digit line numbers it shifts further.
  2. The hover popup for that pin paints at `offset.dx + width`
     of the gutter cell — which lands INSIDE the code area, so
     'Unknown capability ...' overlaps the YAML text the
     operator is trying to read.

The package doesn't expose a fix for either — the icon is a
const in error.dart, the popup position is hard-coded in the
StatefulWidget. Cleanest path: hide the gutter error column
entirely (`GutterStyle.showErrors: false`) and surface the
analyzer issues through a dedicated bottom strip we own.

The new _DiagnosticStrip renders as a single 22-px line under
the editor: a coloured dot + 'N errors · M warnings · L7:
${first.message}'. Tap expands into a per-issue list capped at
140-px scroll, dot-coloured by IssueType. Empty state shows a
muted 'No issues' so the strip never disappears and never
shifts the layout.

Wavy underlines in the text body keep working untouched — the
two surfaces complement each other: the underline points at
the broken token, the strip names what's wrong.

Bumped to 0.16.0.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-04 10:32:40 +02:00
parent ae443c6f81
commit 1b50924e16
2 changed files with 233 additions and 13 deletions

View file

@ -592,6 +592,9 @@ outputs:
); );
return CodeTheme( return CodeTheme(
data: CodeThemeData(styles: _yamlStyle(theme)), data: CodeThemeData(styles: _yamlStyle(theme)),
child: Column(
children: [
Expanded(
child: CodeField( child: CodeField(
controller: _controller.codeController, controller: _controller.codeController,
textStyle: mono, textStyle: mono,
@ -599,12 +602,27 @@ outputs:
minLines: null, minLines: null,
maxLines: null, maxLines: null,
gutterStyle: GutterStyle( gutterStyle: GutterStyle(
textStyle: mono.copyWith(color: theme.colorScheme.onSurfaceVariant), textStyle: mono.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
background: theme.colorScheme.surfaceContainer, background: theme.colorScheme.surfaceContainer,
showLineNumbers: true, showLineNumbers: true,
// Disable the built-in error column entirely.
// The Icons.cancel pin is hard-positioned inside
// a 16-px slot, and its mouse-hover popup paints
// OVER the code area instead of beside it the
// overlap was the operator-visible glitch in
// editor 0.15.1. Issues remain surfaced via the
// wavy underline (FlowYamlCodeController) plus
// the bottom diagnostic strip below.
showErrors: false,
), ),
background: theme.colorScheme.surface, background: theme.colorScheme.surface,
), ),
),
_DiagnosticStrip(controller: _controller.codeController),
],
),
); );
} }
@ -1105,3 +1123,205 @@ class _NewFlowDialogState extends State<_NewFlowDialog> {
); );
} }
} }
/// Bottom diagnostic strip single-line "X issues · {first}"
/// summary that opens into a per-line list on tap. Replaces the
/// gutter's hard-positioned error pin so the popup never paints
/// over the code area, and gives the operator a stable surface
/// to scan even when the wavy underline sits off-screen.
///
/// The strip listens to the CodeController; it rebuilds on every
/// analyzer pass without polling.
class _DiagnosticStrip extends StatefulWidget {
final CodeController controller;
const _DiagnosticStrip({required this.controller});
@override
State<_DiagnosticStrip> createState() => _DiagnosticStripState();
}
class _DiagnosticStripState extends State<_DiagnosticStrip> {
bool _expanded = false;
@override
void initState() {
super.initState();
widget.controller.addListener(_onChange);
}
@override
void dispose() {
widget.controller.removeListener(_onChange);
super.dispose();
}
void _onChange() {
if (mounted) setState(() {});
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final issues = widget.controller.analysisResult.issues;
if (issues.isEmpty) {
return Container(
height: 22,
padding: const EdgeInsets.symmetric(horizontal: 12),
alignment: Alignment.centerLeft,
decoration: BoxDecoration(
color: theme.colorScheme.surfaceContainer,
border: Border(
top: BorderSide(color: theme.colorScheme.outlineVariant),
),
),
child: Text(
'No issues',
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 10,
letterSpacing: 0.4,
color: theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
),
),
);
}
final errorCount = issues.where((i) => i.type == IssueType.error).length;
final warnCount = issues.where((i) => i.type == IssueType.warning).length;
final tone = errorCount > 0
? theme.colorScheme.error
: const Color(0xFFEF6C00); // amber/orange for warnings
return Material(
color: theme.colorScheme.surfaceContainer,
child: InkWell(
onTap: () => setState(() => _expanded = !_expanded),
child: Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: theme.colorScheme.outlineVariant),
),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
color: tone,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
_summary(errorCount, warnCount),
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
fontWeight: FontWeight.w600,
color: tone,
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
'L${issues.first.line + 1}: ${issues.first.message}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
color: theme.colorScheme.onSurface,
),
),
),
Icon(
_expanded ? Icons.expand_more : Icons.expand_less,
size: 16,
color: theme.colorScheme.onSurfaceVariant,
),
],
),
if (_expanded) ...[
const SizedBox(height: 6),
Divider(height: 1, color: theme.colorScheme.outlineVariant),
const SizedBox(height: 6),
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 140),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (final issue in issues)
Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(top: 5, right: 8),
decoration: BoxDecoration(
color: switch (issue.type) {
IssueType.error =>
theme.colorScheme.error,
IssueType.warning =>
const Color(0xFFEF6C00),
IssueType.info =>
theme.colorScheme.primary,
},
shape: BoxShape.circle,
),
),
SizedBox(
width: 42,
child: Text(
'L${issue.line + 1}',
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
color:
theme.colorScheme.onSurfaceVariant,
),
),
),
Expanded(
child: Text(
issue.message,
style: TextStyle(
fontFamily: 'JetBrains Mono',
fontSize: 11,
color: theme.colorScheme.onSurface,
),
),
),
],
),
),
],
),
),
),
],
],
),
),
),
);
}
String _summary(int errors, int warnings) {
final parts = <String>[];
if (errors > 0) parts.add('$errors error${errors == 1 ? '' : 's'}');
if (warnings > 0) {
parts.add('$warnings warning${warnings == 1 ? '' : 's'}');
}
return parts.join(' · ');
}
}

View file

@ -1,6 +1,6 @@
name: fai_studio_flow_editor name: fai_studio_flow_editor
description: Swappable inline YAML editor for F∆I Studio flows. description: Swappable inline YAML editor for F∆I Studio flows.
version: 0.15.1 version: 0.16.0
publish_to: 'none' publish_to: 'none'
repository: https://git.flemming.ai/fai/studio-flow-editor repository: https://git.flemming.ai/fai/studio-flow-editor