Makes the canvas foundation (parser/renderer/view) reachable. The extension owns an app-scoped MultitabController (the diff/T-233 pattern) so open documents survive the pane being rebuilt; each document is a real sub-tab per the refinement decision, kept alive across switches. Routing goes through the existing seams instead of the dead TabContribution.fileGlobs field: openWorkspaceFile gains a .canvas branch mirroring .md, and ui.open gains a canvas reader for D-6 parity (clide ui open canvas <path>). Also corrects the json_canvas doc header that claimed SVG-lowering — the interactive pane paints the model directly (D-103 live-widget exception). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.3 KiB
Dart
99 lines
3.3 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:clide/builtin/canvas/src/canvas_pane_host.dart';
|
|
import 'package:clide/extension/extension.dart';
|
|
import 'package:clide/kernel/kernel.dart';
|
|
import 'package:clide/widgets/widgets.dart';
|
|
|
|
/// Tier-5 interactive `.canvas` pane (T-322). Renders Obsidian JSONCanvas
|
|
/// documents in a workspace tab; each open document is a real sub-tab
|
|
/// (MultitabPane). Opens route in as `selection` messages — from
|
|
/// `openWorkspaceFile` (file tree, quick-open) and from
|
|
/// `clide ui open canvas [path]` (D-6 parity, the diff/T-233 pattern).
|
|
class CanvasExtension extends ClideExtension {
|
|
@override
|
|
String get id => 'builtin.canvas';
|
|
@override
|
|
String get title => 'Canvas';
|
|
@override
|
|
String get version => '0.1.0';
|
|
@override
|
|
List<String> get dependsOn => const [];
|
|
|
|
/// App-scoped so open documents survive the pane view being (re)built
|
|
/// while another workspace tab is active. Built in [activate]; the
|
|
/// contribution's build closure reads the field at widget-build time.
|
|
MultitabController<String>? _tabs;
|
|
StreamSubscription<Message>? _selectionSub;
|
|
StreamSubscription<ProjectOpened>? _projectSub;
|
|
String? _projectRoot;
|
|
|
|
@override
|
|
List<ContributionPoint> get contributions => [
|
|
TabContribution(
|
|
id: 'canvas.view',
|
|
slot: Slots.workspace,
|
|
title: 'Canvas',
|
|
titleKey: 'tab.title',
|
|
i18nNamespace: id,
|
|
priority: -60, // below the readers' home surfaces, near diff (-70)
|
|
build: (_) => CanvasPaneHost(tabs: _tabs),
|
|
),
|
|
];
|
|
|
|
@override
|
|
Future<void> activate(ClideExtensionContext ctx) async {
|
|
_tabs = MultitabController<String>();
|
|
_selectionSub = ctx.messages.subscribe(publisher: id, channel: 'selection').listen((msg) {
|
|
final path = msg.data['path'];
|
|
if (path is! String || path.isEmpty) return;
|
|
openPath(path);
|
|
ctx.panels.activateTab(Slots.workspace, 'canvas.view');
|
|
});
|
|
_projectSub = ctx.events.on<ProjectOpened>().listen(_onProjectChanged);
|
|
}
|
|
|
|
/// Focus the sub-tab for [path], opening one when the document isn't
|
|
/// open yet. Tab id is the path itself — one tab per document.
|
|
void openPath(String path) {
|
|
final tabs = _tabs;
|
|
if (tabs == null) return;
|
|
if (tabs.entries.any((e) => e.id == path)) {
|
|
tabs.activate(path);
|
|
} else {
|
|
tabs.add(MultitabEntry<String>(id: path, title: _basename(path), payload: path));
|
|
}
|
|
}
|
|
|
|
/// Paths of the open documents, oldest-first.
|
|
List<String> get openPaths => _tabs?.entries.map((e) => e.id).toList() ?? const [];
|
|
|
|
/// Drop every open document when the workspace switches in place
|
|
/// (T-269): the old repo's paths don't resolve in the new one.
|
|
void _onProjectChanged(ProjectOpened e) {
|
|
final prev = _projectRoot;
|
|
_projectRoot = e.path;
|
|
if (prev == null || prev == e.path) return;
|
|
final tabs = _tabs;
|
|
if (tabs == null) return;
|
|
for (final id in tabs.entries.map((x) => x.id).toList()) {
|
|
tabs.remove(id);
|
|
}
|
|
}
|
|
|
|
static String _basename(String path) {
|
|
final i = path.lastIndexOf('/');
|
|
return i < 0 ? path : path.substring(i + 1);
|
|
}
|
|
|
|
@override
|
|
Future<void> deactivate() async {
|
|
await _selectionSub?.cancel();
|
|
_selectionSub = null;
|
|
await _projectSub?.cancel();
|
|
_projectSub = null;
|
|
_tabs?.dispose();
|
|
_tabs = null;
|
|
}
|
|
}
|