fix: dirty detection — compare fullText, snapshot after load

Stefan reported false-positive "Discard unsaved changes?"
dialogs when clicking around in the file list. Cause: the
naive `_code.text == _loadedText` check fired stale even
right after _code.text = text, because CodeController
silently rewrites the visible text during the assignment
(trailing-newline normalisation, fold marker insertion on
some YAML constructs, …) so _code.text deviated from the
just-stored _loadedText immediately.

Fix:
  * Drop `_loadedText` field, replace with `_baseline`.
  * Set `_code.text = ...` first (outside setState), then
    capture `_baseline = _code.fullText` inside setState.
    fullText is the canonical post-processed value, not the
    visible-after-folding text — comparing fullText against
    fullText eliminates the spurious diff.
  * _dirty getter now compares `_code.fullText != _baseline`.

Applied to all four load / save / new-flow sites.

Bump version 0.1.0 → 0.1.1. Studio's pubspec already pins
`ref: main` so the next `flutter pub get` picks the fix up.

Signed-off-by: flemming-it <sf@flemming.it>
This commit is contained in:
flemming-it 2026-05-30 14:56:39 +02:00
parent 51f9a1d2b1
commit 8b918f8f2a
2 changed files with 22 additions and 10 deletions

View file

@ -52,7 +52,14 @@ class _FlowEditorPageState extends State<FlowEditorPage> {
late final CodeController _code; late final CodeController _code;
late final FlowEditorStrings _l; late final FlowEditorStrings _l;
String? _activeName; String? _activeName;
String _loadedText = ''; // Baseline captured AFTER the CodeController has finished
// processing the loaded text (line-ending normalisation,
// trailing-newline handling, fold-marker insertion, ).
// Comparing _code.fullText against this baseline avoids the
// false-positive "discard unsaved changes?" prompts the
// naive _code.text == _loadedText check used to trigger on
// files the controller silently rewrote during load.
String _baseline = '';
late Future<List<_FlowFile>> _files; late Future<List<_FlowFile>> _files;
Object? _runError; Object? _runError;
Map<String, Object>? _runOutputs; Map<String, Object>? _runOutputs;
@ -85,7 +92,7 @@ class _FlowEditorPageState extends State<FlowEditorPage> {
if (mounted) setState(() {}); if (mounted) setState(() {});
} }
bool get _dirty => _activeName != null && _code.text != _loadedText; bool get _dirty => _activeName != null && _code.fullText != _baseline;
Future<List<_FlowFile>> _listFiles() async { Future<List<_FlowFile>> _listFiles() async {
final dir = Directory(_defaultFlowsDir()); final dir = Directory(_defaultFlowsDir());
@ -115,10 +122,15 @@ class _FlowEditorPageState extends State<FlowEditorPage> {
if (!f.existsSync()) return; if (!f.existsSync()) return;
final text = await f.readAsString(); final text = await f.readAsString();
if (!mounted) return; if (!mounted) return;
// Set the controller text first, then snapshot fullText
// as the baseline. Reading fullText after the assignment
// captures whatever line-ending normalisation / fold-
// marker insertion CodeController applied, so the dirty
// check is comparing apples to apples.
_code.text = text;
setState(() { setState(() {
_activeName = name; _activeName = name;
_loadedText = text; _baseline = _code.fullText;
_code.text = text;
_runOutputs = null; _runOutputs = null;
_runError = null; _runError = null;
}); });
@ -130,10 +142,10 @@ class _FlowEditorPageState extends State<FlowEditorPage> {
if (keep == false || !mounted) return; if (keep == false || !mounted) return;
} }
final text = await File(f.path).readAsString(); final text = await File(f.path).readAsString();
_code.text = text;
setState(() { setState(() {
_activeName = f.name; _activeName = f.name;
_loadedText = text; _baseline = _code.fullText;
_code.text = text;
_runOutputs = null; _runOutputs = null;
_runError = null; _runError = null;
}); });
@ -168,7 +180,7 @@ class _FlowEditorPageState extends State<FlowEditorPage> {
await f.writeAsString(_code.text, flush: true); await f.writeAsString(_code.text, flush: true);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_loadedText = _code.text; _baseline = _code.fullText;
_files = _listFiles(); _files = _listFiles();
}); });
} catch (e) { } catch (e) {
@ -215,10 +227,10 @@ outputs:
} }
await f.writeAsString(template, flush: true); await f.writeAsString(template, flush: true);
if (!mounted) return; if (!mounted) return;
_code.text = template;
setState(() { setState(() {
_activeName = name; _activeName = name;
_loadedText = template; _baseline = _code.fullText;
_code.text = template;
_files = _listFiles(); _files = _listFiles();
}); });
} catch (e) { } catch (e) {

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.1.0 version: 0.1.1
publish_to: 'none' publish_to: 'none'
repository: https://git.flemming.ai/fai/studio-flow-editor repository: https://git.flemming.ai/fai/studio-flow-editor