fix: run search bar commands on Enter and filter by /folder

This commit is contained in:
randogoth 2026-10-13 12:13:29 +03:00
parent b3df7fba9f
commit 0940fec5e8
4 changed files with 138 additions and 41 deletions

View file

@ -336,10 +336,20 @@ Happy note taking! ✨
final terms = query.split(' '); // Split search into words
final searchTags = terms.where((t) => t.startsWith(RegExp(r'[#@+]'))).toSet();
final searchText = terms.where((t) => !t.startsWith(RegExp(r'[#@+]'))).join(' ');
final searchFolders =
terms.where((t) => t.startsWith('/')).map((t) => t.substring(1));
final searchText =
terms.where((t) => !t.startsWith(RegExp(r'[#@+/]'))).join(' ');
filteredNotes = notes.where((note) {
final filename = note.uri.pathSegments.last.toLowerCase();
// Folder terms match the start of the note's folder path, like tags do.
final relative = path.relative(note.parent.path, from: notesDirectoryPath);
final folder =
relative == '.' ? '' : path.split(relative).join('/').toLowerCase();
if (!searchFolders.every(folder.startsWith)) return false;
final noteTags = _extractTags(note.readAsStringSync());
// ✅ Read file content ONLY if `Include File Content` is enabled
@ -822,7 +832,9 @@ Happy note taking! ✨
onEditingComplete: () {},
onSubmitted: (text) {
final trimmedText = text.trim();
if (_highlighted != null) {
if (trimmedText.startsWith(':')) {
_handleCommand(trimmedText);
} else if (_highlighted != null) {
_openNote(filteredNotes[_highlighted!]);
} else if (trimmedText.isNotEmpty && _isNewNote(trimmedText)) {
_createNewNote(trimmedText);

View file

@ -1,21 +1,8 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:muzzle_velocity/note_editor.dart';
import 'package:muzzle_velocity/note_list.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _FakePathProvider extends PathProviderPlatform
with MockPlatformInterfaceMixin {
_FakePathProvider(this.path);
final String path;
@override
Future<String?> getApplicationDocumentsPath() async => path;
}
import 'note_list_harness.dart';
Future<void> _press(WidgetTester tester, LogicalKeyboardKey key) async {
await tester.sendKeyEvent(key);
@ -23,31 +10,9 @@ Future<void> _press(WidgetTester tester, LogicalKeyboardKey key) async {
}
void main() {
late Directory notes;
setUp(() {
notes = Directory.systemTemp.createTempSync('mv-list');
for (final name in ['alpha', 'beta', 'gamma']) {
File('${notes.path}/$name.md').writeAsStringSync('# $name\n');
}
PathProviderPlatform.instance = _FakePathProvider(notes.path);
SharedPreferences.setMockInitialValues({'initialized': true});
});
Future<void> pumpList(WidgetTester tester) async {
await tester.runAsync(() async {
await tester.pumpWidget(MaterialApp(
home: NoteListScreen(
toggleTheme: (_) {},
toggleTags: () {},
isDarkMode: false,
showTags: true,
),
));
await Future<void>.delayed(const Duration(milliseconds: 200));
});
await tester.pumpAndSettle();
}
setUp(() => setUpNotes({
for (final name in ['alpha', 'beta', 'gamma']) '$name.md': '# $name\n',
}));
bool searchHasFocus(WidgetTester tester) =>
tester.widget<TextField>(find.byType(TextField)).focusNode!.hasFocus;

View file

@ -0,0 +1,56 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:muzzle_velocity/note_list.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _FakePathProvider extends PathProviderPlatform
with MockPlatformInterfaceMixin {
_FakePathProvider(this.path);
final String path;
@override
Future<String?> getApplicationDocumentsPath() async => path;
}
/// Creates a temporary notes folder with [files] (relative path to content)
/// and makes it the app's default notes directory.
Directory setUpNotes(Map<String, String> files) {
final notes = Directory.systemTemp.createTempSync('mv-list');
files.forEach((name, content) {
File('${notes.path}/$name')
..createSync(recursive: true)
..writeAsStringSync(content);
});
PathProviderPlatform.instance = _FakePathProvider(notes.path);
SharedPreferences.setMockInitialValues({'initialized': true});
return notes;
}
/// Pumps the note list, letting its real file I/O finish.
Future<void> pumpList(
WidgetTester tester, {
void Function(bool)? toggleTheme,
VoidCallback? toggleTags,
}) async {
await tester.runAsync(() async {
await tester.pumpWidget(MaterialApp(
home: NoteListScreen(
toggleTheme: toggleTheme ?? (_) {},
toggleTags: toggleTags ?? () {},
isDarkMode: false,
showTags: true,
),
));
await Future<void>.delayed(const Duration(milliseconds: 200));
});
await tester.pumpAndSettle();
}
/// Titles of the notes currently listed.
List<String> listedNotes(WidgetTester tester) => [
for (final tile in tester.widgetList<ListTile>(find.byType(ListTile)))
(tile.title as Text).data!,
];

View file

@ -0,0 +1,64 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'note_list_harness.dart';
Future<void> _search(WidgetTester tester, String query) async {
await tester.enterText(find.byType(TextField), query);
// Filtering is debounced.
await tester.pump(const Duration(milliseconds: 400));
await tester.pumpAndSettle();
}
void main() {
late Directory notes;
setUp(() => notes = setUpNotes({
'root.md': '# root\n',
'work/plan.md': '# plan\n',
'work/deep/notes.md': '# notes\n',
'home/plan.md': '# plan\n',
}));
testWidgets('/folder terms filter by the start of the folder path',
(tester) async {
await pumpList(tester);
expect(listedNotes(tester), hasLength(4));
await _search(tester, '/work');
expect(listedNotes(tester)..sort(), ['notes', 'plan']);
await _search(tester, '/work/deep');
expect(listedNotes(tester), ['notes']);
await _search(tester, '/ho plan');
expect(listedNotes(tester), ['plan']);
await _search(tester, '/nowhere');
expect(listedNotes(tester), isEmpty);
});
testWidgets('Enter runs a command instead of creating a note',
(tester) async {
var tagToggles = 0;
await pumpList(tester, toggleTags: () => tagToggles++);
await _search(tester, ':tags');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(tagToggles, 1);
expect(tester.widget<TextField>(find.byType(TextField)).controller!.text,
isEmpty);
expect(File('${notes.path}/tags.md').existsSync(), isFalse);
});
testWidgets('an unknown command does not create a note', (tester) async {
await pumpList(tester);
await _search(tester, ':nope');
await tester.testTextInput.receiveAction(TextInputAction.done);
await tester.pumpAndSettle();
expect(File('${notes.path}/nope.md').existsSync(), isFalse);
});
}