A file picker overlay over the whole workspace, distinct from the command palette. QuickOpenController holds the file list + a subsequence fuzzy filter; the overlay loads the list via files.walk on open, shows RecentFilesService entries on an empty query, and opens the selection through a shared openWorkspaceFile helper (.md → markdown reader bus, else editor.open) that the files panel now also routes through, so recents stay in sync from every open site. Bound to ctrl+p / meta+p with `when: !palette.open` so it never collides with the palette's ctrl+p navigation; in-overlay arrows/enter/ escape reuse the palette's keymap-driven model via quickOpen.* intents. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
3.0 KiB
Dart
101 lines
3.0 KiB
Dart
/// Unit tests for [QuickOpenController] — state, fuzzy ranking, recents
|
|
/// fallback, and selection wrapping (T-51).
|
|
library;
|
|
|
|
import 'package:clide/kernel/src/quick_open.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
void main() {
|
|
late List<String> recents;
|
|
QuickOpenController make() => QuickOpenController(recentPaths: () => recents);
|
|
|
|
setUp(() => recents = ['recent_a.dart', 'recent_b.dart']);
|
|
|
|
test('open/close/toggle track isOpen and reset the filter', () {
|
|
final c = make();
|
|
expect(c.isOpen, isFalse);
|
|
c.open();
|
|
expect(c.isOpen, isTrue);
|
|
c.setFilter('main');
|
|
c.close();
|
|
expect(c.isOpen, isFalse);
|
|
expect(c.filter, isEmpty);
|
|
c.toggle();
|
|
expect(c.isOpen, isTrue);
|
|
});
|
|
|
|
test('empty query shows recents', () {
|
|
final c = make()..open();
|
|
c.setFiles(['lib/main.dart', 'lib/app.dart']);
|
|
expect(c.filtered(), recents);
|
|
expect(c.selectedPath, 'recent_a.dart');
|
|
});
|
|
|
|
test('non-empty query fuzzy-matches the file list, not recents', () {
|
|
final c = make()..open();
|
|
c.setFiles(['lib/main.dart', 'lib/app.dart', 'README.md']);
|
|
c.setFilter('main');
|
|
expect(c.filtered(), contains('lib/main.dart'));
|
|
expect(c.filtered(), isNot(contains('README.md')));
|
|
});
|
|
|
|
test('subsequence match ranks contiguous/early hits above scattered', () {
|
|
final c = make()..open();
|
|
c.setFiles(['x/abc_extra.dart', 'abc.dart', 'a_b_c.dart']);
|
|
c.setFilter('abc');
|
|
// 'abc.dart' (contiguous, earliest) should rank first.
|
|
expect(c.filtered().first, 'abc.dart');
|
|
});
|
|
|
|
test('non-matching query yields an empty result list', () {
|
|
final c = make()..open();
|
|
c.setFiles(['lib/main.dart']);
|
|
c.setFilter('zzzzz');
|
|
expect(c.filtered(), isEmpty);
|
|
expect(c.selectedPath, isNull);
|
|
});
|
|
|
|
test('selectNext/Previous wrap around the result list', () {
|
|
final c = make()..open();
|
|
c.setFiles(['a.dart', 'b.dart', 'c.dart']);
|
|
c.setFilter('dart'); // matches all three
|
|
expect(c.selectedIndex, 0);
|
|
c.selectPrevious(); // wraps to last
|
|
expect(c.selectedIndex, c.filtered().length - 1);
|
|
c.selectNext(); // wraps back to 0
|
|
expect(c.selectedIndex, 0);
|
|
});
|
|
|
|
test('selection is a no-op with fewer than two results', () {
|
|
final c = make()..open();
|
|
c.setFiles(['only.dart']);
|
|
c.setFilter('only');
|
|
c.selectNext();
|
|
expect(c.selectedIndex, 0);
|
|
});
|
|
|
|
test('setFiles records truncation', () {
|
|
final c = make()..open();
|
|
c.setFiles(['a.dart'], truncated: true);
|
|
expect(c.truncated, isTrue);
|
|
});
|
|
|
|
test('result list is capped at resultCap', () {
|
|
final c = make()..open();
|
|
c.setFiles([for (var i = 0; i < QuickOpenController.resultCap + 50; i++) 'f$i.dart']);
|
|
c.setFilter('dart');
|
|
expect(c.filtered(), hasLength(QuickOpenController.resultCap));
|
|
});
|
|
|
|
test('setLoading toggles the flag and notifies', () {
|
|
final c = make();
|
|
var n = 0;
|
|
c.addListener(() => n++);
|
|
c.setLoading(true);
|
|
expect(c.isLoading, isTrue);
|
|
expect(n, 1);
|
|
c.setLoading(true); // no change → no notify
|
|
expect(n, 1);
|
|
});
|
|
}
|