add ClideFilterBox widget to all sidebar panes
Shared filter box with search icon, debounced input, and clear button. Applied to all six left-panel panes for consistent filtering: Files (flat path results when filtering), Git (filter staged/unstaged/untracked by path), Decisions (by ID/title/domain), Tickets (by ID/title/status), Problems (by source/message), and pql Query (replaces custom _QueryInput). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,14 @@ class FileTreeController extends ChangeNotifier {
|
||||
final Map<String, List<FileEntry>> _entries = {};
|
||||
List<FileEntry>? entriesFor(String path) => _entries[path];
|
||||
|
||||
List<FileEntry> allLoadedEntries() {
|
||||
final out = <FileEntry>[];
|
||||
for (final list in _entries.values) {
|
||||
out.addAll(list.where((e) => !e.isDirectory));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Initial boot: resolve the workspace root, load the root dir,
|
||||
/// subscribe to `files.changed` events.
|
||||
Future<void> load() async {
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:clide/kernel/kernel.dart';
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:clide/src/files/listing.dart' show FileEntry;
|
||||
|
||||
import 'file_tree_controller.dart';
|
||||
|
||||
/// Sidebar panel rendering the workspace file tree.
|
||||
@@ -24,6 +26,7 @@ class FileTreeView extends StatefulWidget {
|
||||
|
||||
class _FileTreeViewState extends State<FileTreeView> {
|
||||
FileTreeController? _controller;
|
||||
String _filter = '';
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
@@ -61,31 +64,45 @@ class _FileTreeViewState extends State<FileTreeView> {
|
||||
);
|
||||
}
|
||||
final rootName = root.split(Platform.pathSeparator).last;
|
||||
return Semantics(
|
||||
label: 'file tree — $rootName',
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_DirRow(
|
||||
name: rootName,
|
||||
path: '',
|
||||
controller: c,
|
||||
depth: 0,
|
||||
return Column(
|
||||
children: [
|
||||
ClideFilterBox(hint: 'Filter files…', onChanged: (v) => setState(() => _filter = v)),
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
label: 'file tree — $rootName',
|
||||
container: true,
|
||||
explicitChildNodes: true,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_filter.isEmpty) ...[
|
||||
_DirRow(name: rootName, path: '', controller: c, depth: 0),
|
||||
if (c.isExpanded('')) _Children(path: '', controller: c, depth: 1),
|
||||
] else
|
||||
..._filteredEntries(c),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (c.isExpanded(''))
|
||||
_Children(path: '', controller: c, depth: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _filteredEntries(FileTreeController c) {
|
||||
final lowerFilter = _filter.toLowerCase();
|
||||
final matches = c.allLoadedEntries().where((e) {
|
||||
return e.path.toLowerCase().contains(lowerFilter) || e.name.toLowerCase().contains(lowerFilter);
|
||||
}).toList();
|
||||
return [
|
||||
for (final e in matches) _FilteredFileRow(entry: e),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _Children extends StatelessWidget {
|
||||
@@ -269,3 +286,37 @@ class _RowState extends State<_Row> {
|
||||
}
|
||||
}
|
||||
|
||||
class _FilteredFileRow extends StatefulWidget {
|
||||
const _FilteredFileRow({required this.entry});
|
||||
final FileEntry entry;
|
||||
|
||||
@override
|
||||
State<_FilteredFileRow> createState() => _FilteredFileRowState();
|
||||
}
|
||||
|
||||
class _FilteredFileRowState extends State<_FilteredFileRow> {
|
||||
bool _hover = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tokens = ClideTheme.of(context).surface;
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
onEnter: (_) => setState(() => _hover = true),
|
||||
onExit: (_) => setState(() => _hover = false),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
final kernel = ClideKernel.of(context);
|
||||
unawaited(kernel.ipc.request('editor.open', args: {'path': widget.entry.path}));
|
||||
},
|
||||
child: Container(
|
||||
color: _hover ? tokens.sidebarItemHover : null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
|
||||
child: ClideText(widget.entry.path, maxLines: 1, overflow: TextOverflow.ellipsis, color: tokens.sidebarForeground),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user