Compare commits
4
Commits
2f6c11e9a6
...
2f7f797288
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f7f797288 | ||
|
|
bfd304a3f9 | ||
|
|
47c2def656 | ||
|
|
b236ec439f |
+15
-1
@@ -84,6 +84,13 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
`clideFontBadge` (11) for sidebar metadata and status badges.
|
||||
`clideLineHeight` (1.25) applied app-wide via `DefaultTextStyle`.
|
||||
|
||||
- `files.read` IPC command for reading file content by path.
|
||||
|
||||
- pql sidebar restructured: Search tab (PQL DSL query) is the
|
||||
default left tab; Markdown tab (filtered to `.md` files) on the
|
||||
right. Clicking a markdown file opens it in the context panel
|
||||
markdown viewer with bidirectional focus highlighting.
|
||||
|
||||
- Graph view in context panel — lists files with inbound/outbound
|
||||
link counts from `pql search --connections` (T-39).
|
||||
|
||||
@@ -113,7 +120,8 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
`clideFontMono` to match surrounding text weight.
|
||||
|
||||
- Default sidebar and context panel widths widened to 400px and
|
||||
420px respectively (previous maximums).
|
||||
420px respectively (previous maximums). Context panel max raised
|
||||
to 1000px.
|
||||
|
||||
- Ticket descriptions render through ClideMarkdown instead of plain
|
||||
text, with clickable DQRT record links.
|
||||
@@ -121,6 +129,12 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit.
|
||||
- Decision detail view subscribes to the message bus for navigation
|
||||
(same pattern as tickets), enabling navigation from any source.
|
||||
|
||||
- TreeSitterService is now a shared singleton — eliminates native
|
||||
double-free crashes from multiple WASM engine instances.
|
||||
|
||||
- Code block syntax highlighting fixes overlapping tree-sitter spans
|
||||
that caused duplicated text (e.g. `makemake` instead of `make`).
|
||||
|
||||
- Context panel drag resize fixed — dragging left now correctly
|
||||
grows the right panel instead of shrinking it.
|
||||
|
||||
|
||||
+1
-1
@@ -892,7 +892,7 @@ class _ContextSlot extends StatelessWidget {
|
||||
return Container(
|
||||
color: tokens.panelBackground,
|
||||
alignment: Alignment.topLeft,
|
||||
padding: const EdgeInsets.fromLTRB(0, 2, 2, 0),
|
||||
padding: const EdgeInsets.only(right: 2),
|
||||
child: active.build(context),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class EditorView extends StatefulWidget {
|
||||
|
||||
class _EditorViewState extends State<EditorView> {
|
||||
EditorController? _controller;
|
||||
final TreeSitterService _syntax = TreeSitterService();
|
||||
final TreeSitterService _syntax = TreeSitterService.shared;
|
||||
late final SyntaxTextController _text;
|
||||
late final FocusNode _focus;
|
||||
String? _lastRemoteContent;
|
||||
@@ -57,7 +57,6 @@ class _EditorViewState extends State<EditorView> {
|
||||
_focus.dispose();
|
||||
_controller?.removeListener(_onControllerChanged);
|
||||
_controller?.dispose();
|
||||
_syntax.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import 'dart:async';
|
||||
import 'package:clide/builtin/markdown/src/markdown_viewer.dart';
|
||||
import 'package:clide/extension/extension.dart';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class MarkdownExtension extends ClideExtension {
|
||||
@override
|
||||
@@ -10,51 +12,35 @@ class MarkdownExtension extends ClideExtension {
|
||||
@override
|
||||
String get title => 'Markdown';
|
||||
@override
|
||||
String get version => '0.2.0';
|
||||
String get version => '0.3.0';
|
||||
@override
|
||||
List<String> get dependsOn => const ['builtin.editor'];
|
||||
List<String> get dependsOn => const [];
|
||||
|
||||
ClideExtensionContext? _ctx;
|
||||
StreamSubscription<DaemonEvent>? _editorSub;
|
||||
bool _viewerSpawned = false;
|
||||
StreamSubscription<Message>? _sub;
|
||||
|
||||
@override
|
||||
List<ContributionPoint> get contributions => const [];
|
||||
List<ContributionPoint> get contributions => [
|
||||
TabContribution(
|
||||
id: 'markdown.viewer',
|
||||
slot: Slots.contextPanel,
|
||||
title: 'Markdown',
|
||||
icon: PhosphorIcons.fileText,
|
||||
build: (_) => const MarkdownViewer(),
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
Future<void> activate(ClideExtensionContext ctx) async {
|
||||
_ctx = ctx;
|
||||
_editorSub = ctx.events.on<DaemonEvent>().listen((e) {
|
||||
if (e.subsystem != 'editor') return;
|
||||
if (e.kind == 'editor.active-changed' || e.kind == 'editor.opened') {
|
||||
final path = e.data['path'] as String?;
|
||||
if (path != null && path.endsWith('.md')) {
|
||||
_spawnViewer();
|
||||
}
|
||||
}
|
||||
_sub = ctx.messages.subscribe(publisher: id, channel: 'selection').listen((msg) {
|
||||
final path = msg.data['path'] as String?;
|
||||
if (path == null) return;
|
||||
ctx.panels.activateTab(Slots.contextPanel, 'markdown.viewer');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ctx.messages.publish(id, 'load', {'path': path});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deactivate() async {
|
||||
_editorSub?.cancel();
|
||||
if (_viewerSpawned) {
|
||||
_ctx?.panels.uncontribute('markdown.viewer');
|
||||
_viewerSpawned = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _spawnViewer() {
|
||||
final ctx = _ctx;
|
||||
if (ctx == null || _viewerSpawned) return;
|
||||
ctx.panels.contribute(TabContribution(
|
||||
id: 'markdown.viewer',
|
||||
slot: Slots.contextPanel,
|
||||
title: 'Viewer',
|
||||
priority: -100,
|
||||
build: (_) => const MarkdownViewer(),
|
||||
));
|
||||
_viewerSpawned = true;
|
||||
ctx.panels.activateTab(Slots.contextPanel, 'markdown.viewer');
|
||||
}
|
||||
Future<void> deactivate() async => _sub?.cancel();
|
||||
}
|
||||
|
||||
@@ -15,14 +15,19 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
String? _path;
|
||||
String? _content;
|
||||
String? _error;
|
||||
StreamSubscription<DaemonEvent>? _eventSub;
|
||||
StreamSubscription<Message>? _selectionSub;
|
||||
StreamSubscription<DaemonEvent>? _editorSub;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_eventSub != null) return;
|
||||
if (_selectionSub != null) return;
|
||||
final kernel = ClideKernel.of(context);
|
||||
_eventSub = kernel.events.on<DaemonEvent>().listen((e) {
|
||||
_selectionSub = kernel.messages.subscribe(publisher: 'builtin.markdown', channel: 'load').listen((msg) {
|
||||
final path = msg.data['path'] as String?;
|
||||
if (path != null) _loadFile(path);
|
||||
});
|
||||
_editorSub = kernel.events.on<DaemonEvent>().listen((e) {
|
||||
if (e.kind == 'editor.buffer_activated') {
|
||||
final path = e.data['path'] as String?;
|
||||
if (path != null && path.endsWith('.md')) {
|
||||
@@ -30,33 +35,21 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
}
|
||||
}
|
||||
});
|
||||
final activeTab = kernel.panels.activeTabIn(Slots.workspace);
|
||||
if (activeTab == 'editor.active') {
|
||||
unawaited(_loadActiveBuffer());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_eventSub?.cancel();
|
||||
_selectionSub?.cancel();
|
||||
_editorSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadActiveBuffer() async {
|
||||
final kernel = ClideKernel.of(context);
|
||||
final resp = await kernel.ipc.request('editor.active');
|
||||
if (!mounted || !resp.ok) return;
|
||||
final path = resp.data['path'] as String?;
|
||||
if (path != null && path.endsWith('.md')) {
|
||||
await _loadFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadFile(String path) async {
|
||||
final kernel = ClideKernel.of(context);
|
||||
final resp = await kernel.ipc.request('files.read', args: {'path': path});
|
||||
if (!mounted) return;
|
||||
if (resp.ok) {
|
||||
kernel.messages.publish('builtin.markdown', 'focus', {'path': path});
|
||||
setState(() {
|
||||
_path = path;
|
||||
_content = resp.data['content'] as String? ?? '';
|
||||
@@ -84,7 +77,7 @@ class _MarkdownViewerState extends State<MarkdownViewer> {
|
||||
if (_content == null) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('Open a .md file to preview it here.', muted: true),
|
||||
child: ClideText('Select a .md file to preview it here.', muted: true),
|
||||
);
|
||||
}
|
||||
return ClidePaneChrome(
|
||||
|
||||
@@ -9,14 +9,14 @@ import 'dart:async';
|
||||
import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
enum PqlView { files, query }
|
||||
enum PqlView { query, markdown }
|
||||
|
||||
class PqlController extends ChangeNotifier {
|
||||
PqlController({required this.ipc});
|
||||
|
||||
final DaemonClient ipc;
|
||||
|
||||
PqlView _view = PqlView.files;
|
||||
PqlView _view = PqlView.query;
|
||||
PqlView get view => _view;
|
||||
|
||||
String? _error;
|
||||
@@ -38,19 +38,19 @@ class PqlController extends ChangeNotifier {
|
||||
_error = null;
|
||||
notifyListeners();
|
||||
switch (v) {
|
||||
case PqlView.files:
|
||||
unawaited(loadFiles());
|
||||
case PqlView.markdown:
|
||||
unawaited(loadMarkdownFiles());
|
||||
case PqlView.query:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadFiles({String? glob}) async {
|
||||
Future<void> loadMarkdownFiles({String? glob}) async {
|
||||
_loading = true;
|
||||
notifyListeners();
|
||||
|
||||
final r = await ipc.request('pql.files', args: {
|
||||
if (glob != null) 'glob': glob,
|
||||
'glob': glob ?? '**/*.md',
|
||||
'limit': 200,
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/// Sidebar panel for pql — file listing, DSL query input,
|
||||
/// decisions list, and ticket board views.
|
||||
/// Sidebar panel for pql — DSL query input and markdown file listing.
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
@@ -19,6 +18,9 @@ class PqlPanelView extends StatefulWidget {
|
||||
|
||||
class _PqlPanelViewState extends State<PqlPanelView> {
|
||||
PqlController? _controller;
|
||||
String? _focusedPath;
|
||||
final _focusedKey = GlobalKey();
|
||||
StreamSubscription<Message>? _focusSub;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -26,11 +28,25 @@ class _PqlPanelViewState extends State<PqlPanelView> {
|
||||
if (_controller != null) return;
|
||||
final kernel = ClideKernel.of(context);
|
||||
_controller = PqlController(ipc: kernel.ipc);
|
||||
unawaited(_controller!.loadFiles());
|
||||
_focusSub = kernel.messages.subscribe(publisher: 'builtin.markdown', channel: 'focus').listen((msg) {
|
||||
final path = msg.data['path'] as String?;
|
||||
if (path == null || path == _focusedPath) return;
|
||||
setState(() {
|
||||
_focusedPath = path;
|
||||
if (_controller!.view != PqlView.markdown) {
|
||||
_controller!.switchView(PqlView.markdown);
|
||||
}
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final ctx = _focusedKey.currentContext;
|
||||
if (ctx != null) Scrollable.ensureVisible(ctx, duration: const Duration(milliseconds: 200), alignment: 0.3);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusSub?.cancel();
|
||||
_controller?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@@ -43,59 +59,52 @@ class _PqlPanelViewState extends State<PqlPanelView> {
|
||||
listenable: c,
|
||||
builder: (context, _) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return Semantics(
|
||||
label: 'pql panel',
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ViewTabs(controller: c),
|
||||
if (c.view == PqlView.query)
|
||||
ClideFilterBox(
|
||||
hint: 'PQL query…',
|
||||
debounce: Duration.zero,
|
||||
onChanged: (_) {},
|
||||
onSubmitted: (v) => unawaited(c.runQuery(v)),
|
||||
),
|
||||
if (c.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 4),
|
||||
child: ClideText(
|
||||
c.error!,
|
||||
color: tokens.statusError,
|
||||
fontSize: clideFontCaption,
|
||||
maxLines: 3,
|
||||
),
|
||||
),
|
||||
if (c.loading && c.results.isEmpty)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('Loading…', muted: true),
|
||||
),
|
||||
if (!c.loading && c.results.isEmpty && c.error == null)
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: ClideText('No results.', muted: true),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (c.view == PqlView.files)
|
||||
for (final f in c.results) _FileRow(entry: f),
|
||||
if (c.view == PqlView.query)
|
||||
for (final r in c.results) _QueryResultRow(entry: r),
|
||||
],
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_ViewTabs(controller: c),
|
||||
if (c.view == PqlView.query)
|
||||
ClideFilterBox(
|
||||
hint: 'PQL query…',
|
||||
debounce: Duration.zero,
|
||||
onChanged: (_) {},
|
||||
onSubmitted: (v) => unawaited(c.runQuery(v)),
|
||||
),
|
||||
if (c.view == PqlView.markdown)
|
||||
ClideFilterBox(
|
||||
hint: 'Filter markdown…',
|
||||
onChanged: (v) => unawaited(c.loadMarkdownFiles(glob: v.isEmpty ? null : '**/*$v*.md')),
|
||||
),
|
||||
if (c.error != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
child: ClideText(c.error!, color: tokens.statusError, fontSize: clideFontCaption, maxLines: 3),
|
||||
),
|
||||
if (c.loading && c.results.isEmpty)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('Loading…', muted: true)),
|
||||
if (!c.loading && c.results.isEmpty && c.error == null)
|
||||
const Padding(padding: EdgeInsets.all(12), child: ClideText('No results.', muted: true)),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (c.view == PqlView.markdown)
|
||||
for (final f in c.results)
|
||||
_FileRow(
|
||||
entry: f,
|
||||
focused: (f['path'] as String?) == _focusedPath,
|
||||
focusKey: (f['path'] as String?) == _focusedPath ? _focusedKey : null,
|
||||
),
|
||||
if (c.view == PqlView.query)
|
||||
for (final r in c.results) _QueryResultRow(entry: r),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -119,21 +128,14 @@ class _ViewTabs extends StatelessWidget {
|
||||
for (final v in PqlView.values)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Semantics(
|
||||
button: true,
|
||||
toggled: controller.view == v,
|
||||
label: v.name,
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.switchView(v),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: ClideText(
|
||||
_tabLabel(v),
|
||||
fontSize: clideFontCaption,
|
||||
color: controller.view == v
|
||||
? tokens.globalForeground
|
||||
: tokens.globalTextMuted,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () => controller.switchView(v),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: ClideText(
|
||||
_tabLabel(v),
|
||||
fontSize: clideFontCaption,
|
||||
color: controller.view == v ? tokens.globalForeground : tokens.globalTextMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -144,36 +146,38 @@ class _ViewTabs extends StatelessWidget {
|
||||
}
|
||||
|
||||
static String _tabLabel(PqlView v) => switch (v) {
|
||||
PqlView.files => 'Files',
|
||||
PqlView.query => 'Query',
|
||||
PqlView.query => 'Search',
|
||||
PqlView.markdown => 'Markdown',
|
||||
};
|
||||
}
|
||||
|
||||
class _FileRow extends StatelessWidget {
|
||||
const _FileRow({required this.entry});
|
||||
const _FileRow({required this.entry, this.focused = false, this.focusKey});
|
||||
final Map<String, Object?> entry;
|
||||
final bool focused;
|
||||
final GlobalKey? focusKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
final path = entry['path'] as String? ?? '';
|
||||
final name = entry['name'] as String? ?? path.split('/').last;
|
||||
return Semantics(
|
||||
button: true,
|
||||
label: 'Open $name',
|
||||
return Padding(
|
||||
key: focusKey,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
child: ClideTappable(
|
||||
onTap: () {
|
||||
final kernel = ClideKernel.of(context);
|
||||
unawaited(
|
||||
kernel.ipc.request('editor.open', args: {'path': path}));
|
||||
},
|
||||
onTap: () => ClideKernel.of(context).messages.publish('builtin.markdown', 'selection', {'path': path}),
|
||||
builder: (context, hovered, _) => Container(
|
||||
color: hovered ? tokens.sidebarItemHover : null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: hovered ? tokens.sidebarItemHover : (focused ? tokens.sidebarItemSelected : null),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: focused ? Border.all(color: tokens.globalFocus, width: 1) : null,
|
||||
),
|
||||
child: ClideText(
|
||||
path,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: clideFontCaption,
|
||||
color: tokens.sidebarForeground,
|
||||
),
|
||||
),
|
||||
@@ -201,8 +205,7 @@ class _QueryResultRow extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ClideText(name, color: tokens.sidebarForeground),
|
||||
if (values.isNotEmpty)
|
||||
ClideText(values, fontSize: clideFontCaption, muted: true, maxLines: 2),
|
||||
if (values.isNotEmpty) ClideText(values, fontSize: clideFontCaption, muted: true, maxLines: 2),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@ LayoutPresetContribution classicPreset() => const LayoutPresetContribution(
|
||||
position: SlotPosition.right,
|
||||
defaultSize: 420,
|
||||
minSize: 220,
|
||||
maxSize: 420,
|
||||
maxSize: 1000,
|
||||
),
|
||||
LayoutSlot(
|
||||
slot: Slots.statusbar,
|
||||
|
||||
@@ -42,6 +42,9 @@ class _LoadedGrammar {
|
||||
}
|
||||
|
||||
class TreeSitterService {
|
||||
static final TreeSitterService shared = TreeSitterService._();
|
||||
TreeSitterService._();
|
||||
|
||||
final Map<String, _LoadedGrammar> _grammars = {};
|
||||
final Set<String> _unavailable = {};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import '../files/ignore.dart';
|
||||
import '../files/listing.dart';
|
||||
import '../files/watcher.dart';
|
||||
import '../ipc/envelope.dart';
|
||||
import '../ipc/schema_v1.dart';
|
||||
import '../panes/event_sink.dart';
|
||||
import 'dispatcher.dart';
|
||||
|
||||
@@ -63,6 +64,19 @@ void registerFilesCommands(DaemonDispatcher d, FilesService files) {
|
||||
},
|
||||
));
|
||||
|
||||
d.register('files.read', (req) async {
|
||||
final path = req.args['path'] as String?;
|
||||
if (path == null || path.isEmpty) {
|
||||
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'files.read requires a path'));
|
||||
}
|
||||
final file = File('${files.root.absolute.path}/$path');
|
||||
if (!file.existsSync()) {
|
||||
return IpcResponse.err(id: req.id, error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'file not found: $path'));
|
||||
}
|
||||
final content = file.readAsStringSync();
|
||||
return IpcResponse.ok(id: req.id, data: {'path': path, 'content': content});
|
||||
});
|
||||
|
||||
d.register('files.ls', (req) async {
|
||||
final dir = (req.args['path'] as String?) ?? '';
|
||||
final entries = await listDir(
|
||||
|
||||
@@ -16,7 +16,6 @@ class ClideCodeBlock extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ClideCodeBlockState extends State<ClideCodeBlock> {
|
||||
final TreeSitterService _syntax = TreeSitterService();
|
||||
List<SyntaxSpan>? _spans;
|
||||
|
||||
@override
|
||||
@@ -33,12 +32,6 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_syntax.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _highlight() async {
|
||||
final lang = widget.language;
|
||||
if (lang == null || lang.isEmpty) {
|
||||
@@ -46,11 +39,11 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
|
||||
return;
|
||||
}
|
||||
final path = 'code.$lang';
|
||||
if (!await _syntax.hasGrammar(path)) {
|
||||
if (!await TreeSitterService.shared.hasGrammar(path)) {
|
||||
setState(() => _spans = null);
|
||||
return;
|
||||
}
|
||||
final result = await _syntax.highlight(path, widget.source);
|
||||
final result = await TreeSitterService.shared.highlight(path, widget.source);
|
||||
if (mounted) setState(() => _spans = result.spans);
|
||||
}
|
||||
|
||||
@@ -115,14 +108,15 @@ class _ClideCodeBlockState extends State<ClideCodeBlock> {
|
||||
for (final span in sorted) {
|
||||
final sChar = span.start < byteToChar.length ? byteToChar[span.start] : source.length;
|
||||
final eChar = span.end < byteToChar.length ? byteToChar[span.end] : source.length;
|
||||
if (sChar > lastChar) {
|
||||
children.add(TextSpan(text: source.substring(lastChar, sChar)));
|
||||
final clippedStart = sChar < lastChar ? lastChar : sChar;
|
||||
if (clippedStart > lastChar) {
|
||||
children.add(TextSpan(text: source.substring(lastChar, clippedStart)));
|
||||
}
|
||||
if (eChar > sChar) {
|
||||
if (eChar > clippedStart) {
|
||||
final color = TreeSitterService.colorForRole(span.role, tokens);
|
||||
children.add(TextSpan(text: source.substring(sChar, eChar), style: base.copyWith(color: color)));
|
||||
children.add(TextSpan(text: source.substring(clippedStart, eChar), style: base.copyWith(color: color)));
|
||||
}
|
||||
lastChar = eChar;
|
||||
if (eChar > lastChar) lastChar = eChar;
|
||||
}
|
||||
if (lastChar < source.length) {
|
||||
children.add(TextSpan(text: source.substring(lastChar)));
|
||||
|
||||
@@ -51,7 +51,6 @@ class ClidePaneChrome extends StatelessWidget {
|
||||
return ColoredBox(
|
||||
color: tokens.panelBackground,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_Header(
|
||||
title: title,
|
||||
|
||||
Reference in New Issue
Block a user