feat(editor): type-token coloring + analyzer diagnostics

The text tab now colours `type: text|json|bytes|file|number`
values in the same hues the graph canvas uses for the wire of
that type — a glance at the YAML confirms what a glance at the
graph shows. Promoted the wire-colour palette to a single
source of truth in `wire_colors.dart` so the graph, the
properties panel and the text tab can't drift.

Adds `FlowAnalyzer` (extends `AbstractAnalyzer`) so the gutter
shows error pins and broken lines get wavy red underlines for:

  - YAML parse errors (source-span pinned to the offending line)
  - `use:` referencing a capability the operator hasn't installed
    (bare provider/name match — `text.echo@^1` and `text.echo`
    compare equal)

Editor host passes a closure into `setAvailableCapabilities` so
the analyser always sees the current installed list without
re-creating the controller on every Studio rebuild. Bumps the
package version to 0.15.0.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-06-04 00:46:51 +02:00
parent 73736769a0
commit 885d2db4e1
8 changed files with 400 additions and 52 deletions

View file

@ -0,0 +1,98 @@
// FlowAnalyzer feeds AbstractAnalyzer-shaped issues into the
// flow editor's CodeController so the gutter can show error
// pins and the text tab can underline broken lines.
//
// Two kinds of issues are reported today:
//
// - YAML parse errors. The exception's source span pins the
// line; the message goes straight through.
// - `use:` lines pointing at a capability the operator hasn't
// installed. We compare the bare `provider/name` (no
// version) against the host-supplied capability list, so
// `text.echo@^1` matches an installed `text.echo` even when
// the constraints differ.
//
// `$step.field` reference resolution and per-field type checks
// will land here next they need the module spec map and are
// staged in the editor controller plumbing in editor_controller.dart.
import 'package:flutter_code_editor/flutter_code_editor.dart';
import 'package:yaml/yaml.dart';
class FlowAnalyzer extends AbstractAnalyzer {
/// Returns the names of capabilities the operator has installed.
/// A closure so the analyzer always sees the current list
/// without needing a re-create on every Studio rebuild.
final List<String> Function() availableCapabilities;
const FlowAnalyzer({required this.availableCapabilities});
@override
Future<AnalysisResult> analyze(Code code) async {
final issues = <Issue>[];
final text = code.text;
if (text.trim().isEmpty) {
return const AnalysisResult(issues: []);
}
final caps = availableCapabilities();
final knownBareCaps = caps.map(_bareCap).toSet();
YamlNode? doc;
try {
doc = loadYamlNode(text);
} on YamlException catch (e) {
issues.add(
Issue(
line: e.span?.start.line ?? 0,
message: 'YAML: ${e.message}',
type: IssueType.error,
),
);
return AnalysisResult(issues: issues);
} catch (e) {
issues.add(
Issue(
line: 0,
message: 'YAML: $e',
type: IssueType.error,
),
);
return AnalysisResult(issues: issues);
}
if (doc is YamlMap) {
final steps = doc['steps'];
if (steps is YamlList) {
for (final step in steps.nodes) {
if (step is! YamlMap) continue;
final useNode = step.nodes['use'];
if (useNode == null) continue;
final useValue = useNode.value;
if (useValue is! String) continue;
if (knownBareCaps.isEmpty) continue;
if (!knownBareCaps.contains(_bareCap(useValue))) {
issues.add(
Issue(
line: useNode.span.start.line,
message:
'Unknown capability "$useValue". '
'Install the module that provides it, or check the spelling.',
type: IssueType.error,
),
);
}
}
}
}
return AnalysisResult(issues: issues);
}
}
/// Strip the `@<version>` tail from a capability spec so
/// `text.echo@^1` and `text.echo` compare equal.
String _bareCap(String full) {
final at = full.indexOf('@');
return at < 0 ? full : full.substring(0, at);
}