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>
This commit is contained in:
2026-06-11 12:11:53 +02:00
co-authored by Claude Opus 4.8
parent bcea5f15b7
commit 6d0ebab721
444 changed files with 7587 additions and 12849 deletions
+10 -31
View File
@@ -54,13 +54,7 @@ class CliInstallStatus {
/// Result of [CliInstaller.install].
class CliInstallResult {
const CliInstallResult({
required this.ok,
required this.message,
this.installedPath,
this.onPath = true,
this.fromDevTree = false,
});
const CliInstallResult({required this.ok, required this.message, this.installedPath, this.onPath = true, this.fromDevTree = false});
final bool ok;
final String message;
@@ -80,14 +74,10 @@ class CliInstallResult {
/// environment, candidate client locations, the target dir) is injectable so
/// the logic is unit-testable without a real install.
class CliInstaller {
CliInstaller({
required this.resolvedExecutable,
Map<String, String>? env,
List<String>? bundledClientCandidates,
String? installDir,
}) : env = env ?? Platform.environment,
bundledClientCandidates = bundledClientCandidates ?? _defaultBundledCandidates(resolvedExecutable, env ?? Platform.environment),
installDir = installDir ?? _defaultInstallDir(env ?? Platform.environment);
CliInstaller({required this.resolvedExecutable, Map<String, String>? env, List<String>? bundledClientCandidates, String? installDir})
: env = env ?? Platform.environment,
bundledClientCandidates = bundledClientCandidates ?? _defaultBundledCandidates(resolvedExecutable, env ?? Platform.environment),
installDir = installDir ?? _defaultInstallDir(env ?? Platform.environment);
/// Path to the running Flutter GUI executable
/// (`Platform.resolvedExecutable`).
@@ -132,7 +122,8 @@ class CliInstaller {
if (src == null) {
return const CliInstallResult(
ok: false,
message: 'No bundled clide client found to install. Build with '
message:
'No bundled clide client found to install. Build with '
'`make build` so the C client ships inside the app bundle.',
);
}
@@ -203,11 +194,7 @@ class CliInstaller {
return null;
}
String _expandedPath() => expandedPath(
env['PATH'] ?? '',
macOS: Platform.isMacOS,
home: env['HOME'] ?? '',
);
String _expandedPath() => expandedPath(env['PATH'] ?? '', macOS: Platform.isMacOS, home: env['HOME'] ?? '');
static String _defaultInstallDir(Map<String, String> env) => '${env['HOME'] ?? ''}/.local/bin';
@@ -217,10 +204,7 @@ class CliInstaller {
/// `Contents/MacOS/` on macOS).
static List<String> _defaultBundledCandidates(String resolvedExecutable, Map<String, String> env) {
final exeDir = File(resolvedExecutable).parent.path;
return [
if ((env['CLIDE_CLI_BIN'] ?? '').isNotEmpty) env['CLIDE_CLI_BIN']!,
'$exeDir/clide-cli',
];
return [if ((env['CLIDE_CLI_BIN'] ?? '').isNotEmpty) env['CLIDE_CLI_BIN']!, '$exeDir/clide-cli'];
}
}
@@ -239,12 +223,7 @@ bool isDevTreeClient(String path) => _devTreeClient.hasMatch(path);
/// platform-parameterized function so both branches are testable off-platform.
String expandedPath(String base, {required bool macOS, String home = ''}) {
if (!macOS) return base;
final extras = <String>[
if (home.isNotEmpty) '$home/.local/bin',
'/opt/homebrew/bin',
'/opt/homebrew/sbin',
'/usr/local/bin',
];
final extras = <String>[if (home.isNotEmpty) '$home/.local/bin', '/opt/homebrew/bin', '/opt/homebrew/sbin', '/usr/local/bin'];
final existing = base.split(':').toSet();
final missing = extras.where((p) => !existing.contains(p));
if (missing.isEmpty) return base;
+1 -4
View File
@@ -14,10 +14,7 @@ class ClideClipboard {
final int historyLimit;
final Map<Type, List<Object>> _history = {};
Future<void> write<T extends Object>(
T value, {
String Function(T)? toPlain,
}) async {
Future<void> write<T extends Object>(T value, {String Function(T)? toPlain}) async {
final bucket = _history.putIfAbsent(T, () => <Object>[]);
bucket.insert(0, value);
if (bucket.length > historyLimit) bucket.removeLast();
+1 -3
View File
@@ -5,9 +5,7 @@ import 'package:flutter/services.dart';
/// (modifiers sorted, lowercased) so equality works for lookup keys.
@immutable
class Keybinding {
Keybinding({required Set<String> modifiers, required String key})
: modifiers = _canonModifiers(modifiers),
key = key.toLowerCase();
Keybinding({required Set<String> modifiers, required String key}) : modifiers = _canonModifiers(modifiers), key = key.toLowerCase();
final List<String> modifiers;
final String key;
+2 -9
View File
@@ -17,19 +17,12 @@ class CommandRegistry extends ChangeNotifier {
Iterable<CommandContribution> get all => _byCommand.values;
CommandContribution? get(String command) => _byCommand[command];
Future<IpcResponse> execute(
String command, {
List<String> args = const [],
}) async {
Future<IpcResponse> execute(String command, {List<String> args = const []}) async {
final c = _byCommand[command];
if (c == null) {
return IpcResponse.err(
id: '',
error: IpcError(
code: IpcExitCode.notFound,
kind: IpcErrorKind.notFound,
message: 'no such command: $command',
),
error: IpcError(code: IpcExitCode.notFound, kind: IpcErrorKind.notFound, message: 'no such command: $command'),
);
}
return c.run(args);
+3 -14
View File
@@ -2,10 +2,7 @@ import 'dart:async';
import 'package:flutter/widgets.dart';
typedef DialogBuilder<T> = Widget Function(
BuildContext context,
void Function([T? result]) dismiss,
);
typedef DialogBuilder<T> = Widget Function(BuildContext context, void Function([T? result]) dismiss);
/// Single-at-a-time modal router.
///
@@ -70,12 +67,7 @@ class _Queued {
/// Hosts the current dialog from [DialogRouter]. Place high in the tree
/// (inside the WidgetsApp) so dialogs overlay every other surface.
class DialogHost extends StatelessWidget {
const DialogHost({
super.key,
required this.router,
required this.child,
this.backdropColor = const Color(0xC0000000),
});
const DialogHost({super.key, required this.router, required this.child, this.backdropColor = const Color(0xC0000000)});
final DialogRouter router;
final Widget child;
@@ -98,10 +90,7 @@ class DialogHost extends StatelessWidget {
child: ColoredBox(
color: backdropColor,
child: Center(
child: GestureDetector(
onTap: () {},
child: b(ctx, router.dismiss),
),
child: GestureDetector(onTap: () {}, child: b(ctx, router.dismiss)),
),
),
),
+1 -5
View File
@@ -1,11 +1,7 @@
import 'dart:async';
class Message {
Message({
required this.publisher,
required this.channel,
required this.data,
}) : timestamp = DateTime.now();
Message({required this.publisher, required this.channel, required this.data}) : timestamp = DateTime.now();
final String publisher;
final String channel;
+13 -28
View File
@@ -13,13 +13,7 @@ class ClideEventEnvelope {
final ClideEvent event;
final DateTime timestamp;
Map<String, Object?> toJson() => {
'v': 1,
'subsystem': event.subsystem,
'kind': event.kind,
'ts': timestamp.toIso8601String(),
'data': event.payload(),
};
Map<String, Object?> toJson() => {'v': 1, 'subsystem': event.subsystem, 'kind': event.kind, 'ts': timestamp.toIso8601String(), 'data': event.payload()};
}
class DaemonConnectionChanged extends ClideEvent {
@@ -89,12 +83,7 @@ class ExtensionDeactivated extends ClideEvent {
/// narrow by subsystem+kind, or register a converter that emits a typed
/// `ClideEvent` subclass into the bus.
class DaemonEvent extends ClideEvent {
const DaemonEvent({
required this.subsystem,
required this.kind,
required this.data,
required this.ts,
});
const DaemonEvent({required this.subsystem, required this.kind, required this.data, required this.ts});
@override
final String subsystem;
@@ -145,26 +134,22 @@ class TeamMemberJoined extends ClideEvent {
String get kind => 'member-joined';
@override
Map<String, Object?> payload() => {
'team': team,
'agentId': agentId,
'name': name,
'agentType': agentType,
'paneId': paneId,
if (model != null) 'model': model,
if (color != null) 'color': color,
if (cwd != null) 'cwd': cwd,
if (transcriptPath != null) 'transcriptPath': transcriptPath,
};
'team': team,
'agentId': agentId,
'name': name,
'agentType': agentType,
'paneId': paneId,
if (model != null) 'model': model,
if (color != null) 'color': color,
if (cwd != null) 'cwd': cwd,
if (transcriptPath != null) 'transcriptPath': transcriptPath,
};
}
/// A Claude Code tmux teammate's pane went away (it exited or the team
/// dissolved) — T-139.
class TeamMemberLeft extends ClideEvent {
const TeamMemberLeft({
required this.team,
required this.agentId,
required this.paneId,
});
const TeamMemberLeft({required this.team, required this.agentId, required this.paneId});
final String team;
final String agentId;
+4 -13
View File
@@ -147,13 +147,7 @@ class KernelServices {
final settings = SettingsStore(appDir: appDir);
await settings.load();
final i18n = I18n(
loader: i18nLoader,
log: log,
defaultLocale: defaultLocale,
initialLocale: initialLocale,
availableLocales: availableLocales,
);
final i18n = I18n(loader: i18nLoader, log: log, defaultLocale: defaultLocale, initialLocale: initialLocale, availableLocales: availableLocales);
for (final ns in preloadNamespaces) {
await i18n.ensureNamespaceLoaded(ns);
}
@@ -193,7 +187,8 @@ class KernelServices {
onProjectOpen: onProjectOpen,
onValidateProject: onValidateProject,
);
final ipc = isolateClient ??
final ipc =
isolateClient ??
(daemonClientFactory != null
? daemonClientFactory(log, events, arrangement, panels)
: DaemonClient(
@@ -309,11 +304,7 @@ class KernelServices {
}
class ClideKernel extends InheritedWidget {
const ClideKernel({
super.key,
required this.services,
required super.child,
});
const ClideKernel({super.key, required this.services, required super.child});
final KernelServices services;
+3 -12
View File
@@ -16,10 +16,7 @@ class FilesDropped extends ClideEvent {
@override
String get kind => 'dropped';
@override
Map<String, Object?> payload() => {
'paths': paths,
'slot': slot.value,
};
Map<String, Object?> payload() => {'paths': paths, 'slot': slot.value};
}
/// Tier-0 stub for file pickers and drop targets.
@@ -33,17 +30,11 @@ class FileServices {
FileServices(this._events);
final DaemonBus _events;
Future<List<String>> pickOpen({
List<String> extensions = const [],
bool multiple = false,
}) async {
Future<List<String>> pickOpen({List<String> extensions = const [], bool multiple = false}) async {
throw UnimplementedError('pickOpen — wired in a later tier');
}
Future<String?> pickSave({
String? defaultName,
List<String> extensions = const [],
}) async {
Future<String?> pickSave({String? defaultName, List<String> extensions = const []}) async {
throw UnimplementedError('pickSave — wired in a later tier');
}
+1 -5
View File
@@ -18,11 +18,7 @@ class FocusTracker extends ChangeNotifier {
/// of the three-column layout (sidebar → workspace → context); the
/// statusbar / toolbar aren't included because they don't host
/// keyboard-active content.
static const List<SlotId> traversalOrder = [
Slots.sidebar,
Slots.workspace,
Slots.contextPanel,
];
static const List<SlotId> traversalOrder = [Slots.sidebar, Slots.workspace, Slots.contextPanel];
SlotId? get activeSlot => _slot;
String? get activeContributionId => _contributionId;
+2 -10
View File
@@ -14,22 +14,14 @@ import 'package:flutter/foundation.dart';
/// canonicalized by omitting it (not empty string) so equality works.
@immutable
class FallbackChain {
const FallbackChain({
required this.current,
required this.defaultLocale,
});
const FallbackChain({required this.current, required this.defaultLocale});
final Locale current;
final Locale defaultLocale;
List<Locale> resolve() {
final out = <Locale>[];
for (final l in [
current,
Locale(current.languageCode),
defaultLocale,
Locale(defaultLocale.languageCode),
]) {
for (final l in [current, Locale(current.languageCode), defaultLocale, Locale(defaultLocale.languageCode)]) {
final canon = _canon(l);
if (!out.any((e) => _canon(e) == canon)) {
out.add(l);
+11 -39
View File
@@ -26,9 +26,9 @@ class I18n extends ChangeNotifier {
required Locale defaultLocale,
Locale? initialLocale,
List<Locale> availableLocales = const [Locale('en', 'US')],
}) : _defaultLocale = defaultLocale,
_current = initialLocale ?? defaultLocale,
_available = List<Locale>.unmodifiable(availableLocales);
}) : _defaultLocale = defaultLocale,
_current = initialLocale ?? defaultLocale,
_available = List<Locale>.unmodifiable(availableLocales);
final CatalogLoader loader;
final Logger log;
@@ -50,11 +50,7 @@ class I18n extends ChangeNotifier {
/// Register a catalog that was loaded outside of [loader] — e.g. by the
/// ExtensionManager when a third-party extension activates.
void registerCatalog(
String namespace,
Locale locale,
Map<String, Object?> catalog,
) {
void registerCatalog(String namespace, Locale locale, Map<String, Object?> catalog) {
_cache.putIfAbsent(namespace, () => <Locale, Map<String, Object?>>{})[locale] = catalog;
notifyListeners();
}
@@ -85,14 +81,8 @@ class I18n extends ChangeNotifier {
}
Future<void> _ensureLoaded(String namespace) async {
final byLocale = _cache.putIfAbsent(
namespace,
() => <Locale, Map<String, Object?>>{},
);
final chain = FallbackChain(
current: _current,
defaultLocale: _defaultLocale,
).resolve();
final byLocale = _cache.putIfAbsent(namespace, () => <Locale, Map<String, Object?>>{});
final chain = FallbackChain(current: _current, defaultLocale: _defaultLocale).resolve();
for (final l in chain) {
if (byLocale.containsKey(l)) continue;
byLocale[l] = await loader.load(namespace, l);
@@ -102,24 +92,14 @@ class I18n extends ChangeNotifier {
/// Look up a key, walking the locale fallback chain. Returns the
/// placeholder if nothing hits; returns the key itself when placeholder
/// is null (developer fallback — keys are more useful than blanks).
String string(
String key, {
required String namespace,
String? placeholder,
}) {
String string(String key, {required String namespace, String? placeholder}) {
final byLocale = _cache[namespace];
if (byLocale == null) {
_warnOnce(
'$namespace::MISSING_NAMESPACE::$key',
'i18n: namespace not registered: $namespace (key: $key)',
);
_warnOnce('$namespace::MISSING_NAMESPACE::$key', 'i18n: namespace not registered: $namespace (key: $key)');
return placeholder ?? key;
}
final chain = FallbackChain(
current: _current,
defaultLocale: _defaultLocale,
).resolve();
final chain = FallbackChain(current: _current, defaultLocale: _defaultLocale).resolve();
for (final locale in chain) {
final catalog = byLocale[locale];
@@ -128,22 +108,14 @@ class I18n extends ChangeNotifier {
if (hit != null) return hit;
}
_warnOnce(
'$namespace::${_current.languageCode}::$key',
'i18n: missing key "$key" in namespace "$namespace" (locale ${_current.toString()})',
);
_warnOnce('$namespace::${_current.languageCode}::$key', 'i18n: missing key "$key" in namespace "$namespace" (locale ${_current.toString()})');
return placeholder ?? key;
}
/// [string] + naive `replaceAll` interpolation per replacer.
/// Matches fframe: replacers whose [I18nReplacer.from] isn't present
/// are silent no-ops.
String interpolated(
String key, {
required String namespace,
String? placeholder,
List<I18nReplacer> replacers = const [],
}) {
String interpolated(String key, {required String namespace, String? placeholder, List<I18nReplacer> replacers = const []}) {
var out = string(key, namespace: namespace, placeholder: placeholder);
for (final r in replacers) {
out = out.replaceAll(r.from, r.replace);
+19 -39
View File
@@ -10,13 +10,7 @@ import 'package:clide/kernel/src/log.dart';
import 'package:flutter/foundation.dart';
class DaemonClient extends ChangeNotifier {
DaemonClient({
required String socketPath,
required Logger log,
required DaemonBus events,
}) : _socketPath = socketPath,
_log = log,
_events = events;
DaemonClient({required String socketPath, required Logger log, required DaemonBus events}) : _socketPath = socketPath, _log = log, _events = events;
String _socketPath;
String get socketPath => _socketPath;
@@ -85,10 +79,7 @@ class DaemonClient extends ChangeNotifier {
await _connect();
}
Future<IpcResponse> request(
String cmd, {
Map<String, Object?> args = const {},
}) async {
Future<IpcResponse> request(String cmd, {Map<String, Object?> args = const {}}) async {
if (!_connected || _socket == null) {
// A connection attempt is in flight (startup or reconnect) — wait
// for it rather than failing instantly, so queries issued during
@@ -100,11 +91,7 @@ class DaemonClient extends ChangeNotifier {
if (!_connected || _socket == null) {
return IpcResponse.err(
id: '',
error: IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: 'daemon not connected',
),
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'daemon not connected'),
);
}
}
@@ -150,15 +137,19 @@ class DaemonClient extends ChangeNotifier {
_backoff = const Duration(milliseconds: 200);
_setConnected(true);
_log.info('ipc', 'connected to $_socketPath');
socket.cast<List<int>>().transform(utf8.decoder).transform(const LineSplitter()).listen(
_handleLine,
onDone: _handleDisconnect,
onError: (Object e) {
_log.warn('ipc', 'socket error', error: e);
_handleDisconnect();
},
cancelOnError: true,
);
socket
.cast<List<int>>()
.transform(utf8.decoder)
.transform(const LineSplitter())
.listen(
_handleLine,
onDone: _handleDisconnect,
onError: (Object e) {
_log.warn('ipc', 'socket error', error: e);
_handleDisconnect();
},
cancelOnError: true,
);
} catch (e) {
_log.debug('ipc', 'connect failed ($e); retry in ${_backoff.inMilliseconds}ms');
_scheduleReconnect();
@@ -174,12 +165,7 @@ class DaemonClient extends ChangeNotifier {
final c = _pending.remove(r.id);
if (c != null && !c.isCompleted) c.complete(r);
case IpcEvent e:
_events.emit(DaemonEvent(
subsystem: e.subsystem,
kind: e.kind,
data: e.data,
ts: e.timestamp,
));
_events.emit(DaemonEvent(subsystem: e.subsystem, kind: e.kind, data: e.data, ts: e.timestamp));
case IpcRequest _:
_log.warn('ipc', 'daemon sent a request — unexpected');
}
@@ -196,11 +182,7 @@ class DaemonClient extends ChangeNotifier {
}
void _failPending(String reason) {
final err = IpcError(
code: IpcExitCode.toolError,
kind: IpcErrorKind.toolError,
message: reason,
);
final err = IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: reason);
for (final entry in _pending.entries) {
if (!entry.value.isCompleted) {
entry.value.complete(IpcResponse.err(id: entry.key, error: err));
@@ -213,9 +195,7 @@ class DaemonClient extends ChangeNotifier {
if (_disposed) return;
_reconnectTimer?.cancel();
_reconnectTimer = Timer(_backoff, _connect);
_backoff = Duration(
milliseconds: math.min(_backoff.inMilliseconds * 2, 5000),
);
_backoff = Duration(milliseconds: math.min(_backoff.inMilliseconds * 2, 5000));
}
void _setConnected(bool v) {
+5 -5
View File
@@ -23,11 +23,11 @@ enum KeyModifier {
/// Display string used in palette + tooltip hints.
String get display => switch (this) {
KeyModifier.ctrl => 'Ctrl',
KeyModifier.alt => 'Alt',
KeyModifier.shift => 'Shift',
KeyModifier.meta => 'Cmd',
};
KeyModifier.ctrl => 'Ctrl',
KeyModifier.alt => 'Alt',
KeyModifier.shift => 'Shift',
KeyModifier.meta => 'Cmd',
};
}
/// A modifier-set + a single key, identified by layout-independent
+2 -8
View File
@@ -23,11 +23,7 @@ import 'when_clause.dart';
/// to fire when the sequence matches and the when-clause is true.
@immutable
class KeymapBinding {
const KeymapBinding({
required this.sequence,
required this.intent,
this.when,
});
const KeymapBinding({required this.sequence, required this.intent, this.when});
/// Convenience constructor for a single-chord binding.
KeymapBinding.chord(KeyChord chord, {required this.intent, this.when}) : sequence = [chord];
@@ -204,9 +200,7 @@ class Keymap {
/// must come first. We don't dedupe — a no-op match in a later layer
/// just earns the first slot.
static List<KeymapBinding> _flatten(List<KeymapLayer> layers) {
return [
for (final l in layers.reversed) ...l.bindings,
];
return [for (final l in layers.reversed) ...l.bindings];
}
@override
+6 -18
View File
@@ -41,13 +41,10 @@ const String kKeymapOverridesSetting = 'app.keymap.overrides';
const String kKeymapUserFile = 'keybindings.yaml';
class KeymapService extends ChangeNotifier {
KeymapService({
required SettingsStore settings,
required Directory appDir,
AssetBundle? bundle,
}) : _settings = settings,
_appDir = appDir,
_bundle = bundle ?? rootBundle;
KeymapService({required SettingsStore settings, required Directory appDir, AssetBundle? bundle})
: _settings = settings,
_appDir = appDir,
_bundle = bundle ?? rootBundle;
final SettingsStore _settings;
final Directory _appDir;
@@ -127,11 +124,7 @@ class KeymapService extends ChangeNotifier {
/// defaults, the user can override either via the user file or
/// settings overlay.
void registerCommandBinding(String chordSpec, String commandId, {String? when}) {
_contributions.add(KeymapBinding(
sequence: KeyChord.parseSequence(chordSpec),
intent: InvokeCommandIntent(commandId),
when: WhenExpr.tryParse(when),
));
_contributions.add(KeymapBinding(sequence: KeyChord.parseSequence(chordSpec), intent: InvokeCommandIntent(commandId), when: WhenExpr.tryParse(when)));
_rebuildActive();
}
@@ -149,12 +142,7 @@ class KeymapService extends ChangeNotifier {
}
void _rebuildActive() {
final layers = <KeymapLayer>[
if (_preset != null) _preset!,
KeymapLayer(name: 'contributions', bindings: List.unmodifiable(_contributions)),
if (_userFile != null) _userFile!,
if (_settingsOverlay != null) _settingsOverlay!,
];
final layers = <KeymapLayer>[?_preset, KeymapLayer(name: 'contributions', bindings: List.unmodifiable(_contributions)), ?_userFile, ?_settingsOverlay];
_active = Keymap(layers);
notifyListeners();
}
+6 -18
View File
@@ -33,18 +33,9 @@ enum SeqOutcome {
@immutable
class SeqResult {
const SeqResult.fired(Intent this.intent, this.count)
: outcome = SeqOutcome.fired,
passKey = null;
const SeqResult.pending()
: outcome = SeqOutcome.pending,
intent = null,
count = 1,
passKey = null;
const SeqResult.unmatched(this.passKey)
: outcome = SeqOutcome.unmatched,
intent = null,
count = 1;
const SeqResult.fired(Intent this.intent, this.count) : outcome = SeqOutcome.fired, passKey = null;
const SeqResult.pending() : outcome = SeqOutcome.pending, intent = null, count = 1, passKey = null;
const SeqResult.unmatched(this.passKey) : outcome = SeqOutcome.unmatched, intent = null, count = 1;
final SeqOutcome outcome;
final Intent? intent;
@@ -53,12 +44,9 @@ class SeqResult {
}
class SequenceMatcher {
SequenceMatcher({
required Keymap Function() keymap,
required Map<String, bool> Function() context,
this.captureCounts = true,
}) : _keymap = keymap,
_context = context;
SequenceMatcher({required Keymap Function() keymap, required Map<String, bool> Function() context, this.captureCounts = true})
: _keymap = keymap,
_context = context;
final Keymap Function() _keymap;
final Map<String, bool> Function() _context;
+2 -16
View File
@@ -4,14 +4,7 @@ import 'dart:io';
enum LogLevel { trace, debug, info, warn, error }
class LogRecord {
LogRecord({
required this.level,
required this.source,
required this.message,
required this.timestamp,
this.error,
this.stackTrace,
});
LogRecord({required this.level, required this.source, required this.message, required this.timestamp, this.error, this.stackTrace});
final LogLevel level;
final String source;
@@ -51,14 +44,7 @@ class Logger {
void _emit(LogLevel level, String source, String message, {Object? error, StackTrace? stackTrace}) {
if (level.index < minLevel.index) return;
final rec = LogRecord(
level: level,
source: source,
message: message,
timestamp: DateTime.now().toUtc(),
error: error,
stackTrace: stackTrace,
);
final rec = LogRecord(level: level, source: source, message: message, timestamp: DateTime.now().toUtc(), error: error, stackTrace: stackTrace);
for (final sink in _sinks) {
try {
sink(rec);
+4 -20
View File
@@ -6,13 +6,8 @@ enum NotificationLevel { info, warning, error, success }
@immutable
class ClideNotification {
ClideNotification({
required this.id,
required this.level,
required this.message,
this.title,
this.duration = const Duration(seconds: 4),
}) : createdAt = DateTime.now().toUtc();
ClideNotification({required this.id, required this.level, required this.message, this.title, this.duration = const Duration(seconds: 4)})
: createdAt = DateTime.now().toUtc();
final String id;
final NotificationLevel level;
@@ -41,20 +36,9 @@ class Notifications extends ChangeNotifier {
if (_active.length != before) notifyListeners();
}
void _push(
NotificationLevel level,
String message, {
String? title,
Duration? duration,
}) {
void _push(NotificationLevel level, String message, {String? title, Duration? duration}) {
final id = 'n${_seq++}';
final n = ClideNotification(
id: id,
level: level,
message: message,
title: title,
duration: duration ?? const Duration(seconds: 4),
);
final n = ClideNotification(id: id, level: level, message: message, title: title, duration: duration ?? const Duration(seconds: 4));
_active.add(n);
_timers[id] = Timer(n.duration, () => dismiss(id));
notifyListeners();
+1 -3
View File
@@ -16,9 +16,7 @@ class OsLifecycleEvent extends ClideEvent {
}
class OsBridge {
OsBridge({required Logger log, required DaemonBus events})
: _log = log,
_events = events;
OsBridge({required Logger log, required DaemonBus events}) : _log = log, _events = events;
final Logger _log;
final DaemonBus _events;
+6 -30
View File
@@ -19,13 +19,7 @@ class LayoutArrangement extends ChangeNotifier {
_focusModeSnapshot = null;
_focusModeSlot = null;
for (final slot in preset.slots) {
_state[slot.slot] = _SlotState(
position: slot.position,
size: slot.defaultSize,
minSize: slot.minSize,
maxSize: slot.maxSize,
visible: slot.visible,
);
_state[slot.slot] = _SlotState(position: slot.position, size: slot.defaultSize, minSize: slot.minSize, maxSize: slot.maxSize, visible: slot.visible);
}
notifyListeners();
}
@@ -131,26 +125,15 @@ class LayoutArrangement extends ChangeNotifier {
void registerSlotsInto(PanelRegistry registry, LayoutPresetContribution preset) {
for (final slot in preset.slots) {
registry.registerSlot(SlotDefinition(
id: slot.slot,
position: slot.position,
defaultSize: slot.defaultSize,
minSize: slot.minSize,
maxSize: slot.maxSize,
));
registry.registerSlot(
SlotDefinition(id: slot.slot, position: slot.position, defaultSize: slot.defaultSize, minSize: slot.minSize, maxSize: slot.maxSize),
);
}
}
}
class _SlotState {
const _SlotState({
required this.position,
this.size,
this.minSize,
this.maxSize,
this.visible = true,
this.collapsed = false,
});
const _SlotState({required this.position, this.size, this.minSize, this.maxSize, this.visible = true, this.collapsed = false});
final SlotPosition position;
final double? size;
@@ -159,14 +142,7 @@ class _SlotState {
final bool visible;
final bool collapsed;
_SlotState copyWith({
SlotPosition? position,
double? size,
double? minSize,
double? maxSize,
bool? visible,
bool? collapsed,
}) {
_SlotState copyWith({SlotPosition? position, double? size, double? minSize, double? maxSize, bool? visible, bool? collapsed}) {
return _SlotState(
position: position ?? this.position,
size: size ?? this.size,
+2 -12
View File
@@ -12,13 +12,7 @@ import 'package:flutter/widgets.dart';
/// coarse step). Exposes a `slider` Semantics node so screen readers
/// announce the current width.
class DragResizeHandle extends StatefulWidget {
const DragResizeHandle({
super.key,
required this.arrangement,
required this.slot,
required this.axis,
this.thickness = DragResizeHandle.defaultThickness,
});
const DragResizeHandle({super.key, required this.arrangement, required this.slot, required this.axis, this.thickness = DragResizeHandle.defaultThickness});
final LayoutArrangement arrangement;
final SlotId slot;
@@ -155,11 +149,7 @@ class _BumpIntent extends Intent {
/// delta = right/down. Context-panel sits on the right edge of the
/// app, so we flip the sign there — right-arrow should *shrink* it,
/// matching how dragging the left-edge handle rightward works.
double bumpedSlotSize({
required SlotId slot,
required double current,
required double rawDelta,
}) {
double bumpedSlotSize({required SlotId slot, required double current, required double rawDelta}) {
final delta = slot == Slots.contextPanel ? -rawDelta : rawDelta;
return current + delta;
}
+10 -38
View File
@@ -11,41 +11,13 @@ import 'package:clide/kernel/src/panels/slot_id.dart';
/// context 420 (drag 220420)
/// statusbar 26 (fixed height strip)
LayoutPresetContribution classicPreset() => const LayoutPresetContribution(
id: 'builtin.default-layout.classic',
displayName: 'Classic',
slots: [
LayoutSlot(
slot: Slots.sidebar,
position: SlotPosition.left,
defaultSize: 400,
minSize: 180,
maxSize: 400,
),
LayoutSlot(
slot: Slots.workspace,
position: SlotPosition.center,
),
LayoutSlot(
slot: Slots.contextPanel,
position: SlotPosition.right,
defaultSize: 420,
minSize: 220,
maxSize: 1000,
),
LayoutSlot(
slot: Slots.dock,
position: SlotPosition.bottom,
defaultSize: 200,
minSize: 100,
maxSize: 600,
visible: false,
),
LayoutSlot(
slot: Slots.statusbar,
position: SlotPosition.bottom,
defaultSize: 26,
minSize: 26,
maxSize: 26,
),
],
);
id: 'builtin.default-layout.classic',
displayName: 'Classic',
slots: [
LayoutSlot(slot: Slots.sidebar, position: SlotPosition.left, defaultSize: 400, minSize: 180, maxSize: 400),
LayoutSlot(slot: Slots.workspace, position: SlotPosition.center),
LayoutSlot(slot: Slots.contextPanel, position: SlotPosition.right, defaultSize: 420, minSize: 220, maxSize: 1000),
LayoutSlot(slot: Slots.dock, position: SlotPosition.bottom, defaultSize: 200, minSize: 100, maxSize: 600, visible: false),
LayoutSlot(slot: Slots.statusbar, position: SlotPosition.bottom, defaultSize: 26, minSize: 26, maxSize: 26),
],
);
+1 -7
View File
@@ -4,13 +4,7 @@ import 'package:flutter/foundation.dart';
@immutable
class SlotDefinition {
const SlotDefinition({
required this.id,
required this.position,
this.defaultSize,
this.minSize,
this.maxSize,
});
const SlotDefinition({required this.id, required this.position, this.defaultSize, this.minSize, this.maxSize});
final SlotId id;
final SlotPosition position;
@@ -20,13 +20,7 @@ List<ViewPane> snapshotViewPanes(PanelRegistry panels, LayoutArrangement arrange
final activeId = panels.activeTabIn(slot.id);
final visible = arrangement.isVisible(slot.id);
for (final tab in panels.tabsFor(slot.id)) {
out.add(ViewPane(
id: tab.id,
slot: slot.id.value,
title: tab.title,
active: tab.id == activeId,
visible: visible,
));
out.add(ViewPane(id: tab.id, slot: slot.id.value, title: tab.title, active: tab.id == activeId, visible: visible));
}
}
return out;
+26 -35
View File
@@ -9,13 +9,7 @@ import 'package:clide/kernel/src/toolchain.dart';
import 'package:flutter/foundation.dart';
class RecentProject {
const RecentProject({
required this.path,
required this.name,
this.branch,
required this.lastOpened,
this.startupSticky = false,
});
const RecentProject({required this.path, required this.name, this.branch, required this.lastOpened, this.startupSticky = false});
final String path;
final String name;
@@ -28,28 +22,28 @@ class RecentProject {
final bool startupSticky;
RecentProject copyWith({bool? startupSticky, DateTime? lastOpened, String? branch}) => RecentProject(
path: path,
name: name,
branch: branch ?? this.branch,
lastOpened: lastOpened ?? this.lastOpened,
startupSticky: startupSticky ?? this.startupSticky,
);
path: path,
name: name,
branch: branch ?? this.branch,
lastOpened: lastOpened ?? this.lastOpened,
startupSticky: startupSticky ?? this.startupSticky,
);
Map<String, dynamic> toJson() => {
'path': path,
'name': name,
'branch': branch,
'lastOpened': lastOpened.toIso8601String(),
if (startupSticky) 'startupSticky': true,
};
'path': path,
'name': name,
'branch': branch,
'lastOpened': lastOpened.toIso8601String(),
if (startupSticky) 'startupSticky': true,
};
factory RecentProject.fromJson(Map<String, dynamic> json) => RecentProject(
path: json['path'] as String? ?? '',
name: json['name'] as String? ?? '',
branch: json['branch'] as String?,
lastOpened: DateTime.tryParse(json['lastOpened'] as String? ?? '') ?? DateTime.now(),
startupSticky: json['startupSticky'] as bool? ?? false,
);
path: json['path'] as String? ?? '',
name: json['name'] as String? ?? '',
branch: json['branch'] as String?,
lastOpened: DateTime.tryParse(json['lastOpened'] as String? ?? '') ?? DateTime.now(),
startupSticky: json['startupSticky'] as bool? ?? false,
);
String get relativePath {
final home = Platform.environment['HOME'] ?? '';
@@ -77,12 +71,12 @@ class ProjectManager extends ChangeNotifier {
required Toolchain toolchain,
Future<void> Function(String path)? onProjectOpen,
Future<String?> Function(String path)? onValidateProject,
}) : _log = log,
_events = events,
_settings = settings,
_toolchain = toolchain,
_onProjectOpen = onProjectOpen,
_onValidateProject = onValidateProject;
}) : _log = log,
_events = events,
_settings = settings,
_toolchain = toolchain,
_onProjectOpen = onProjectOpen,
_onValidateProject = onValidateProject;
final Logger _log;
final DaemonBus _events;
@@ -137,10 +131,7 @@ class ProjectManager extends ChangeNotifier {
// project (T-115).
final wasSticky = isStickyStartup(root);
_recents.removeWhere((r) => r.path == root);
_recents.insert(
0,
RecentProject(path: root, name: name, branch: branch, lastOpened: DateTime.now(), startupSticky: wasSticky),
);
_recents.insert(0, RecentProject(path: root, name: name, branch: branch, lastOpened: DateTime.now(), startupSticky: wasSticky));
if (_recents.length > 10) _recents = _recents.sublist(0, 10);
await _settings.set<String>('app.recentProjects', jsonEncode(_recents.map((r) => r.toJson()).toList()));
+5 -12
View File
@@ -17,11 +17,7 @@ import 'package:clide/kernel/src/events/message_bus.dart';
import 'package:flutter/foundation.dart';
class ReaderNav extends ChangeNotifier {
ReaderNav({
required MessageBus messages,
required this.publisherId,
required this.dataKey,
}) : _messages = messages {
ReaderNav({required MessageBus messages, required this.publisherId, required this.dataKey}) : _messages = messages {
// Retained recorder: every selection for this reader is captured
// here whether or not the reader widget is mounted.
_selectionSub = _messages.subscribe(publisher: publisherId, channel: 'selection').listen((m) {
@@ -132,17 +128,14 @@ class ReaderNavRegistry {
/// `clide status` to surface what the user is reading (T-221, D-6 parity) —
/// viewer files aren't editor buffers, so they don't live in EditorRegistry.
Map<String, String> get currentByReader => {
for (final e in _navs.entries)
if (e.value.current != null) e.key: e.value.current!,
};
for (final e in _navs.entries)
if (e.value.current != null) e.key: e.value.current!,
};
/// The retained [ReaderNav] for [publisherId], created on first use.
/// [dataKey] is the bus-payload key for this reader's entry.
ReaderNav navFor(String publisherId, {required String dataKey}) {
return _navs.putIfAbsent(
publisherId,
() => ReaderNav(messages: _messages, publisherId: publisherId, dataKey: dataKey),
);
return _navs.putIfAbsent(publisherId, () => ReaderNav(messages: _messages, publisherId: publisherId, dataKey: dataKey));
}
void dispose() {
+3 -13
View File
@@ -6,25 +6,15 @@
class SecretsVault {
final Map<String, String> _memory = {};
Future<void> write({
required String extensionId,
required String key,
required String value,
}) async {
Future<void> write({required String extensionId, required String key, required String value}) async {
_memory['$extensionId/$key'] = value;
}
Future<String?> read({
required String extensionId,
required String key,
}) async {
Future<String?> read({required String extensionId, required String key}) async {
return _memory['$extensionId/$key'];
}
Future<void> delete({
required String extensionId,
required String key,
}) async {
Future<void> delete({required String extensionId, required String key}) async {
_memory.remove('$extensionId/$key');
}
+50 -50
View File
@@ -124,28 +124,28 @@ typedef DWasmEngineDelete = void Function(Pointer<TSWasmEngine>);
class TreeSitterLib {
TreeSitterLib._(DynamicLibrary lib)
: parserNew = lib.lookupFunction<_TsParserNew, DTsParserNew>('ts_parser_new'),
parserDelete = lib.lookupFunction<_TsParserDelete, DTsParserDelete>('ts_parser_delete'),
parserSetLanguage = lib.lookupFunction<_TsParserSetLanguage, DTsParserSetLanguage>('ts_parser_set_language'),
parserSetWasmStore = lib.lookupFunction<_TsParserSetWasmStore, DTsParserSetWasmStore>('ts_parser_set_wasm_store'),
parserParseString = lib.lookupFunction<_TsParserParseString, DTsParserParseString>('ts_parser_parse_string'),
treeDelete = lib.lookupFunction<_TsTreeDelete, DTsTreeDelete>('ts_tree_delete'),
treeRootNode = lib.lookupFunction<_TsTreeRootNode, DTsTreeRootNode>('ts_tree_root_node'),
nodeStartByte = lib.lookupFunction<_TsNodeStartByte, DTsNodeStartByte>('ts_node_start_byte'),
nodeEndByte = lib.lookupFunction<_TsNodeEndByte, DTsNodeEndByte>('ts_node_end_byte'),
queryNew = lib.lookupFunction<_TsQueryNew, DTsQueryNew>('ts_query_new'),
queryDelete = lib.lookupFunction<_TsQueryDelete, DTsQueryDelete>('ts_query_delete'),
queryCaptureCount = lib.lookupFunction<_TsQueryCaptureCount, DTsQueryCaptureCount>('ts_query_capture_count'),
queryCaptureNameForId = lib.lookupFunction<_TsQueryCaptureNameForId, DTsQueryCaptureNameForId>('ts_query_capture_name_for_id'),
queryCursorNew = lib.lookupFunction<_TsQueryCursorNew, DTsQueryCursorNew>('ts_query_cursor_new'),
queryCursorDelete = lib.lookupFunction<_TsQueryCursorDelete, DTsQueryCursorDelete>('ts_query_cursor_delete'),
queryCursorExec = lib.lookupFunction<_TsQueryCursorExec, DTsQueryCursorExec>('ts_query_cursor_exec'),
queryCursorNextMatch = lib.lookupFunction<_TsQueryCursorNextMatch, DTsQueryCursorNextMatch>('ts_query_cursor_next_match'),
wasmStoreNew = lib.lookupFunction<_TsWasmStoreNew, DTsWasmStoreNew>('ts_wasm_store_new'),
wasmStoreDelete = lib.lookupFunction<_TsWasmStoreDelete, DTsWasmStoreDelete>('ts_wasm_store_delete'),
wasmStoreLoadLanguage = lib.lookupFunction<_TsWasmStoreLoadLanguage, DTsWasmStoreLoadLanguage>('ts_wasm_store_load_language'),
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
: parserNew = lib.lookupFunction<_TsParserNew, DTsParserNew>('ts_parser_new'),
parserDelete = lib.lookupFunction<_TsParserDelete, DTsParserDelete>('ts_parser_delete'),
parserSetLanguage = lib.lookupFunction<_TsParserSetLanguage, DTsParserSetLanguage>('ts_parser_set_language'),
parserSetWasmStore = lib.lookupFunction<_TsParserSetWasmStore, DTsParserSetWasmStore>('ts_parser_set_wasm_store'),
parserParseString = lib.lookupFunction<_TsParserParseString, DTsParserParseString>('ts_parser_parse_string'),
treeDelete = lib.lookupFunction<_TsTreeDelete, DTsTreeDelete>('ts_tree_delete'),
treeRootNode = lib.lookupFunction<_TsTreeRootNode, DTsTreeRootNode>('ts_tree_root_node'),
nodeStartByte = lib.lookupFunction<_TsNodeStartByte, DTsNodeStartByte>('ts_node_start_byte'),
nodeEndByte = lib.lookupFunction<_TsNodeEndByte, DTsNodeEndByte>('ts_node_end_byte'),
queryNew = lib.lookupFunction<_TsQueryNew, DTsQueryNew>('ts_query_new'),
queryDelete = lib.lookupFunction<_TsQueryDelete, DTsQueryDelete>('ts_query_delete'),
queryCaptureCount = lib.lookupFunction<_TsQueryCaptureCount, DTsQueryCaptureCount>('ts_query_capture_count'),
queryCaptureNameForId = lib.lookupFunction<_TsQueryCaptureNameForId, DTsQueryCaptureNameForId>('ts_query_capture_name_for_id'),
queryCursorNew = lib.lookupFunction<_TsQueryCursorNew, DTsQueryCursorNew>('ts_query_cursor_new'),
queryCursorDelete = lib.lookupFunction<_TsQueryCursorDelete, DTsQueryCursorDelete>('ts_query_cursor_delete'),
queryCursorExec = lib.lookupFunction<_TsQueryCursorExec, DTsQueryCursorExec>('ts_query_cursor_exec'),
queryCursorNextMatch = lib.lookupFunction<_TsQueryCursorNextMatch, DTsQueryCursorNextMatch>('ts_query_cursor_next_match'),
wasmStoreNew = lib.lookupFunction<_TsWasmStoreNew, DTsWasmStoreNew>('ts_wasm_store_new'),
wasmStoreDelete = lib.lookupFunction<_TsWasmStoreDelete, DTsWasmStoreDelete>('ts_wasm_store_delete'),
wasmStoreLoadLanguage = lib.lookupFunction<_TsWasmStoreLoadLanguage, DTsWasmStoreLoadLanguage>('ts_wasm_store_load_language'),
wasmEngineNew = lib.lookupFunction<_WasmEngineNew, DWasmEngineNew>('wasm_engine_new'),
wasmEngineDelete = lib.lookupFunction<_WasmEngineDelete, DWasmEngineDelete>('wasm_engine_delete');
/// Constructs a [TreeSitterLib] from caller-supplied Dart closures. Used by
/// tests to substitute the FFI surface without dlopen'ing the real library;
@@ -176,30 +176,30 @@ class TreeSitterLib {
DTsWasmStoreLoadLanguage? wasmStoreLoadLanguage,
DWasmEngineNew? wasmEngineNew,
DWasmEngineDelete? wasmEngineDelete,
}) : parserNew = parserNew ?? (() => nullptr),
parserDelete = parserDelete ?? ((_) {}),
parserSetLanguage = parserSetLanguage ?? ((_, __) => false),
parserSetWasmStore = parserSetWasmStore ?? ((_, __) {}),
parserParseString = parserParseString ?? ((_, __, ___, ____) => nullptr),
treeDelete = treeDelete ?? ((_) {}),
// Leaks a zeroed TSNode allocation — only hit when the test supplies
// a non-null parserParseString without also supplying treeRootNode.
treeRootNode = treeRootNode ?? ((_) => calloc<TSNode>().ref),
nodeStartByte = nodeStartByte ?? ((_) => 0),
nodeEndByte = nodeEndByte ?? ((_) => 0),
queryNew = queryNew ?? ((_, __, ___, ____, _____) => nullptr),
queryDelete = queryDelete ?? ((_) {}),
queryCaptureCount = queryCaptureCount ?? ((_) => 0),
queryCaptureNameForId = queryCaptureNameForId ?? ((_, __, ___) => nullptr),
queryCursorNew = queryCursorNew ?? (() => nullptr),
queryCursorDelete = queryCursorDelete ?? ((_) {}),
queryCursorExec = queryCursorExec ?? ((_, __, ___) {}),
queryCursorNextMatch = queryCursorNextMatch ?? ((_, __) => false),
wasmStoreNew = wasmStoreNew ?? ((_, __) => nullptr),
wasmStoreDelete = wasmStoreDelete ?? ((_) {}),
wasmStoreLoadLanguage = wasmStoreLoadLanguage ?? ((_, __, ___, ____, _____) => nullptr),
wasmEngineNew = wasmEngineNew ?? (() => nullptr),
wasmEngineDelete = wasmEngineDelete ?? ((_) {});
}) : parserNew = parserNew ?? (() => nullptr),
parserDelete = parserDelete ?? ((_) {}),
parserSetLanguage = parserSetLanguage ?? ((_, _) => false),
parserSetWasmStore = parserSetWasmStore ?? ((_, _) {}),
parserParseString = parserParseString ?? ((_, _, _, _) => nullptr),
treeDelete = treeDelete ?? ((_) {}),
// Leaks a zeroed TSNode allocation — only hit when the test supplies
// a non-null parserParseString without also supplying treeRootNode.
treeRootNode = treeRootNode ?? ((_) => calloc<TSNode>().ref),
nodeStartByte = nodeStartByte ?? ((_) => 0),
nodeEndByte = nodeEndByte ?? ((_) => 0),
queryNew = queryNew ?? ((_, _, _, _, _) => nullptr),
queryDelete = queryDelete ?? ((_) {}),
queryCaptureCount = queryCaptureCount ?? ((_) => 0),
queryCaptureNameForId = queryCaptureNameForId ?? ((_, _, _) => nullptr),
queryCursorNew = queryCursorNew ?? (() => nullptr),
queryCursorDelete = queryCursorDelete ?? ((_) {}),
queryCursorExec = queryCursorExec ?? ((_, _, _) {}),
queryCursorNextMatch = queryCursorNextMatch ?? ((_, _) => false),
wasmStoreNew = wasmStoreNew ?? ((_, _) => nullptr),
wasmStoreDelete = wasmStoreDelete ?? ((_) {}),
wasmStoreLoadLanguage = wasmStoreLoadLanguage ?? ((_, _, _, _, _) => nullptr),
wasmEngineNew = wasmEngineNew ?? (() => nullptr),
wasmEngineDelete = wasmEngineDelete ?? ((_) {});
final DTsParserNew parserNew;
final DTsParserDelete parserDelete;
@@ -256,10 +256,10 @@ class TreeSitterLib {
final libName = Platform.isLinux
? 'libtree-sitter.so'
: Platform.isMacOS
? 'libtree-sitter.dylib'
: Platform.isWindows
? 'tree-sitter.dll'
: null;
? 'libtree-sitter.dylib'
: Platform.isWindows
? 'tree-sitter.dll'
: null;
if (libName == null) {
lastOpenError = 'unsupported platform ${Platform.operatingSystem}';
lastOpenErrorPath = null;
+11 -47
View File
@@ -20,11 +20,7 @@ typedef GrammarBytesLoader = Future<Uint8List> Function(String language);
typedef GrammarQueryLoader = Future<String?> Function(String language);
class SyntaxSpan {
const SyntaxSpan({
required this.start,
required this.end,
required this.role,
});
const SyntaxSpan({required this.start, required this.end, required this.role});
final int start;
final int end;
@@ -39,11 +35,7 @@ class SyntaxResult {
}
class _LoadedGrammar {
_LoadedGrammar({
required this.language,
required this.query,
required this.captureNames,
});
_LoadedGrammar({required this.language, required this.query, required this.captureNames});
final Pointer<Void> language;
final Pointer<TSQuery> query;
@@ -56,13 +48,10 @@ class TreeSitterService {
/// Production constructor: uses the dlopen'd [TreeSitterLib.instance] and
/// the Flutter [rootBundle]. Tests pass [lib] / [grammarBytes] /
/// [grammarQuery] to substitute a fake FFI surface and in-memory assets.
TreeSitterService({
TreeSitterLib? lib,
GrammarBytesLoader? grammarBytes,
GrammarQueryLoader? grammarQuery,
}) : _injectedLib = lib,
_grammarBytes = grammarBytes ?? _defaultGrammarBytes,
_grammarQuery = grammarQuery ?? _defaultGrammarQuery;
TreeSitterService({TreeSitterLib? lib, GrammarBytesLoader? grammarBytes, GrammarQueryLoader? grammarQuery})
: _injectedLib = lib,
_grammarBytes = grammarBytes ?? _defaultGrammarBytes,
_grammarQuery = grammarQuery ?? _defaultGrammarQuery;
final TreeSitterLib? _injectedLib;
final GrammarBytesLoader _grammarBytes;
@@ -142,13 +131,7 @@ class TreeSitterService {
wasmNative.asTypedList(wasmBytes.length).setAll(0, wasmBytes);
final error = calloc<TSWasmError>();
final lang = lib.wasmStoreLoadLanguage(
_store!,
nameNative.cast(),
wasmNative,
wasmBytes.length,
error,
);
final lang = lib.wasmStoreLoadLanguage(_store!, nameNative.cast(), wasmNative, wasmBytes.length, error);
calloc.free(wasmNative);
calloc.free(nameNative);
@@ -174,13 +157,7 @@ class TreeSitterService {
final errorOffset = calloc<Uint32>();
final errorType = calloc<Int32>();
query = lib.queryNew(
lang,
queryNative.cast(),
queryLen,
errorOffset,
errorType,
);
query = lib.queryNew(lang, queryNative.cast(), queryLen, errorOffset, errorType);
calloc.free(queryNative);
calloc.free(errorOffset);
@@ -198,11 +175,7 @@ class TreeSitterService {
}
}
final grammar = _LoadedGrammar(
language: lang,
query: query,
captureNames: captureNames,
);
final grammar = _LoadedGrammar(language: lang, query: query, captureNames: captureNames);
_grammars[language] = grammar;
return grammar;
} catch (_) {
@@ -244,12 +217,7 @@ class TreeSitterService {
// Parse source.
final sourceNative = source.toNativeUtf8();
final sourceLen = utf8.encode(source).length;
final tree = lib.parserParseString(
parser,
nullptr,
sourceNative.cast(),
sourceLen,
);
final tree = lib.parserParseString(parser, nullptr, sourceNative.cast(), sourceLen);
if (tree == nullptr) {
calloc.free(sourceNative);
@@ -270,11 +238,7 @@ class TreeSitterService {
final cap = m.captures[i];
final captureIndex = cap.index;
if (captureIndex < grammar.captureNames.length) {
spans.add(SyntaxSpan(
start: lib.nodeStartByte(cap.node),
end: lib.nodeEndByte(cap.node),
role: grammar.captureNames[captureIndex],
));
spans.add(SyntaxSpan(start: lib.nodeStartByte(cap.node), end: lib.nodeEndByte(cap.node), role: grammar.captureNames[captureIndex]));
}
}
}
+48 -162
View File
@@ -7,12 +7,7 @@ import 'package:flutter/foundation.dart';
/// A foreground/background token pair the a11y contrast suite walks.
@immutable
class ContrastPair {
const ContrastPair({
required this.name,
required this.foreground,
required this.background,
this.largeText = false,
});
const ContrastPair({required this.name, required this.foreground, required this.background, this.largeText = false});
final String name;
final Color foreground;
@@ -49,62 +44,18 @@ double minimumRatio(ContrastPair pair) => pair.largeText ? 3.0 : 4.5;
/// muted text, status chips, syntax tokens, and the focus border lives
/// in [extendedPairs], which only `-hc`/`-cb` variants must pass.
List<ContrastPair> canonicalPairs(SurfaceTokens s) => [
ContrastPair(
name: 'global.text_on_background',
foreground: s.globalForeground,
background: s.globalBackground,
),
ContrastPair(
name: 'panel.header_foreground_on_panel',
foreground: s.panelHeaderForeground,
background: s.panelHeader,
),
ContrastPair(
name: 'sidebar.foreground_on_sidebar',
foreground: s.sidebarForeground,
background: s.sidebarBackground,
),
ContrastPair(
name: 'statusbar.foreground_on_statusbar',
foreground: s.statusBarForeground,
background: s.statusBarBackground,
),
ContrastPair(
name: 'tab.active_text_on_active_bg',
foreground: s.tabActiveForeground,
background: s.tabActive,
),
ContrastPair(
name: 'tab.inactive_text_on_inactive_bg',
foreground: s.tabInactiveForeground,
background: s.tabInactive,
),
ContrastPair(
name: 'button.text_on_button',
foreground: s.buttonForeground,
background: s.buttonBackground,
),
ContrastPair(
name: 'listItem.selected_text_on_selected_bg',
foreground: s.listItemSelectedForeground,
background: s.listItemSelectedBackground,
),
ContrastPair(
name: 'listItem.text_on_list',
foreground: s.listItemForeground,
background: s.listItemBackground,
),
ContrastPair(
name: 'tooltip.text_on_tooltip',
foreground: s.tooltipForeground,
background: s.tooltipBackground,
),
ContrastPair(
name: 'dropdown.text_on_dropdown',
foreground: s.dropdownForeground,
background: s.dropdownBackground,
),
];
ContrastPair(name: 'global.text_on_background', foreground: s.globalForeground, background: s.globalBackground),
ContrastPair(name: 'panel.header_foreground_on_panel', foreground: s.panelHeaderForeground, background: s.panelHeader),
ContrastPair(name: 'sidebar.foreground_on_sidebar', foreground: s.sidebarForeground, background: s.sidebarBackground),
ContrastPair(name: 'statusbar.foreground_on_statusbar', foreground: s.statusBarForeground, background: s.statusBarBackground),
ContrastPair(name: 'tab.active_text_on_active_bg', foreground: s.tabActiveForeground, background: s.tabActive),
ContrastPair(name: 'tab.inactive_text_on_inactive_bg', foreground: s.tabInactiveForeground, background: s.tabInactive),
ContrastPair(name: 'button.text_on_button', foreground: s.buttonForeground, background: s.buttonBackground),
ContrastPair(name: 'listItem.selected_text_on_selected_bg', foreground: s.listItemSelectedForeground, background: s.listItemSelectedBackground),
ContrastPair(name: 'listItem.text_on_list', foreground: s.listItemForeground, background: s.listItemBackground),
ContrastPair(name: 'tooltip.text_on_tooltip', foreground: s.tooltipForeground, background: s.tooltipBackground),
ContrastPair(name: 'dropdown.text_on_dropdown', foreground: s.dropdownForeground, background: s.dropdownBackground),
];
/// Stricter pair set — only the high-contrast (`-hc`) and colour-blind
/// (`-cb`) theme variants must clear it. See D-69. These are the
@@ -112,94 +63,37 @@ List<ContrastPair> canonicalPairs(SurfaceTokens s) => [
/// text, status chip foregrounds, syntax tokens on the code-block
/// surface, and the focus-indicating panel border.
List<ContrastPair> extendedPairs(SurfaceTokens s) => [
ContrastPair(
name: 'global.text_muted_on_background',
foreground: s.globalTextMuted,
background: s.globalBackground,
),
ContrastPair(
name: 'global.text_muted_on_panel',
foreground: s.globalTextMuted,
background: s.panelBackground,
),
ContrastPair(
name: 'status.success_on_statusbar',
foreground: s.statusSuccess,
background: s.statusBarBackground,
),
ContrastPair(
name: 'status.warning_on_statusbar',
foreground: s.statusWarning,
background: s.statusBarBackground,
),
ContrastPair(
name: 'status.error_on_statusbar',
foreground: s.statusError,
background: s.statusBarBackground,
),
ContrastPair(
name: 'status.info_on_statusbar',
foreground: s.statusInfo,
background: s.statusBarBackground,
),
ContrastPair(
name: 'syntax.keyword_on_panel',
foreground: s.syntaxKeyword,
background: s.panelBackground,
),
ContrastPair(
name: 'syntax.type_on_panel',
foreground: s.syntaxType,
background: s.panelBackground,
),
ContrastPair(
name: 'syntax.string_on_panel',
foreground: s.syntaxString,
background: s.panelBackground,
),
ContrastPair(
name: 'syntax.number_on_panel',
foreground: s.syntaxNumber,
background: s.panelBackground,
),
ContrastPair(
name: 'syntax.comment_on_panel',
foreground: s.syntaxComment,
background: s.panelBackground,
),
ContrastPair(
name: 'syntax.method_on_panel',
foreground: s.syntaxMethod,
background: s.panelBackground,
),
ContrastPair(
name: 'syntax.punct_on_panel',
foreground: s.syntaxPunct,
background: s.panelBackground,
),
// WCAG 1.4.11 wants 3:1 for non-text UI components like a focus
// border against the adjacent surface.
ContrastPair(
name: 'panel.active_border_on_background',
foreground: s.panelActiveBorder,
background: s.globalBackground,
largeText: true,
),
// selection.foreground_on_selection is intentionally omitted here.
//
// The `selectionBackground` token defaults to `globalFocus.withAlpha(0x66)`
// — a semi-transparent tint composited onto the real content background at
// runtime. The WCAG compositor in contrastRatio() blends onto neutral grey
// (0x808080) rather than the actual dark panel background, which
// systematically understates the readable contrast for all current bundled
// themes. Adding the pair here would require retuning palettes, which D-69
// forbids for user-contract themes.
//
// Enforcement is deferred to a follow-up ticket: -hc/-cb variants will
// declare an explicit `surface.selectionBackground` override that is
// opaque enough to clear 3:1 against the grey compositor, at which point
// the pair can be added to extendedPairs.
];
ContrastPair(name: 'global.text_muted_on_background', foreground: s.globalTextMuted, background: s.globalBackground),
ContrastPair(name: 'global.text_muted_on_panel', foreground: s.globalTextMuted, background: s.panelBackground),
ContrastPair(name: 'status.success_on_statusbar', foreground: s.statusSuccess, background: s.statusBarBackground),
ContrastPair(name: 'status.warning_on_statusbar', foreground: s.statusWarning, background: s.statusBarBackground),
ContrastPair(name: 'status.error_on_statusbar', foreground: s.statusError, background: s.statusBarBackground),
ContrastPair(name: 'status.info_on_statusbar', foreground: s.statusInfo, background: s.statusBarBackground),
ContrastPair(name: 'syntax.keyword_on_panel', foreground: s.syntaxKeyword, background: s.panelBackground),
ContrastPair(name: 'syntax.type_on_panel', foreground: s.syntaxType, background: s.panelBackground),
ContrastPair(name: 'syntax.string_on_panel', foreground: s.syntaxString, background: s.panelBackground),
ContrastPair(name: 'syntax.number_on_panel', foreground: s.syntaxNumber, background: s.panelBackground),
ContrastPair(name: 'syntax.comment_on_panel', foreground: s.syntaxComment, background: s.panelBackground),
ContrastPair(name: 'syntax.method_on_panel', foreground: s.syntaxMethod, background: s.panelBackground),
ContrastPair(name: 'syntax.punct_on_panel', foreground: s.syntaxPunct, background: s.panelBackground),
// WCAG 1.4.11 wants 3:1 for non-text UI components like a focus
// border against the adjacent surface.
ContrastPair(name: 'panel.active_border_on_background', foreground: s.panelActiveBorder, background: s.globalBackground, largeText: true),
// selection.foreground_on_selection is intentionally omitted here.
//
// The `selectionBackground` token defaults to `globalFocus.withAlpha(0x66)`
// — a semi-transparent tint composited onto the real content background at
// runtime. The WCAG compositor in contrastRatio() blends onto neutral grey
// (0x808080) rather than the actual dark panel background, which
// systematically understates the readable contrast for all current bundled
// themes. Adding the pair here would require retuning palettes, which D-69
// forbids for user-contract themes.
//
// Enforcement is deferred to a follow-up ticket: -hc/-cb variants will
// declare an explicit `surface.selectionBackground` override that is
// opaque enough to clear 3:1 against the grey compositor, at which point
// the pair can be added to extendedPairs.
];
/// Convenience for tests: returns the list of [canonicalPairs] that
/// fail WCAG AA.
@@ -223,18 +117,15 @@ List<ContrastFailure> _failures(List<ContrastPair> pairs) {
@immutable
class ContrastFailure {
const ContrastFailure({
required this.pair,
required this.ratio,
required this.minimum,
});
const ContrastFailure({required this.pair, required this.ratio, required this.minimum});
final ContrastPair pair;
final double ratio;
final double minimum;
@override
String toString() => 'contrast ${pair.name}: ${ratio.toStringAsFixed(2)} < '
String toString() =>
'contrast ${pair.name}: ${ratio.toStringAsFixed(2)} < '
'${minimum.toStringAsFixed(1)}';
}
@@ -244,12 +135,7 @@ Color _composite(Color src, Color dst) {
final a = src.a;
if (a >= 0.999) return src;
double mix(double s, double d) => s * a + d * (1 - a);
return Color.from(
alpha: 1.0,
red: mix(src.r, dst.r),
green: mix(src.g, dst.g),
blue: mix(src.b, dst.b),
);
return Color.from(alpha: 1.0, red: mix(src.r, dst.r), green: mix(src.g, dst.g), blue: mix(src.b, dst.b));
}
double _relativeLuminance(Color c) {
+6 -23
View File
@@ -5,12 +5,7 @@ import 'package:flutter/widgets.dart';
@immutable
class ClideThemeData {
const ClideThemeData({
required this.name,
required this.displayName,
required this.dark,
required this.surface,
});
const ClideThemeData({required this.name, required this.displayName, required this.dark, required this.surface});
final String name;
final String displayName;
@@ -19,12 +14,9 @@ class ClideThemeData {
}
class ThemeController extends ChangeNotifier {
ThemeController({
required List<ThemeDefinition> bundled,
ThemeResolver resolver = const ThemeResolver(),
String? initialName,
}) : _resolver = resolver,
_defs = Map.fromEntries(bundled.map((d) => MapEntry(d.name, d))) {
ThemeController({required List<ThemeDefinition> bundled, ThemeResolver resolver = const ThemeResolver(), String? initialName})
: _resolver = resolver,
_defs = Map.fromEntries(bundled.map((d) => MapEntry(d.name, d))) {
final first = initialName != null && _defs.containsKey(initialName) ? initialName : bundled.first.name;
_currentName = first;
_current = _build(first);
@@ -68,21 +60,12 @@ class ThemeController extends ChangeNotifier {
surfaceOverride: def.surfaceOverride,
extensionOverride: def.extensionOverride,
);
return ClideThemeData(
name: def.name,
displayName: def.displayName,
dark: def.dark,
surface: tokens,
);
return ClideThemeData(name: def.name, displayName: def.displayName, dark: def.dark, surface: tokens);
}
}
class ClideTheme extends InheritedNotifier<ThemeController> {
const ClideTheme({
super.key,
required ThemeController controller,
required super.child,
}) : super(notifier: controller);
const ClideTheme({super.key, required ThemeController controller, required super.child}) : super(notifier: controller);
static ClideThemeData of(BuildContext context) {
final w = context.dependOnInheritedWidgetOfExactType<ClideTheme>();
+1 -4
View File
@@ -70,10 +70,7 @@ class ThemeLoader {
final surface = doc['surface'];
final extension = doc['extension'];
final mergedSurface = <String, String>{
...syntaxSurface,
if (surface is Map) ..._parseRefMap(surface),
};
final mergedSurface = <String, String>{...syntaxSurface, if (surface is Map) ..._parseRefMap(surface)};
return ThemeDefinition(
name: name,
+2 -12
View File
@@ -28,12 +28,7 @@ class ThemeResolver {
final semantic = _buildSemantic(palette, semanticOverride);
final surface = <String, Color>{};
for (final key in TokenKeys.all) {
surface[key] = _resolveSurface(
key: key,
palette: palette,
semantic: semantic,
surfaceOverride: surfaceOverride,
);
surface[key] = _resolveSurface(key: key, palette: palette, semantic: semantic, surfaceOverride: surfaceOverride);
}
// selectionBackground defaults to globalFocus at ~40 % opacity (0x66 alpha)
@@ -149,12 +144,7 @@ class ThemeResolver {
return SemanticRoles(roles);
}
Color _resolveSurface({
required String key,
required Palette palette,
required SemanticRoles semantic,
Map<String, String>? surfaceOverride,
}) {
Color _resolveSurface({required String key, required Palette palette, required SemanticRoles semantic, Map<String, String>? surfaceOverride}) {
final override = surfaceOverride?[key];
if (override != null) {
final resolved = _resolveRef(override, palette, semantic);
+1 -13
View File
@@ -24,17 +24,5 @@ abstract class SemanticKeys {
static const error = 'error';
static const info = 'info';
static const all = <String>[
mainchrome,
calltoaction,
focus,
background,
surface,
text,
textMuted,
success,
warning,
error,
info,
];
static const all = <String>[mainchrome, calltoaction, focus, background, surface, text, textMuted, success, warning, error, info];
}
+7 -17
View File
@@ -13,27 +13,17 @@ enum ToastSeverity { success, warning, error, info }
const String toastChannel = 'toast';
ToastSeverity _severityFromName(Object? name) => switch (name) {
'success' => ToastSeverity.success,
'warning' => ToastSeverity.warning,
'error' => ToastSeverity.error,
_ => ToastSeverity.info,
};
'success' => ToastSeverity.success,
'warning' => ToastSeverity.warning,
'error' => ToastSeverity.error,
_ => ToastSeverity.info,
};
/// Raise a toast by publishing to the MessageBus — the decoupled path:
/// emitters depend only on the bus, never on the [ToastService]. [publisher]
/// is the emitter id (e.g. `builtin.git`), kept for provenance/filtering.
void publishToast(
MessageBus messages,
String publisher,
String message, {
ToastSeverity severity = ToastSeverity.info,
Duration? duration,
}) {
messages.publish(publisher, toastChannel, {
'message': message,
'severity': severity.name,
if (duration != null) 'durationMs': duration.inMilliseconds,
});
void publishToast(MessageBus messages, String publisher, String message, {ToastSeverity severity = ToastSeverity.info, Duration? duration}) {
messages.publish(publisher, toastChannel, {'message': message, 'severity': severity.name, if (duration != null) 'durationMs': duration.inMilliseconds});
}
/// One live toast. Immutable; the [ToastService] owns the list.
+1 -5
View File
@@ -12,11 +12,7 @@ class ToolCheck extends ChangeNotifier {
bool get allOk => pqlOk && tmuxOk && gitOk;
List<String> get errors => [
if (!pqlOk) 'pql not found',
if (!tmuxOk) 'tmux not found',
if (!gitOk) 'git not found',
];
List<String> get errors => [if (!pqlOk) 'pql not found', if (!tmuxOk) 'tmux not found', if (!gitOk) 'git not found'];
/// Workspace root, set by the app at boot. Falls back to cwd.
static String? workspaceRoot;
+1 -5
View File
@@ -44,11 +44,7 @@ class Toolchain extends ChangeNotifier implements ToolchainView {
bool get allOk => _resolved && missing.isEmpty;
@override
List<String> get missing => [
if (_git == null) 'git',
if (_pql == null) 'pql',
if (_tmux == null) 'tmux',
];
List<String> get missing => [if (_git == null) 'git', if (_pql == null) 'pql', if (_tmux == null) 'tmux'];
/// Returns a Future that completes when resolution finishes.
Future<void> waitForResolution() {
+6 -28
View File
@@ -12,13 +12,7 @@ import 'dart:io';
/// Serializable result of tool resolution (crosses isolate boundary).
class ResolvedPaths {
const ResolvedPaths({
this.git,
this.pql,
this.tmux,
this.shell,
this.gitEnv,
});
const ResolvedPaths({this.git, this.pql, this.tmux, this.shell, this.gitEnv});
final String? git;
final String? pql;
@@ -66,11 +60,7 @@ class _StaticToolchain implements ToolchainView {
@override
bool get allOk => missing.isEmpty;
@override
List<String> get missing => [
if (_paths.git == null) 'git',
if (_paths.pql == null) 'pql',
if (_paths.tmux == null) 'tmux',
];
List<String> get missing => [if (_paths.git == null) 'git', if (_paths.pql == null) 'pql', if (_paths.tmux == null) 'tmux'];
}
/// Top-level function for compute/isolate use. Returns a plain-data
@@ -88,10 +78,7 @@ ResolvedPaths resolveToolchainPaths() {
if (dugiteGit != null) {
git = dugiteGit;
final dugiteRoot = File(dugiteGit).parent.parent.path;
gitEnv = {
'GIT_EXEC_PATH': '$dugiteRoot/libexec/git-core',
'GIT_TEMPLATE_DIR': '$dugiteRoot/share/git-core/templates',
};
gitEnv = {'GIT_EXEC_PATH': '$dugiteRoot/libexec/git-core', 'GIT_TEMPLATE_DIR': '$dugiteRoot/share/git-core/templates'};
} else {
git = _findOnPath('git');
}
@@ -145,12 +132,8 @@ String? _firstExisting(List<String> candidates) {
}
/// Build expanded PATH inline — must be self-contained for isolate use.
String _expandedPath() => expandToolPath(
Platform.environment['PATH'] ?? '',
isMac: Platform.isMacOS,
isLinux: Platform.isLinux,
home: Platform.environment['HOME'],
);
String _expandedPath() =>
expandToolPath(Platform.environment['PATH'] ?? '', isMac: Platform.isMacOS, isLinux: Platform.isLinux, home: Platform.environment['HOME']);
/// Pure PATH-expansion logic, extracted so it's testable without touching the
/// process environment.
@@ -164,12 +147,7 @@ String _expandedPath() => expandToolPath(
String expandToolPath(String base, {required bool isMac, required bool isLinux, String? home}) {
if (!isMac && !isLinux) return base;
final h = home ?? '';
final extras = <String>[
if (h.isNotEmpty) '$h/.local/bin',
if (isMac) '/opt/homebrew/bin',
if (isMac) '/opt/homebrew/sbin',
'/usr/local/bin',
];
final extras = <String>[if (h.isNotEmpty) '$h/.local/bin', if (isMac) '/opt/homebrew/bin', if (isMac) '/opt/homebrew/sbin', '/usr/local/bin'];
final existing = base.split(':').toSet();
final missing = extras.where((p) => !existing.contains(p));
if (missing.isEmpty) return base;