fix decision first-click + editor reveal via retained reader nav
Two reveal-on-open bugs: Decisions opened only on the second click (T-196): the detail view subscribed in didChangeDependencies, which runs after the tab is revealed, so the broadcast 'selection' that triggered the reveal was already gone. Hoist the back/forward history out of per-view State into a retained per-reader ReaderNav (kernel ChangeNotifier in a ReaderNavRegistry, D-81). The nav records selections, emits 'load' (the single channel readers display from), and survives mount/unmount — the reader grabs nav.current on mount, so the first selection lands. Both the markdown and decisions readers move to this model; the per-view ReaderHistoryMixin and the markdown post-frame forward hack are gone. The editor pane never opened (T-197): EditorExtension contributed a workspace tab but nothing activated it on editor.open. Add an activate() that reveals the tab on editor.opened / editor.active-changed; the view's hydrate() pulls the active buffer on mount. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import 'package:clide/extension/src/extension.dart';
|
||||
import 'package:clide/kernel/src/clipboard.dart';
|
||||
import 'package:clide/kernel/src/commands/keybindings.dart';
|
||||
import 'package:clide/kernel/src/commands/palette.dart';
|
||||
import 'package:clide/kernel/src/reader_nav.dart';
|
||||
import 'package:clide/kernel/src/commands/registry.dart';
|
||||
import 'package:clide/kernel/src/dialog.dart';
|
||||
import 'package:clide/kernel/src/events/bus.dart';
|
||||
@@ -40,6 +41,7 @@ class ExtensionManager extends ChangeNotifier {
|
||||
required this.arrangement,
|
||||
required this.commands,
|
||||
required this.palette,
|
||||
required this.readerNav,
|
||||
required this.keybindings,
|
||||
required this.keymap,
|
||||
required this.clipboard,
|
||||
@@ -65,6 +67,7 @@ class ExtensionManager extends ChangeNotifier {
|
||||
final LayoutArrangement arrangement;
|
||||
final CommandRegistry commands;
|
||||
final PaletteController palette;
|
||||
final ReaderNavRegistry readerNav;
|
||||
final KeybindingResolver keybindings;
|
||||
final KeymapService keymap;
|
||||
final ClideClipboard clipboard;
|
||||
@@ -289,6 +292,8 @@ class _ExtensionContext implements ClideExtensionContext {
|
||||
@override
|
||||
PaletteController get palette => manager.palette;
|
||||
@override
|
||||
ReaderNavRegistry get readerNav => manager.readerNav;
|
||||
@override
|
||||
ClideClipboard get clipboard => manager.clipboard;
|
||||
@override
|
||||
FileServices get files => manager.files;
|
||||
|
||||
@@ -24,6 +24,7 @@ import 'package:clide/kernel/src/panels/arrangement.dart';
|
||||
import 'package:clide/kernel/src/panels/registry.dart';
|
||||
import 'package:clide/kernel/src/project.dart';
|
||||
import 'package:clide/kernel/src/quick_open.dart';
|
||||
import 'package:clide/kernel/src/reader_nav.dart';
|
||||
import 'package:clide/kernel/src/recent_files.dart';
|
||||
import 'package:clide/kernel/src/scheduler.dart';
|
||||
import 'package:clide/kernel/src/secrets.dart';
|
||||
@@ -53,6 +54,7 @@ class KernelServices {
|
||||
required this.palette,
|
||||
required this.quickOpen,
|
||||
required this.recentFiles,
|
||||
required this.readerNav,
|
||||
required this.keybindings,
|
||||
required this.clipboard,
|
||||
required this.files,
|
||||
@@ -85,6 +87,7 @@ class KernelServices {
|
||||
final PaletteController palette;
|
||||
final QuickOpenController quickOpen;
|
||||
final RecentFilesService recentFiles;
|
||||
final ReaderNavRegistry readerNav;
|
||||
final KeybindingResolver keybindings;
|
||||
final ClideClipboard clipboard;
|
||||
final FileServices files;
|
||||
@@ -148,6 +151,7 @@ class KernelServices {
|
||||
final palette = PaletteController(commands);
|
||||
final recentFiles = RecentFilesService();
|
||||
final quickOpen = QuickOpenController(recentPaths: () => recentFiles.paths);
|
||||
final readerNav = ReaderNavRegistry(messages);
|
||||
final clipboard = ClideClipboard();
|
||||
final files = FileServices(events);
|
||||
final notify = Notifications();
|
||||
@@ -196,6 +200,7 @@ class KernelServices {
|
||||
arrangement: arrangement,
|
||||
commands: commands,
|
||||
palette: palette,
|
||||
readerNav: readerNav,
|
||||
keybindings: keybindings,
|
||||
keymap: keymap,
|
||||
clipboard: clipboard,
|
||||
@@ -229,6 +234,7 @@ class KernelServices {
|
||||
palette: palette,
|
||||
quickOpen: quickOpen,
|
||||
recentFiles: recentFiles,
|
||||
readerNav: readerNav,
|
||||
keybindings: keybindings,
|
||||
clipboard: clipboard,
|
||||
files: files,
|
||||
@@ -260,6 +266,7 @@ class KernelServices {
|
||||
palette.dispose();
|
||||
quickOpen.dispose();
|
||||
recentFiles.dispose();
|
||||
readerNav.dispose();
|
||||
i18n.dispose();
|
||||
notify.dispose();
|
||||
dialog.dispose();
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
/// Retained back/forward navigation history for the right-pane readers
|
||||
/// (markdown, decisions — T-196).
|
||||
///
|
||||
/// The history is the single source of truth for "what the reader is
|
||||
/// showing", and it OUTLIVES the reader widget — so a reader that mounts
|
||||
/// after a selection (its tab was just revealed) grabs [ReaderNav.current]
|
||||
/// instead of missing a broadcast it subscribed to too late. The bus
|
||||
/// stays a dumb pipe: a [ReaderNav] subscribes to its reader's
|
||||
/// `selection` channel, records the entry, and (re-)emits `load` — which
|
||||
/// is the single channel a reader loads from. Back/forward/pin navigation
|
||||
/// re-emits `load` the same way, so every load flows through one path.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
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 {
|
||||
// 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) {
|
||||
final entry = m.data[dataKey];
|
||||
if (entry is String) open(entry);
|
||||
});
|
||||
}
|
||||
|
||||
final MessageBus _messages;
|
||||
|
||||
/// The reader's publisher id, e.g. `builtin.decisions` / `builtin.markdown`.
|
||||
final String publisherId;
|
||||
|
||||
/// The key under which the entry travels in the bus payload (`id` for
|
||||
/// decisions, `path` for markdown).
|
||||
final String dataKey;
|
||||
|
||||
StreamSubscription<Message>? _selectionSub;
|
||||
|
||||
final List<String> _stack = [];
|
||||
int _index = -1;
|
||||
int? _pinnedIndex;
|
||||
|
||||
String? get current => _index >= 0 && _index < _stack.length ? _stack[_index] : null;
|
||||
bool get canGoBack => _index > 0;
|
||||
bool get canGoForward => _index < _stack.length - 1;
|
||||
bool get hasPinned => _pinnedIndex != null && _pinnedIndex! < _stack.length;
|
||||
|
||||
/// External navigation: record [entry] (browser semantics — truncates
|
||||
/// forward history) and emit a load. A repeat of the current entry
|
||||
/// re-emits the load without pushing a duplicate.
|
||||
void open(String entry) {
|
||||
if (current == entry) {
|
||||
_emit(entry);
|
||||
return;
|
||||
}
|
||||
if (_index < _stack.length - 1) {
|
||||
_stack.removeRange(_index + 1, _stack.length);
|
||||
}
|
||||
_stack.add(entry);
|
||||
_index = _stack.length - 1;
|
||||
_emit(entry);
|
||||
}
|
||||
|
||||
/// Step back and re-emit the now-current entry. No-op at the start.
|
||||
void back() {
|
||||
if (!canGoBack) return;
|
||||
_index--;
|
||||
_emit(current!);
|
||||
}
|
||||
|
||||
/// Step forward and re-emit the now-current entry. No-op at the end.
|
||||
void forward() {
|
||||
if (!canGoForward) return;
|
||||
_index++;
|
||||
_emit(current!);
|
||||
}
|
||||
|
||||
/// Pin the current entry (one slot; a later pin replaces it).
|
||||
void pin() {
|
||||
if (_index < 0) return;
|
||||
_pinnedIndex = _index;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Jump to the pinned entry and re-emit it. No-op when nothing is pinned.
|
||||
void jumpToPin() {
|
||||
final p = _pinnedIndex;
|
||||
if (p == null || p >= _stack.length) return;
|
||||
_index = p;
|
||||
_emit(current!);
|
||||
}
|
||||
|
||||
void _emit(String entry) {
|
||||
_messages.publish(publisherId, 'load', {dataKey: entry});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_selectionSub?.cancel();
|
||||
_selectionSub = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// Lazily creates and retains one [ReaderNav] per reader id, so the
|
||||
/// history persists across reader widget mount/unmount.
|
||||
class ReaderNavRegistry {
|
||||
ReaderNavRegistry(this._messages);
|
||||
|
||||
final MessageBus _messages;
|
||||
final Map<String, ReaderNav> _navs = {};
|
||||
|
||||
/// 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),
|
||||
);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final n in _navs.values) {
|
||||
n.dispose();
|
||||
}
|
||||
_navs.clear();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user