Files
clide/lib/builtin/diff/src/diff_controller.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

100 lines
2.8 KiB
Dart

/// State model for the diff workspace tab.
///
/// Holds the parsed diff data for a single file (or all files). Hydrates
/// via `git.diff` IPC, subscribes to `git.changed` events to refresh.
library;
import 'dart:async';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter/foundation.dart';
class DiffController extends ChangeNotifier {
DiffController({required this.ipc, required this.events}) {
_eventSub = events.on<DaemonEvent>().listen(_onEvent);
}
final DaemonClient ipc;
final DaemonBus events;
StreamSubscription<DaemonEvent>? _eventSub;
List<Map<String, Object?>> _diffs = const [];
List<Map<String, Object?>> get diffs => _diffs;
bool _staged = false;
bool get showStaged => _staged;
String? _error;
String? get error => _error;
bool _loading = false;
bool get loading => _loading;
String? _focusPath;
/// The file the view should scroll into view + highlight (T-233), set by
/// [focus] when `clide ui open diff <path>` (or a UI reveal) targets a file.
/// Null until something focuses a path; cleared when that file leaves the
/// diff (e.g. its changes are reverted).
String? get focusPath => _focusPath;
/// Focus [path] within the working-tree diff (T-233): record it so the view
/// scrolls it into view + highlights it, and reload so the latest edits to
/// that file are present even if the `git.changed` refresh hasn't landed yet.
/// Revealing the diff tab itself is the caller's job (the diff extension).
void focus(String path) {
_focusPath = path;
notifyListeners();
unawaited(load(staged: _staged));
}
/// Load diffs. Optionally filter to [paths] and toggle [staged].
Future<void> load({bool staged = false, List<String> paths = const []}) async {
_staged = staged;
_loading = true;
notifyListeners();
final r = await ipc.request('git.diff', args: {'staged': staged, if (paths.isNotEmpty) 'paths': paths});
_loading = false;
if (!r.ok) {
_error = r.error?.message ?? 'git.diff failed';
notifyListeners();
return;
}
_error = null;
_diffs = _castList(r.data['diffs']);
// Drop a focus whose file no longer has changes, so the view doesn't keep
// a highlight on something that's gone.
if (_focusPath != null && !_diffs.any((d) => d['path'] == _focusPath)) {
_focusPath = null;
}
notifyListeners();
}
void toggleStaged() {
unawaited(load(staged: !_staged));
}
void _onEvent(DaemonEvent e) {
if (e.subsystem != 'git') return;
if (e.kind == 'git.changed') {
unawaited(load(staged: _staged));
}
}
static List<Map<String, Object?>> _castList(Object? raw) {
if (raw is! List) return const [];
return [for (final e in raw) (e as Map).cast<String, Object?>()];
}
@override
void dispose() {
_eventSub?.cancel();
_eventSub = null;
super.dispose();
}
}