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:
@@ -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