Complete three overdue cleanups discovered during macOS health check: D-56 daemon dissolution: delete bin/clide.dart, DaemonServer, and orphaned tests (test/cli/, subprocess_test, in_process_test). Update stale "clide --daemon" references in i18n catalogs, error messages, editor_commands, CI scripts, and decision records. ptyc retirement: delete ptyc/ source tree, PtySession, scm_rights. Remove from Toolchain resolution, ToolCheck gate, backend serialization, testmode harness, Makefile, CI, and sandbox entitlements. PTY spawning uses NativePty (Dart FFI forkpty) since the terminal was absorbed in-tree. D-5 amended. Golden tests: wire the existing but never-applied clideGoldenConfig via flutter_test_config.dart. Disable CI goldens (Skia anti-aliasing differs between macOS/Linux even with Ahem). Keep platform-keyed goldens only — goldens/linux/ and goldens/macos/ each run on their own OS. Test suite: 826 pass, 0 fail on macOS (was 829 pass, 11 fail). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
183 lines
6.5 KiB
Dart
183 lines
6.5 KiB
Dart
/// Registers `editor.*` command handlers on a [DaemonDispatcher].
|
|
///
|
|
/// Verb list matches CLAUDE.md's tier-2 surface:
|
|
/// editor.open editor.active editor.activate editor.insert
|
|
/// editor.replace-selection editor.save editor.close editor.list
|
|
/// editor.read editor.set-selection editor.set-content
|
|
///
|
|
/// Single-word CLI shortcuts (`clide open`, `clide active`, …) map
|
|
/// one-to-one onto these via the IPC dispatch layer.
|
|
library;
|
|
|
|
import 'dart:io' show FileSystemException;
|
|
|
|
import '../editor/registry.dart';
|
|
import '../ipc/envelope.dart';
|
|
import '../ipc/errno_mapping.dart';
|
|
import '../ipc/schema_v1.dart';
|
|
import 'dispatcher.dart';
|
|
|
|
export '../editor/buffer.dart' show Selection;
|
|
|
|
void registerEditorCommands(DaemonDispatcher d, EditorRegistry registry) {
|
|
d.register('editor.open', (req) => _open(req, registry));
|
|
d.register('editor.active', (req) => _active(req, registry));
|
|
d.register('editor.activate', (req) => _activate(req, registry));
|
|
d.register('editor.list', (req) => _list(req, registry));
|
|
d.register('editor.read', (req) => _read(req, registry));
|
|
d.register('editor.insert', (req) => _insert(req, registry));
|
|
d.register('editor.replace-selection', (req) => _replace(req, registry));
|
|
d.register('editor.set-selection', (req) => _setSelection(req, registry));
|
|
d.register('editor.set-content', (req) => _setContent(req, registry));
|
|
d.register('editor.save', (req) => _save(req, registry));
|
|
d.register('editor.close', (req) => _close(req, registry));
|
|
}
|
|
|
|
IpcResponse _userErr(String id, String msg, {String? hint}) => IpcResponse.err(
|
|
id: id,
|
|
error: IpcError(
|
|
code: IpcExitCode.userError,
|
|
kind: IpcErrorKind.userError,
|
|
message: msg,
|
|
hint: hint,
|
|
),
|
|
);
|
|
|
|
IpcResponse _notFound(String id, String msg) => IpcResponse.err(
|
|
id: id,
|
|
error: IpcError(
|
|
code: IpcExitCode.notFound,
|
|
kind: IpcErrorKind.notFound,
|
|
message: msg,
|
|
),
|
|
);
|
|
|
|
String? _resolveId(IpcRequest req, EditorRegistry r) {
|
|
final id = req.args['id'] as String?;
|
|
if (id != null) return id;
|
|
// CLI shortcut: omitting `id` means the active buffer.
|
|
return r.active?.id;
|
|
}
|
|
|
|
Future<IpcResponse> _open(IpcRequest req, EditorRegistry r) async {
|
|
final path = req.args['path'] as String?;
|
|
if (path == null || path.isEmpty) {
|
|
return _userErr(req.id, 'path is required');
|
|
}
|
|
try {
|
|
final buf = await r.open(path);
|
|
return IpcResponse.ok(id: req.id, data: buf.toJson());
|
|
} on FileSystemException catch (e) {
|
|
final errno = e.osError?.errorCode;
|
|
if (errno != null) {
|
|
return IpcResponse.err(
|
|
id: req.id,
|
|
error: errnoToIpcError(errno: errno, op: 'editor.open', target: path),
|
|
);
|
|
}
|
|
return IpcResponse.err(
|
|
id: req.id,
|
|
error: IpcError(
|
|
code: IpcExitCode.toolError,
|
|
kind: IpcErrorKind.toolError,
|
|
message: 'editor.open failed: ${e.message}',
|
|
),
|
|
);
|
|
} catch (e) {
|
|
return IpcResponse.err(
|
|
id: req.id,
|
|
error: IpcError(
|
|
code: IpcExitCode.toolError,
|
|
kind: IpcErrorKind.toolError,
|
|
message: 'editor.open failed: $e',
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<IpcResponse> _active(IpcRequest req, EditorRegistry r) async {
|
|
final buf = r.active;
|
|
if (buf == null) {
|
|
return IpcResponse.ok(id: req.id, data: const {'active': null});
|
|
}
|
|
return IpcResponse.ok(id: req.id, data: {'active': buf.toJson()});
|
|
}
|
|
|
|
Future<IpcResponse> _activate(IpcRequest req, EditorRegistry r) async {
|
|
final id = req.args['id'] as String?;
|
|
if (id == null) return _userErr(req.id, 'id is required');
|
|
if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id');
|
|
r.activate(id);
|
|
return IpcResponse.ok(id: req.id, data: {'active': id});
|
|
}
|
|
|
|
Future<IpcResponse> _list(IpcRequest req, EditorRegistry r) async {
|
|
return IpcResponse.ok(
|
|
id: req.id,
|
|
data: {
|
|
'buffers': [for (final b in r.buffers) b.toJson()]
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<IpcResponse> _read(IpcRequest req, EditorRegistry r) async {
|
|
final id = _resolveId(req, r);
|
|
if (id == null) return _notFound(req.id, 'no active buffer');
|
|
final buf = r.get(id);
|
|
if (buf == null) return _notFound(req.id, 'no such buffer: $id');
|
|
return IpcResponse.ok(id: req.id, data: buf.toFullJson());
|
|
}
|
|
|
|
Future<IpcResponse> _insert(IpcRequest req, EditorRegistry r) async {
|
|
final id = _resolveId(req, r);
|
|
if (id == null) return _notFound(req.id, 'no active buffer');
|
|
if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id');
|
|
final text = EditorRegistry.contentFromArgs(req.args);
|
|
r.insert(id, text);
|
|
return IpcResponse.ok(id: req.id, data: {'id': id, 'inserted': text.length});
|
|
}
|
|
|
|
Future<IpcResponse> _replace(IpcRequest req, EditorRegistry r) async {
|
|
final id = _resolveId(req, r);
|
|
if (id == null) return _notFound(req.id, 'no active buffer');
|
|
if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id');
|
|
final text = EditorRegistry.contentFromArgs(req.args);
|
|
r.replaceSelection(id, text);
|
|
return IpcResponse.ok(id: req.id, data: {'id': id, 'length': text.length});
|
|
}
|
|
|
|
Future<IpcResponse> _setSelection(IpcRequest req, EditorRegistry r) async {
|
|
final id = _resolveId(req, r);
|
|
if (id == null) return _notFound(req.id, 'no active buffer');
|
|
if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id');
|
|
final sel = EditorRegistry.selectionFromArgs(req.args['selection']);
|
|
r.setSelection(id, sel);
|
|
return IpcResponse.ok(id: req.id, data: {'id': id});
|
|
}
|
|
|
|
Future<IpcResponse> _setContent(IpcRequest req, EditorRegistry r) async {
|
|
final id = _resolveId(req, r);
|
|
if (id == null) return _notFound(req.id, 'no active buffer');
|
|
if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id');
|
|
final content = EditorRegistry.contentFromArgs(req.args);
|
|
final sel = req.args['selection'] == null ? null : EditorRegistry.selectionFromArgs(req.args['selection']);
|
|
r.setContent(id, content, selection: sel);
|
|
return IpcResponse.ok(id: req.id, data: {'id': id, 'length': content.length});
|
|
}
|
|
|
|
Future<IpcResponse> _save(IpcRequest req, EditorRegistry r) async {
|
|
final id = _resolveId(req, r);
|
|
if (id == null) return _notFound(req.id, 'no active buffer');
|
|
final ok = await r.save(id);
|
|
if (!ok) return _notFound(req.id, 'no such buffer: $id');
|
|
return IpcResponse.ok(id: req.id, data: {'id': id, 'saved': true});
|
|
}
|
|
|
|
Future<IpcResponse> _close(IpcRequest req, EditorRegistry r) async {
|
|
final id = req.args['id'] as String?;
|
|
if (id == null) return _userErr(req.id, 'id is required');
|
|
if (r.get(id) == null) return _notFound(req.id, 'no such buffer: $id');
|
|
r.close(id);
|
|
return IpcResponse.ok(id: req.id, data: {'id': id});
|
|
}
|