From 9d26d28579f224af2f8508ed6bf497187a093a87 Mon Sep 17 00:00:00 2001 From: ArthurHeitmann <art.tec.15@gmail.com> Date: Tue, 25 Oct 2022 08:10:48 +0200 Subject: [PATCH] fixed search cancel race condition --- lib/background/searchService.dart | 30 ++++- lib/utils.dart | 6 + .../FileHierarchyExplorer/FileExplorer.dart | 3 +- lib/widgets/filesView/fileTabView.dart | 2 +- lib/widgets/filesView/searchPanel.dart | 117 +++++++++++++++--- pubspec.lock | 7 ++ pubspec.yaml | 1 + 7 files changed, 142 insertions(+), 24 deletions(-) diff --git a/lib/background/searchService.dart b/lib/background/searchService.dart index 5253b955..17792e91 100644 --- a/lib/background/searchService.dart +++ b/lib/background/searchService.dart @@ -59,6 +59,7 @@ class SearchService { final StreamController<SearchResult> controller = StreamController<SearchResult>(); Isolate? _isolate; SendPort? _sendPort; + final _isDoneCompleter = Completer<void>(); final BoolProp isSearching; SearchService({ required this.isSearching }); @@ -96,30 +97,40 @@ class SearchService { else if (message is SearchResult) controller.add(message); else if (message is String && message == "done") { + if (!_isDoneCompleter.isCompleted) { + isSearching.value = false; + _isDoneCompleter.complete(); + } if (controller.isClosed) return; controller.close(); _isolate?.kill(priority: Isolate.beforeNextEvent); - isSearching.value = false; } else print("Unhandled message: $message"); } - void cancel() async { + Future<void> cancel() { _sendPort?.send("cancel"); - await Future.delayed(Duration(milliseconds: 500)); - _isolate?.kill(priority: Isolate.immediate); + Future.delayed(Duration(milliseconds: 500)).then((_) { + _isolate?.kill(priority: Isolate.immediate); + if (!_isDoneCompleter.isCompleted) + _isDoneCompleter.complete(); + }); + return _isDoneCompleter.future; } } /// In new isolate search recursively for files with given extensions in given path class _SearchServiceWorker { bool _isCanceled = false; + int _resultsCount = 0; + static const int _maxResults = 1000; void search(SearchOptions options) async { var receivePort = ReceivePort(); receivePort.listen(_onMessage); options.sendPort!.send(receivePort.sendPort); + var t1 = DateTime.now(); try { if (options is SearchOptionsText) @@ -129,7 +140,8 @@ class _SearchServiceWorker { } finally { options.sendPort!.send("done"); - print("Search done"); + var t2 = DateTime.now(); + print("Search done in ${t2.difference(t1).inSeconds} s"); } } @@ -159,6 +171,11 @@ class _SearchServiceWorker { if (!test(line)) continue; options.sendPort!.send(SearchResultText(filePath, line)); + _resultsCount++; + if (_resultsCount >= _maxResults) { + _isCanceled = true; + return; + } } } else { @@ -230,6 +247,9 @@ class _SearchServiceWorker { if (idData.id != options.id) return; options.sendPort!.send(SearchResultId(idData.xmlPath, idData)); + _resultsCount++; + if (_resultsCount >= _maxResults) + _isCanceled = true; } void _searchIdInXml(String filePath, XmlElement root, SearchOptionsId options) { diff --git a/lib/utils.dart b/lib/utils.dart index d3f08df9..ccad4ee7 100644 --- a/lib/utils.dart +++ b/lib/utils.dart @@ -463,3 +463,9 @@ Future<List<String>> _getDatFileListFromMetadata(String metadataPath) async { return files; } + +String pluralStr(int number, String label, [String numberSuffix = ""]) { + if (number == 1) + return "$number$numberSuffix $label"; + return "$number$numberSuffix ${label}s"; +} diff --git a/lib/widgets/FileHierarchyExplorer/FileExplorer.dart b/lib/widgets/FileHierarchyExplorer/FileExplorer.dart index d6548694..3ba87ed3 100644 --- a/lib/widgets/FileHierarchyExplorer/FileExplorer.dart +++ b/lib/widgets/FileHierarchyExplorer/FileExplorer.dart @@ -3,6 +3,7 @@ import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/material.dart'; import '../../stateManagement/statusInfo.dart'; +import '../../utils.dart'; import '../../widgets/theme/customTheme.dart'; import '../../stateManagement/ChangeNotifierWidget.dart'; import '../../stateManagement/FileHierarchy.dart'; @@ -29,7 +30,7 @@ class _FileExplorerState extends ChangeNotifierState<FileExplorer> { await Future.wait(futures); - messageLog.add("Opened ${details.files.length} file${details.files.length == 1 ? "" : "s"}"); + messageLog.add("Opened ${pluralStr(details.files.length, "file")}"); } @override diff --git a/lib/widgets/filesView/fileTabView.dart b/lib/widgets/filesView/fileTabView.dart index 99162cf9..6795ac24 100644 --- a/lib/widgets/filesView/fileTabView.dart +++ b/lib/widgets/filesView/fileTabView.dart @@ -84,7 +84,7 @@ class _FileTabViewState extends ChangeNotifierState<FileTabView> { windowManager.focus(); setState(() {}); - messageLog.add("Opened ${files.length} file${files.length == 1 ? "" : "s"}"); + messageLog.add("Opened ${pluralStr(files.length, "file")}"); } void pruneCachedWidgets() { diff --git a/lib/widgets/filesView/searchPanel.dart b/lib/widgets/filesView/searchPanel.dart index a8b1be92..27f61843 100644 --- a/lib/widgets/filesView/searchPanel.dart +++ b/lib/widgets/filesView/searchPanel.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:mutex/mutex.dart'; import '../../background/IdsIndexer.dart'; import '../../background/searchService.dart'; @@ -7,6 +8,7 @@ import '../../stateManagement/ChangeNotifierWidget.dart'; import '../../stateManagement/Property.dart'; import '../../stateManagement/nestedNotifier.dart'; import '../../stateManagement/openFilesManager.dart'; +import '../../utils.dart'; import '../misc/RowSeparated.dart'; import '../propEditors/simpleProps/boolPropIcon.dart'; import '../propEditors/simpleProps/propEditorFactory.dart'; @@ -28,6 +30,8 @@ class _SearchPanelState extends State<SearchPanel> { Stream<SearchResult>? searchStream; ValueNestedNotifier<SearchResult> searchResults = ValueNestedNotifier([]); BoolProp isSearching = BoolProp(false); + Mutex cancelMutex = Mutex(); + late final void Function() updateSearchStream; // common options StringProp extensions = StringProp(""); StringProp path = StringProp(""); @@ -41,6 +45,8 @@ class _SearchPanelState extends State<SearchPanel> { @override void initState() { + updateSearchStream = throttle(_updateSearchStream, 200); + extensions.addListener(updateSearchStream); path.addListener(updateSearchStream); query.addListener(updateSearchStream); @@ -80,12 +86,13 @@ class _SearchPanelState extends State<SearchPanel> { ); } - void resetSearch() { - searchService?.cancel(); + Future<void> resetSearch() async { + if (searchService == null) + return; + await searchService?.cancel(); searchService = null; searchStream = null; searchResults.clear(); - print("#########"); } bool areAllFieldsFilled() { @@ -101,8 +108,9 @@ class _SearchPanelState extends State<SearchPanel> { return true; } - void updateSearchStream() { - resetSearch(); + void _updateSearchStream() async { + await cancelMutex.protect<void>(() => resetSearch()); + if (!areAllFieldsFilled()) return; @@ -265,20 +273,57 @@ class _SearchPanelState extends State<SearchPanel> { child: ChangeNotifierBuilder( notifier: searchResults, builder: (context) { + String? errorText; + String? infoText; if (!areAllFieldsFilled()) - return Center(child: Text("Fill all fields to start search")); + errorText = "Fill all fields to start search"; if (searchResults.isEmpty) { if (isSearching.value) - return Center(child: Text("Searching...")); + errorText = "Searching..."; else - return Center(child: Text("No results")); + errorText = "No results"; } - return ListView.builder( - itemCount: searchResults.length, - itemBuilder: (context, index) => _makeSearchResult(searchResults[index]), - ); + var results = searchResults.toList(); + var optP = ""; + if (searchResults.length >= 1000) { + results = results.sublist(0, 1000); + infoText = "Stopped at 1000 results"; + optP = "+"; + } + + var resultsFilesCount = searchResults.map((e) => e.filePath).toSet().length; + var infoStyle = TextStyle( + color: getTheme(context).textColor!.withOpacity(0.5), + fontSize: 12, + ); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), + child: Text( + errorText ?? "Found ${pluralStr(searchResults.length, "result", optP)} in ${pluralStr(resultsFilesCount, "file", optP)}", + style: infoStyle, + ), + ), + if (infoText != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), + child: Text(infoText, style: infoStyle), + ), + SizedBox(height: 5), + Divider(height: 1,), + if (errorText == null) + Expanded( + child: ListView.builder( + itemCount: results.length, + itemBuilder: (context, index) => _makeSearchResult(results[index]), + ), + ), + ], + ); }, - ) + ), ); } @@ -300,14 +345,52 @@ class _SearchPanelState extends State<SearchPanel> { } Widget _makeSearchResultText(SearchResultText result) { + String text = result.line.replaceAll("\t", " "); + List<TextSpan> textSpans; + List<String> fillStrings; + RegExp regex; + if (isRegex.value) { + regex = RegExp(query.value, caseSensitive: isCaseSensitive.value); + textSpans = text.split(regex) + .map((e) => TextSpan(text: e)) + .toList(); + } + else { + regex = RegExp(RegExp.escape(query.value), caseSensitive: isCaseSensitive.value); + textSpans = text.split(regex) + .map((e) => TextSpan( + text: e, + style: TextStyle( + color: getTheme(context).textColor, + ), + )) + .toList(); + } + fillStrings = regex.allMatches(text) + .map((e) => e.group(0)!) + .toList(); + // insert colored spans of query between all text spans + for (int i = 1; i < textSpans.length; i += 2) { + textSpans.insert(i, TextSpan( + text: fillStrings[(i - 1) ~/ 2], + style: TextStyle( + color: getTheme(context).textColor, + backgroundColor: Theme.of(context).colorScheme.secondary.withOpacity(0.35), + overflow: TextOverflow.ellipsis, + ), + )); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - result.line, - style: TextStyle( - fontSize: 14, + RichText( + text: TextSpan( + children: textSpans, + style: TextStyle( + overflow: TextOverflow.ellipsis, + ), ), + overflow: TextOverflow.ellipsis, maxLines: 1, ), Tooltip( diff --git a/pubspec.lock b/pubspec.lock index c768ee36..9a9b00de 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -268,6 +268,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.0" + mutex: + dependency: "direct main" + description: + name: mutex + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 70689674..b68e6bd4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: flutter_launcher_icons: ^0.10.0 fluttertoast: ^8.0.9 highlight: ^0.7.0 + mutex: ^3.0.0 path: ^1.8.2 shared_preferences: ^2.0.15 tuple: ^2.0.0