feat(markdown): live-sync read-mirror of the open editor buffer (T-36)
D-50 behavior 4: opening a renderable .md in the editor now auto-reveals the context-panel reader, which mirrors the buffer read-only and re-renders as the user types — rather than a one-shot disk read. The extension reveals the tab on editor.opened (renderable only — D-50 behavior 5 leaves non-.md files alone). The viewer owns the mirror: on mount it picks up the active buffer (editor.read, no id), enters mirror mode on editor.opened / active-changed for a renderable file, re-reads the in-memory buffer on editor.edited, and drops the mirror (with its edit affordance back) on a disk load or a switch to a non-renderable buffer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7267,3 +7267,6 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 08:32:16', '2026-06-28 08:32:16.329', '2026-06-28 08:32:16.329', NULL, 'b212ea2e61acbb81bd5d089e97240c97', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'status', 'in_progress', 'in_progress', NULL, '2026-06-28 08:32:36', '2026-06-28 08:32:36.557', '2026-06-28 08:32:36.557', NULL, '09e582bb30f33f9c7330e041067a48b6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FGSQP1YPG70RPQJ4XZDH2T1M', 'status', 'in_progress', 'done', NULL, '2026-06-28 08:53:08', '2026-06-28 08:53:08.149', '2026-06-28 08:53:08.149', NULL, 'fbe0fcce351e196779c912689805075f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'status', 'backlog', 'in_progress', NULL, '2026-06-28 08:55:04', '2026-06-28 08:55:04.660', '2026-06-28 08:55:04.660', NULL, 'e5c075285fc2422545f90d372c175e28', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'status', 'in_progress', 'in_progress', NULL, '2026-06-28 08:55:15', '2026-06-28 08:55:15.737', '2026-06-28 08:55:15.737', NULL, '793c4f2984d01abfb3a52d860e60f6d7', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'status', 'in_progress', 'done', NULL, '2026-06-28 09:08:31', '2026-06-28 09:08:31.757', '2026-06-28 09:08:31.757', NULL, '168bec5a771d23ab7f0d850a8202ef0c', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
|
||||
@@ -9446,3 +9446,18 @@ Considerations:
|
||||
2. The post-/clear respawn also gets it (it spawns through the same path).
|
||||
3. A --resume of an existing session does NOT get re-injected.
|
||||
4. The text is short and does not crowd out clideContextNote.', 'done', 'medium', NULL, NULL, NULL, '2026-06-28 06:15:53.077', '2026-06-28 08:53:08.149', NULL, '1c8842605724aab0461805297ad03ac7', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'story', '06FB0TNQM7RQMN1GKEZ5JQKJT0', 'Live-sync read-mirror: context panel tracks the open editor buffer', 'When an editor is open in edit mode on a renderable doc (.md), the right-hand context panel auto-opens a read-only view that mirrors the buffer and updates live as the user types (D-50 behavior 4). If the editor is on a non-renderable file, no auto-viewer (D-50 behavior 5, already holds).
|
||||
|
||||
Wiring sketch: EditorRegistry already emits editor.opened / editor.active-changed / editor.edited (lib/src/editor/registry.dart:85). The markdown reader (lib/builtin/markdown/src/markdown_viewer.dart) currently pulls content once via files.read with no subscription. The live-sync work is: (1) on editor.opened for a renderable file, auto-activate the markdown reader in the context panel; (2) subscribe the reader to editor.edited and re-render from the in-memory buffer rather than re-reading disk; (3) read-only — no edit affordances in the mirror.
|
||||
|
||||
HISTORY: T-36 originally bundled four D-50 clauses (parse Claude''s output for file references, swap the open viewer, badge the spine when collapsed, live-sync). The give-clide-hands push (T-208) superseded the first three: instead of clide scraping the terminal for references, the agent explicitly drives the reader via `clide ui open markdown <path>` (T-231) and ui.open -> diff (T-233). The spine badge was dropped (the open is now an intentional agent act, not a passive notification). Re-scoped 2026-06-06 to the one UI-owned piece that give-clide-hands did not deliver: the live-sync read-mirror. Re-homed from T-7 (Tier 5 canvas/graph, a mis-parent) to T-259 (interaction model). See the D-50 amendment.', 'in_progress', 'low', NULL, NULL, 'D-50', '2026-04-22 20:34:09', '2026-06-28 08:55:04.660', NULL, '4efae1f339b66f4ee9ff7aa14581867e', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'story', '06FB0TNQM7RQMN1GKEZ5JQKJT0', 'Live-sync read-mirror: context panel tracks the open editor buffer', 'When an editor is open in edit mode on a renderable doc (.md), the right-hand context panel auto-opens a read-only view that mirrors the buffer and updates live as the user types (D-50 behavior 4). If the editor is on a non-renderable file, no auto-viewer (D-50 behavior 5, already holds).
|
||||
|
||||
Wiring sketch: EditorRegistry already emits editor.opened / editor.active-changed / editor.edited (lib/src/editor/registry.dart:85). The markdown reader (lib/builtin/markdown/src/markdown_viewer.dart) currently pulls content once via files.read with no subscription. The live-sync work is: (1) on editor.opened for a renderable file, auto-activate the markdown reader in the context panel; (2) subscribe the reader to editor.edited and re-render from the in-memory buffer rather than re-reading disk; (3) read-only — no edit affordances in the mirror.
|
||||
|
||||
HISTORY: T-36 originally bundled four D-50 clauses (parse Claude''s output for file references, swap the open viewer, badge the spine when collapsed, live-sync). The give-clide-hands push (T-208) superseded the first three: instead of clide scraping the terminal for references, the agent explicitly drives the reader via `clide ui open markdown <path>` (T-231) and ui.open -> diff (T-233). The spine badge was dropped (the open is now an intentional agent act, not a passive notification). Re-scoped 2026-06-06 to the one UI-owned piece that give-clide-hands did not deliver: the live-sync read-mirror. Re-homed from T-7 (Tier 5 canvas/graph, a mis-parent) to T-259 (interaction model). See the D-50 amendment.', 'in_progress', 'low', NULL, NULL, 'D-50', '2026-04-22 20:34:09', '2026-06-28 08:55:15.737', NULL, 'eb473131afb64548a097a45d74f3637c', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'story', '06FB0TNQM7RQMN1GKEZ5JQKJT0', 'Live-sync read-mirror: context panel tracks the open editor buffer', 'When an editor is open in edit mode on a renderable doc (.md), the right-hand context panel auto-opens a read-only view that mirrors the buffer and updates live as the user types (D-50 behavior 4). If the editor is on a non-renderable file, no auto-viewer (D-50 behavior 5, already holds).
|
||||
|
||||
Wiring sketch: EditorRegistry already emits editor.opened / editor.active-changed / editor.edited (lib/src/editor/registry.dart:85). The markdown reader (lib/builtin/markdown/src/markdown_viewer.dart) currently pulls content once via files.read with no subscription. The live-sync work is: (1) on editor.opened for a renderable file, auto-activate the markdown reader in the context panel; (2) subscribe the reader to editor.edited and re-render from the in-memory buffer rather than re-reading disk; (3) read-only — no edit affordances in the mirror.
|
||||
|
||||
HISTORY: T-36 originally bundled four D-50 clauses (parse Claude''s output for file references, swap the open viewer, badge the spine when collapsed, live-sync). The give-clide-hands push (T-208) superseded the first three: instead of clide scraping the terminal for references, the agent explicitly drives the reader via `clide ui open markdown <path>` (T-231) and ui.open -> diff (T-233). The spine badge was dropped (the open is now an intentional agent act, not a passive notification). Re-scoped 2026-06-06 to the one UI-owned piece that give-clide-hands did not deliver: the live-sync read-mirror. Re-homed from T-7 (Tier 5 canvas/graph, a mis-parent) to T-259 (interaction model). See the D-50 amendment.', 'done', 'low', NULL, NULL, 'D-50', '2026-04-22 20:34:09', '2026-06-28 09:08:31.757', NULL, 'cb89bbf9b488b2611acbe49cc3d59a6a', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash);
|
||||
|
||||
@@ -18,6 +18,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
|
||||
### Added
|
||||
|
||||
- **Live-sync markdown read-mirror.** Editing a `.md` in the editor auto-opens a
|
||||
read-only preview in the context panel that mirrors the buffer and re-renders
|
||||
as you type; non-renderable files get no auto-viewer. (T-36, D-50)
|
||||
- **New project flow.** A "New project…" action in the welcome view creates +
|
||||
opens a project, then prompts for the Claude account to bind it to — the
|
||||
per-repo account roadblock fires only for freshly-created projects. (T-488,
|
||||
|
||||
@@ -16,6 +16,7 @@ class MarkdownExtension extends ClideExtension {
|
||||
List<String> get dependsOn => const [];
|
||||
|
||||
StreamSubscription<Message>? _sub;
|
||||
StreamSubscription<DaemonEvent>? _editorSub;
|
||||
|
||||
@override
|
||||
List<ContributionPoint> get contributions => [
|
||||
@@ -38,8 +39,21 @@ class MarkdownExtension extends ClideExtension {
|
||||
if (msg.data['path'] is! String) return;
|
||||
ctx.panels.activateTab(Slots.contextPanel, 'markdown.viewer');
|
||||
});
|
||||
// Live-sync read-mirror (T-36, D-50 behavior 4): opening a renderable .md in
|
||||
// the editor auto-reveals the reader, which then mirrors the buffer live.
|
||||
// Non-renderable files reveal nothing (D-50 behavior 5).
|
||||
_editorSub = ctx.events.on<DaemonEvent>().listen((e) {
|
||||
if (e.subsystem != 'editor' || e.kind != 'editor.opened') return;
|
||||
final path = e.data['path'] as String?;
|
||||
if (path != null && isRenderableMarkdownPath(path)) {
|
||||
ctx.panels.activateTab(Slots.contextPanel, 'markdown.viewer');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deactivate() async => _sub?.cancel();
|
||||
Future<void> deactivate() async {
|
||||
await _sub?.cancel();
|
||||
await _editorSub?.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,13 @@ import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Whether [path] is a markdown file the reader can mirror live (T-36, D-50).
|
||||
/// Non-renderable files get no auto-viewer (D-50 behavior 5).
|
||||
bool isRenderableMarkdownPath(String path) {
|
||||
final p = path.toLowerCase();
|
||||
return p.endsWith('.md') || p.endsWith('.markdown');
|
||||
}
|
||||
|
||||
class MarkdownViewer extends StatefulWidget {
|
||||
const MarkdownViewer({super.key});
|
||||
|
||||
@@ -17,8 +24,15 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
String? _content;
|
||||
String? _error;
|
||||
StreamSubscription<Message>? _selectionSub;
|
||||
StreamSubscription<DaemonEvent>? _editorSub;
|
||||
ReaderNav? _nav;
|
||||
|
||||
/// Live-sync mirror state (T-36, D-50 behavior 4): when [_mirror] is true the
|
||||
/// view is a read-only reflection of the open editor buffer [_mirrorId] and
|
||||
/// re-reads it on every edit, rather than a disk snapshot.
|
||||
bool _mirror = false;
|
||||
String? _mirrorId;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -31,12 +45,68 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
final path = msg.data['path'] as String?;
|
||||
if (path != null) _loadFile(path);
|
||||
});
|
||||
// Live-sync: mirror the editor buffer as it's typed (T-36).
|
||||
_editorSub = kernel.events.on<DaemonEvent>().listen(_onEditorEvent);
|
||||
// The editor may have opened before this viewer mounted (the extension
|
||||
// reveals the tab on editor.opened) — pick up the active buffer now.
|
||||
unawaited(_mirrorActiveIfRenderable(kernel));
|
||||
// Grab the latest entry the nav already holds (a selection that
|
||||
// revealed this tab before we subscribed).
|
||||
final current = _nav!.current;
|
||||
if (current != null) _loadFile(current);
|
||||
}
|
||||
|
||||
/// On mount, mirror the active editor buffer if it's a renderable file —
|
||||
/// `editor.read` with no id resolves to the active buffer.
|
||||
Future<void> _mirrorActiveIfRenderable(KernelServices kernel) async {
|
||||
final resp = await kernel.ipc.request('editor.read', args: const {});
|
||||
if (!mounted || !resp.ok) return;
|
||||
final path = resp.data['path'] as String?;
|
||||
final id = resp.data['id'] as String?;
|
||||
if (path != null && id != null && isRenderableMarkdownPath(path)) {
|
||||
_enterMirror(id, path, resp.data['content'] as String? ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
void _onEditorEvent(DaemonEvent e) {
|
||||
if (e.subsystem != 'editor') return;
|
||||
final id = e.data['id'] as String?;
|
||||
final path = e.data['path'] as String?;
|
||||
switch (e.kind) {
|
||||
case 'editor.opened':
|
||||
if (id != null && path != null && isRenderableMarkdownPath(path)) _enterMirror(id, path, e.data['content'] as String? ?? '');
|
||||
case 'editor.active-changed':
|
||||
// Followed the active buffer: mirror a renderable one, drop the mirror
|
||||
// for a non-renderable / no buffer (D-50 behavior 5 — no auto-viewer).
|
||||
if (id != null && path != null && isRenderableMarkdownPath(path)) {
|
||||
unawaited(_reread(id, path));
|
||||
} else {
|
||||
if (_mirror) setState(() => _mirror = false);
|
||||
}
|
||||
case 'editor.edited':
|
||||
if (_mirror && id != null && id == _mirrorId && _path != null) unawaited(_reread(id, _path!));
|
||||
case 'editor.closed':
|
||||
if (id == _mirrorId && _mirror) setState(() => _mirror = false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-read the in-memory buffer [id] and refresh the mirror (live edits).
|
||||
Future<void> _reread(String id, String path) async {
|
||||
final resp = await ClideKernel.of(context).ipc.request('editor.read', args: {'id': id});
|
||||
if (!mounted || !resp.ok) return;
|
||||
_enterMirror(id, path, resp.data['content'] as String? ?? '');
|
||||
}
|
||||
|
||||
void _enterMirror(String id, String path, String content) {
|
||||
setState(() {
|
||||
_mirror = true;
|
||||
_mirrorId = id;
|
||||
_path = path;
|
||||
_content = content;
|
||||
_error = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _onNavChanged() {
|
||||
if (mounted) setState(() {}); // refresh action-bar button state
|
||||
}
|
||||
@@ -44,11 +114,14 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
@override
|
||||
void dispose() {
|
||||
_selectionSub?.cancel();
|
||||
_editorSub?.cancel();
|
||||
_nav?.removeListener(_onNavChanged);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Fetch + display [path]. History lives in [ReaderNav]; never pushes.
|
||||
/// Fetch + display [path] from disk. An explicit (agent-driven) selection, so
|
||||
/// it leaves mirror mode — the edit affordance returns. History lives in
|
||||
/// [ReaderNav]; never pushes.
|
||||
Future<void> _loadFile(String path) async {
|
||||
final kernel = ClideKernel.of(context);
|
||||
final resp = await kernel.ipc.request('files.read', args: {'path': path});
|
||||
@@ -56,6 +129,7 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
if (resp.ok) {
|
||||
kernel.messages.publish('builtin.markdown', 'focus', {'path': path});
|
||||
setState(() {
|
||||
_mirror = false;
|
||||
_path = path;
|
||||
_content = resp.data['content'] as String? ?? '';
|
||||
_error = null;
|
||||
@@ -121,7 +195,9 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
onBack: (_nav?.canGoBack ?? false) ? _onBack : null,
|
||||
onForward: (_nav?.canGoForward ?? false) ? _onForward : null,
|
||||
onJumpToPin: (_nav?.hasPinned ?? false) ? _onJumpToPin : null,
|
||||
onEdit: _path != null ? _onEdit : null,
|
||||
// Read-only while mirroring the live buffer (T-36): the file is already
|
||||
// open in the editor, so no edit affordance here.
|
||||
onEdit: (!_mirror && _path != null) ? _onEdit : null,
|
||||
),
|
||||
],
|
||||
child: SingleChildScrollView(
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/// T-36 / D-50 behavior 4+5: MarkdownExtension reveals the context-panel reader
|
||||
/// when a renderable .md opens in the editor, and leaves a non-renderable file
|
||||
/// alone. (The viewer then mirrors the buffer live — see markdown_viewer_test.)
|
||||
library;
|
||||
|
||||
import 'package:clide/builtin/markdown/src/extension.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/kernel_fixture.dart';
|
||||
|
||||
void main() {
|
||||
late KernelFixture f;
|
||||
|
||||
setUp(() async {
|
||||
f = await KernelFixture.create();
|
||||
f.services.extensions.register(MarkdownExtension());
|
||||
await f.services.extensions.activate('builtin.markdown');
|
||||
});
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
void open(String path) {
|
||||
f.services.events.emit(DaemonEvent(subsystem: 'editor', kind: 'editor.opened', data: {'id': 'b1', 'path': path}, ts: DateTime.now().toUtc()));
|
||||
}
|
||||
|
||||
test('contributes the markdown.viewer context-panel tab', () {
|
||||
expect(f.services.panels.tabsFor(Slots.contextPanel).any((t) => t.id == 'markdown.viewer'), isTrue);
|
||||
});
|
||||
|
||||
test('opening a renderable .md reveals the markdown viewer', () async {
|
||||
// markdown.viewer is the default contextPanel tab; flip to a sentinel first
|
||||
// so the reveal is observable.
|
||||
f.services.panels.activateTab(Slots.contextPanel, 'sentinel');
|
||||
open('docs/notes.md');
|
||||
await pumpEventQueue();
|
||||
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'markdown.viewer');
|
||||
});
|
||||
|
||||
test('opening a non-renderable file does not reveal it (D-50 behavior 5)', () async {
|
||||
f.services.panels.activateTab(Slots.contextPanel, 'sentinel');
|
||||
open('lib/main.dart');
|
||||
await pumpEventQueue();
|
||||
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'sentinel');
|
||||
});
|
||||
|
||||
test('after deactivate, editor.opened no longer reveals the viewer', () async {
|
||||
await f.services.extensions.deactivate('builtin.markdown');
|
||||
f.services.panels.activateTab(Slots.contextPanel, 'sentinel');
|
||||
open('notes.md');
|
||||
await pumpEventQueue();
|
||||
expect(f.services.panels.activeTabIn(Slots.contextPanel), 'sentinel');
|
||||
});
|
||||
}
|
||||
@@ -391,4 +391,44 @@ void main() {
|
||||
expect(editorOpenArgs.first['path'], path);
|
||||
});
|
||||
});
|
||||
|
||||
// T-36 / D-50 behavior 4: live-sync read-mirror of the open editor buffer.
|
||||
group('MarkdownViewer — live-sync mirror (T-36)', () {
|
||||
DaemonEvent opened(String id, String path, String content) =>
|
||||
DaemonEvent(subsystem: 'editor', kind: 'editor.opened', data: {'id': id, 'path': path, 'content': content}, ts: DateTime.now().toUtc());
|
||||
DaemonEvent edited(String id) =>
|
||||
DaemonEvent(subsystem: 'editor', kind: 'editor.edited', data: {'id': id, 'kind': 'replace', 'length': 0}, ts: DateTime.now().toUtc());
|
||||
|
||||
bool editVisible() => find.byWidgetPredicate((w) => w is Semantics && w.properties.label == 'Edit in editor').evaluate().isNotEmpty;
|
||||
|
||||
testWidgets('mirrors the buffer on editor.opened, and the mirror is read-only', (tester) async {
|
||||
await pumpView(tester, f);
|
||||
f.services.events.emit(opened('b1', 'notes.md', '# Live mirror\n'));
|
||||
await pumpAsync(tester);
|
||||
expect(find.textContaining('Live mirror'), findsWidgets);
|
||||
expect(editVisible(), isFalse, reason: 'no edit affordance while mirroring the live buffer');
|
||||
});
|
||||
|
||||
testWidgets('re-reads the buffer on editor.edited and re-renders', (tester) async {
|
||||
var content = '# First\n';
|
||||
f.ipc.stub('editor.read', (args) async => _ok({'id': 'b1', 'path': 'notes.md', 'content': content}));
|
||||
await pumpView(tester, f);
|
||||
f.services.events.emit(opened('b1', 'notes.md', content));
|
||||
await pumpAsync(tester);
|
||||
expect(find.textContaining('First'), findsWidgets);
|
||||
|
||||
content = '# Second\n'; // the user typed in the editor
|
||||
f.services.events.emit(edited('b1'));
|
||||
await pumpAsync(tester);
|
||||
expect(find.textContaining('Second'), findsWidgets);
|
||||
expect(find.textContaining('First'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('a non-renderable editor.opened does not mirror (D-50 behavior 5)', (tester) async {
|
||||
await pumpView(tester, f);
|
||||
f.services.events.emit(opened('b9', 'main.py', 'print(1)'));
|
||||
await pumpAsync(tester);
|
||||
expect(find.textContaining('print'), findsNothing, reason: 'non-renderable file gets no auto-mirror');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user