chore: local and server result

This commit is contained in:
Nathan
2025-04-14 22:05:21 +08:00
parent a44ad63230
commit 35bc095760
21 changed files with 346 additions and 314 deletions

View File

@ -1,5 +1,5 @@
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_views_list.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@ -27,11 +27,12 @@ void main() {
expect(find.byType(RecentViewsList), findsOneWidget);
// Expect three recent history items
expect(find.byType(RecentViewTile), findsNWidgets(3));
expect(find.byType(SearchRecentViewCell), findsNWidgets(3));
// Expect the first item to be the last viewed document
final firstDocumentWidget =
tester.widget(find.byType(RecentViewTile).first) as RecentViewTile;
tester.widget(find.byType(SearchRecentViewCell).first)
as SearchRecentViewCell;
expect(firstDocumentWidget.view.name, secondDocument);
});
});

View File

@ -11,10 +11,25 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'command_palette_bloc.freezed.dart';
class Debouncer {
Debouncer({required this.delay});
final Duration delay;
Timer? _timer;
void run(void Function() action) {
_timer?.cancel();
_timer = Timer(delay, action);
}
void dispose() {
_timer?.cancel();
}
}
class CommandPaletteBloc
extends Bloc<CommandPaletteEvent, CommandPaletteState> {
CommandPaletteBloc() : super(CommandPaletteState.initial()) {
// Register event handlers
on<_SearchChanged>(_onSearchChanged);
on<_PerformSearch>(_onPerformSearch);
on<_NewSearchStream>(_onNewSearchStream);
@ -26,38 +41,35 @@ class CommandPaletteBloc
_initTrash();
}
Timer? _debounceOnChanged;
final Debouncer _searchDebouncer = Debouncer(
delay: const Duration(milliseconds: 300),
);
final TrashService _trashService = TrashService();
final TrashListener _trashListener = TrashListener();
String? _oldQuery;
String? _activeQuery;
String? _workspaceId;
@override
Future<void> close() {
_trashListener.close();
_debounceOnChanged?.cancel();
_searchDebouncer.dispose();
state.searchResponseStream?.dispose();
return super.close();
}
Future<void> _initTrash() async {
// Start listening for trash updates
_trashListener.start(
trashUpdated: (trashOrFailed) {
add(
CommandPaletteEvent.trashChanged(
trash: trashOrFailed.toNullable(),
),
);
},
trashUpdated: (trashOrFailed) => add(
CommandPaletteEvent.trashChanged(
trash: trashOrFailed.toNullable(),
),
),
);
// Read initial trash state and forward results
final trashOrFailure = await _trashService.readTrash();
add(
CommandPaletteEvent.trashChanged(
trash: trashOrFailure.toNullable()?.items,
),
trashOrFailure.fold(
(trash) => add(CommandPaletteEvent.trashChanged(trash: trash.items)),
(error) => debugPrint('Failed to load trash: $error'),
);
}
@ -65,9 +77,7 @@ class CommandPaletteBloc
_SearchChanged event,
Emitter<CommandPaletteState> emit,
) {
_debounceOnChanged?.cancel();
_debounceOnChanged = Timer(
const Duration(milliseconds: 300),
_searchDebouncer.run(
() {
if (!isClosed) {
add(CommandPaletteEvent.performSearch(search: event.search));
@ -80,31 +90,44 @@ class CommandPaletteBloc
_PerformSearch event,
Emitter<CommandPaletteState> emit,
) async {
if (event.search.isNotEmpty && event.search != state.query) {
_oldQuery = state.query;
if (event.search.isEmpty && event.search != state.query) {
emit(
state.copyWith(
query: null,
isLoading: false,
serverResponseItems: [],
localResponseItems: [],
combinedResponseItems: {},
resultSummaries: [],
),
);
} else {
emit(state.copyWith(query: event.search, isLoading: true));
_activeQuery = event.search;
// Fire off search asynchronously (fire and forget)
unawaited(
SearchBackendService.performSearch(
event.search,
workspaceId: _workspaceId,
).then(
(result) => result.onSuccess((stream) {
if (!isClosed) {
add(CommandPaletteEvent.newSearchStream(stream: stream));
}
}),
),
);
} else {
// Clear state if search is empty or unchanged
emit(
state.copyWith(
query: null,
isLoading: false,
resultItems: [],
resultSummaries: [],
(result) => result.fold(
(stream) {
if (!isClosed && _activeQuery == event.search) {
add(CommandPaletteEvent.newSearchStream(stream: stream));
}
},
(error) {
debugPrint('Search error: $error');
if (!isClosed) {
add(
CommandPaletteEvent.resultsChanged(
searchId: '',
isLoading: false,
),
);
}
},
),
),
);
}
@ -123,83 +146,88 @@ class CommandPaletteBloc
);
event.stream.listen(
onItems: (
List<SearchResponseItemPB> items,
String searchId,
bool isLoading,
) {
if (_isActiveSearch(searchId)) {
add(
CommandPaletteEvent.resultsChanged(
items: items,
searchId: searchId,
isLoading: isLoading,
),
);
}
},
onSummaries: (
List<SearchSummaryPB> summaries,
String searchId,
bool isLoading,
) {
if (_isActiveSearch(searchId)) {
add(
CommandPaletteEvent.resultsChanged(
summaries: summaries,
searchId: searchId,
isLoading: isLoading,
),
);
}
},
onFinished: (String searchId) {
if (_isActiveSearch(searchId)) {
add(
CommandPaletteEvent.resultsChanged(
searchId: searchId,
isLoading: false,
),
);
}
},
onLocalItems: (items, searchId) => _handleResultsUpdate(
searchId: searchId,
localItems: items,
),
onServerItems: (items, searchId, isLoading) => _handleResultsUpdate(
searchId: searchId,
serverItems: items,
isLoading: isLoading,
),
onSummaries: (summaries, searchId, isLoading) => _handleResultsUpdate(
searchId: searchId,
summaries: summaries,
isLoading: isLoading,
),
onFinished: (searchId) => _handleResultsUpdate(
searchId: searchId,
isLoading: false,
),
);
}
void _handleResultsUpdate({
required String searchId,
List<SearchResponseItemPB>? serverItems,
List<LocalSearchResponseItemPB>? localItems,
List<SearchSummaryPB>? summaries,
bool isLoading = true,
}) {
if (_isActiveSearch(searchId)) {
add(
CommandPaletteEvent.resultsChanged(
searchId: searchId,
serverItems: serverItems,
localItems: localItems,
summaries: summaries,
isLoading: isLoading,
),
);
}
}
FutureOr<void> _onResultsChanged(
_ResultsChanged event,
Emitter<CommandPaletteState> emit,
) async {
// If query was updated since last emission, clear previous results.
if (state.query != _oldQuery) {
emit(
state.copyWith(
resultItems: [],
resultSummaries: [],
isLoading: event.isLoading,
),
);
_oldQuery = state.query;
}
// Check for outdated search streams
if (state.searchId != event.searchId) return;
final updatedItems =
event.items ?? List<SearchResponseItemPB>.from(state.resultItems);
final updatedSummaries =
event.summaries ?? List<SearchSummaryPB>.from(state.resultSummaries);
final combinedItems = <String, SearchResultItem>{};
for (final item in event.serverItems ?? state.serverResponseItems) {
combinedItems[item.id] = SearchResultItem(
id: item.id,
icon: item.icon,
displayName: item.displayName,
content: item.content,
workspaceId: item.workspaceId,
);
}
for (final item in event.localItems ?? state.localResponseItems) {
combinedItems.putIfAbsent(
item.id,
() => SearchResultItem(
id: item.id,
icon: item.icon,
displayName: item.displayName,
content: '',
workspaceId: item.workspaceId,
),
);
}
emit(
state.copyWith(
resultItems: updatedItems,
resultSummaries: updatedSummaries,
serverResponseItems: event.serverItems ?? state.serverResponseItems,
localResponseItems: event.localItems ?? state.localResponseItems,
resultSummaries: event.summaries ?? state.resultSummaries,
combinedResponseItems: combinedItems,
isLoading: event.isLoading,
),
);
}
// Update trash state and, in case of null, retry reading trash from the service
FutureOr<void> _onTrashChanged(
_TrashChanged event,
Emitter<CommandPaletteState> emit,
@ -216,7 +244,6 @@ class CommandPaletteBloc
}
}
// Update the workspace and clear current search results and query
FutureOr<void> _onWorkspaceChanged(
_WorkspaceChanged event,
Emitter<CommandPaletteState> emit,
@ -225,27 +252,20 @@ class CommandPaletteBloc
emit(
state.copyWith(
query: '',
resultItems: [],
serverResponseItems: [],
localResponseItems: [],
combinedResponseItems: {},
resultSummaries: [],
isLoading: false,
),
);
}
// Clear search state
FutureOr<void> _onClearSearch(
_ClearSearch event,
Emitter<CommandPaletteState> emit,
) {
emit(
state.copyWith(
query: '',
resultItems: [],
resultSummaries: [],
isLoading: false,
searchId: null,
),
);
emit(CommandPaletteState.initial().copyWith(trash: state.trash));
}
bool _isActiveSearch(String searchId) =>
@ -264,7 +284,8 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
const factory CommandPaletteEvent.resultsChanged({
required String searchId,
required bool isLoading,
List<SearchResponseItemPB>? items,
List<SearchResponseItemPB>? serverItems,
List<LocalSearchResponseItemPB>? localItems,
List<SearchSummaryPB>? summaries,
}) = _ResultsChanged;
@ -277,12 +298,30 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
const factory CommandPaletteEvent.clearSearch() = _ClearSearch;
}
class SearchResultItem {
const SearchResultItem({
required this.id,
required this.icon,
required this.content,
required this.displayName,
this.workspaceId,
});
final String id;
final String content;
final ResultIconPB icon;
final String displayName;
final String? workspaceId;
}
@freezed
class CommandPaletteState with _$CommandPaletteState {
const CommandPaletteState._();
const factory CommandPaletteState({
@Default(null) String? query,
@Default([]) List<SearchResponseItemPB> resultItems,
@Default([]) List<SearchResponseItemPB> serverResponseItems,
@Default([]) List<LocalSearchResponseItemPB> localResponseItems,
@Default({}) Map<String, SearchResultItem> combinedResponseItems,
@Default([]) List<SearchSummaryPB> resultSummaries,
@Default(null) SearchResponseStream? searchResponseStream,
required bool isLoading,
@ -290,6 +329,7 @@ class CommandPaletteState with _$CommandPaletteState {
@Default(null) String? searchId,
}) = _CommandPaletteState;
factory CommandPaletteState.initial() =>
const CommandPaletteState(isLoading: false);
factory CommandPaletteState.initial() => const CommandPaletteState(
isLoading: false,
);
}

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:flutter/foundation.dart';
@ -13,6 +14,7 @@ class SearchResultListBloc
// Register event handlers
on<_OnHoverSummary>(_onHoverSummary);
on<_OnHoverResult>(_onHoverResult);
on<_OpenPage>(_onOpenPage);
}
FutureOr<void> _onHoverSummary(
@ -23,6 +25,7 @@ class SearchResultListBloc
state.copyWith(
hoveredSummary: event.summary,
hoveredResult: null,
openPageId: null,
),
);
}
@ -35,9 +38,17 @@ class SearchResultListBloc
state.copyWith(
hoveredSummary: null,
hoveredResult: event.item,
openPageId: null,
),
);
}
FutureOr<void> _onOpenPage(
_OpenPage event,
Emitter<SearchResultListState> emit,
) {
emit(state.copyWith(openPageId: event.pageId));
}
}
@freezed
@ -46,8 +57,12 @@ class SearchResultListEvent with _$SearchResultListEvent {
required SearchSummaryPB summary,
}) = _OnHoverSummary;
const factory SearchResultListEvent.onHoverResult({
required SearchResponseItemPB item,
required SearchResultItem item,
}) = _OnHoverResult;
const factory SearchResultListEvent.openPage({
required String pageId,
}) = _OpenPage;
}
@freezed
@ -55,7 +70,8 @@ class SearchResultListState with _$SearchResultListState {
const SearchResultListState._();
const factory SearchResultListState({
@Default(null) SearchSummaryPB? hoveredSummary,
@Default(null) SearchResponseItemPB? hoveredResult,
@Default(null) SearchResultItem? hoveredResult,
@Default(null) String? openPageId,
}) = _SearchResultListState;
factory SearchResultListState.initial() => const SearchResultListState();

View File

@ -50,12 +50,18 @@ class SearchResponseStream {
List<SearchResponseItemPB> items,
String searchId,
bool isLoading,
)? _onItems;
)? _onServerItems;
void Function(
List<SearchSummaryPB> summaries,
String searchId,
bool isLoading,
)? _onSummaries;
void Function(
List<LocalSearchResponseItemPB> items,
String searchId,
)? _onLocalItems;
void Function(String searchId)? _onFinished;
int get nativePort => _port.sendPort.nativePort;
@ -65,21 +71,28 @@ class SearchResponseStream {
}
void _onResultsChanged(Uint8List data) {
final response = SearchResponsePB.fromBuffer(data);
final searchState = SearchStatePB.fromBuffer(data);
if (response.hasResult()) {
if (response.result.hasSearchResult()) {
_onItems?.call(
response.result.searchResult.items,
if (searchState.hasResponse()) {
if (searchState.response.hasSearchResult()) {
_onServerItems?.call(
searchState.response.searchResult.items,
searchId,
response.isLoading,
searchState.isLoading,
);
}
if (response.result.hasSearchSummary()) {
if (searchState.response.hasSearchSummary()) {
_onSummaries?.call(
response.result.searchSummary.items,
searchState.response.searchSummary.items,
searchId,
searchState.isLoading,
);
}
if (searchState.response.hasLocalSearchResult()) {
_onLocalItems?.call(
searchState.response.localSearchResult.items,
searchId,
response.isLoading,
);
}
} else {
@ -92,16 +105,21 @@ class SearchResponseStream {
List<SearchResponseItemPB> items,
String searchId,
bool isLoading,
)? onItems,
)? onServerItems,
required void Function(
List<SearchSummaryPB> summaries,
String searchId,
bool isLoading,
)? onSummaries,
required void Function(
List<LocalSearchResponseItemPB> items,
String searchId,
)? onLocalItems,
required void Function(String searchId)? onFinished,
}) {
_onItems = onItems;
_onServerItems = onServerItems;
_onSummaries = onSummaries;
_onLocalItems = onLocalItems;
_onFinished = onFinished;
}
}

View File

@ -153,13 +153,13 @@ class CommandPaletteModal extends StatelessWidget {
),
),
],
if (state.resultItems.isNotEmpty &&
if (state.combinedResponseItems.isNotEmpty &&
(state.query?.isNotEmpty ?? false)) ...[
const Divider(height: 0),
Flexible(
child: SearchResultList(
trash: state.trash,
resultItems: state.resultItems,
resultItems: state.combinedResponseItems.values.toList(),
resultSummaries: state.resultSummaries,
),
),

View File

@ -4,7 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_view_tile.dart';
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_recent_view_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
@ -52,7 +52,7 @@ class RecentViewsList extends StatelessWidget {
)
: FlowySvg(view.iconData, size: const Size.square(20));
return RecentViewTile(
return SearchRecentViewCell(
icon: SizedBox(width: 24, child: icon),
view: view,
onSelected: onSelected,

View File

@ -7,8 +7,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
class RecentViewTile extends StatelessWidget {
const RecentViewTile({
class SearchRecentViewCell extends StatelessWidget {
const SearchRecentViewCell({
super.key,
required this.icon,
required this.view,

View File

@ -1,11 +1,8 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/string_extension.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';
import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
@ -19,12 +16,10 @@ class SearchResultCell extends StatefulWidget {
const SearchResultCell({
super.key,
required this.item,
required this.onSelected,
this.isTrashed = false,
});
final SearchResponseItemPB item;
final VoidCallback onSelected;
final SearchResultItem item;
final bool isTrashed;
@override
@ -43,12 +38,9 @@ class _SearchResultCellState extends State<SearchResultCell> {
/// Helper to handle the selection action.
void _handleSelection() {
widget.onSelected();
getIt<ActionNavigationBloc>().add(
ActionNavigationEvent.performAction(
action: NavigationAction(objectId: widget.item.id),
),
);
context.read<SearchResultListBloc>().add(
SearchResultListEvent.openPage(pageId: widget.item.id),
);
}
/// Helper to clean up preview text.
@ -62,7 +54,7 @@ class _SearchResultCellState extends State<SearchResultCell> {
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
);
final icon = widget.item.icon.getIcon();
final cleanedPreview = _cleanPreview(widget.item.preview);
final cleanedPreview = _cleanPreview(widget.item.content);
final hasPreview = cleanedPreview.isNotEmpty;
final trashHintText =
widget.isTrashed ? LocaleKeys.commandPalette_fromTrashHint.tr() : null;
@ -208,7 +200,7 @@ class SearchResultPreview extends StatelessWidget {
required this.data,
});
final SearchResponseItemPB data;
final SearchResultItem data;
@override
Widget build(BuildContext context) {

View File

@ -1,3 +1,7 @@
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart';
import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart';
import 'package:flutter/material.dart';
@ -20,9 +24,8 @@ class SearchResultList extends StatelessWidget {
});
final List<TrashPB> trash;
final List<SearchResponseItemPB> resultItems;
final List<SearchResultItem> resultItems;
final List<SearchSummaryPB> resultSummaries;
Widget _buildSectionHeader(String title) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8) +
const EdgeInsets.only(left: 8),
@ -65,7 +68,6 @@ class SearchResultList extends StatelessWidget {
final item = resultItems[index];
return SearchResultCell(
item: item,
onSelected: () => FlowyOverlay.pop(context),
isTrashed: trash.any((t) => t.id == item.id),
);
},
@ -80,33 +82,46 @@ class SearchResultList extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 6),
child: BlocProvider(
create: (context) => SearchResultListBloc(),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 7,
child: ListView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
children: [
if (resultSummaries.isNotEmpty) _buildSummariesSection(),
const VSpace(10),
if (resultItems.isNotEmpty) _buildResultsSection(context),
],
),
),
const HSpace(10),
Flexible(
flex: 3,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
child: BlocListener<SearchResultListBloc, SearchResultListState>(
listener: (context, state) {
if (state.openPageId != null) {
FlowyOverlay.pop(context);
getIt<ActionNavigationBloc>().add(
ActionNavigationEvent.performAction(
action: NavigationAction(objectId: state.openPageId!),
),
);
}
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Flexible(
flex: 7,
child: ListView(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
children: [
if (resultSummaries.isNotEmpty) _buildSummariesSection(),
const VSpace(10),
if (resultItems.isNotEmpty) _buildResultsSection(context),
],
),
child: const SearchCellPreview(),
),
),
],
const HSpace(10),
if (resultItems.any((item) => item.content.isNotEmpty))
Flexible(
flex: 3,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
child: const SearchCellPreview(),
),
),
],
),
),
),
);

View File

@ -36,7 +36,7 @@ class SearchSummaryCell extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: FlowyText(
summary.content,
maxLines: 3,
maxLines: 20,
),
),
);
@ -78,14 +78,19 @@ class SearchSummarySource extends StatelessWidget {
@override
Widget build(BuildContext context) {
final icon = source.icon.getIcon();
return Row(
children: [
if (icon != null) ...[
SizedBox(width: 24, child: icon),
const HSpace(6),
],
FlowyText(source.displayName),
],
return SizedBox(
height: 30,
child: FlowyButton(
leftIcon: icon,
hoverColor:
Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
text: FlowyText(source.displayName),
onTap: () {
context.read<SearchResultListBloc>().add(
SearchResultListEvent.openPage(pageId: source.id),
);
},
),
);
}
}

View File

@ -2694,8 +2694,8 @@
"placeholder": "Search or ask a question...",
"bestMatches": "Best matches",
"aiOverview": "AI overview",
"aiOverviewSource": "Sources",
"pagePreview": "Preview",
"aiOverviewSource": "Reference sources",
"pagePreview": "Content preview",
"recentHistory": "Recent history",
"navigateHint": "to navigate",
"loadingTooltip": "We are looking for results...",

View File

@ -71,10 +71,10 @@ impl LocalAIResourceController {
) -> Self {
let (resource_notify, _) = tokio::sync::broadcast::channel(1);
let (app_state_sender, _) = tokio::sync::broadcast::channel(1);
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
let mut offline_app_disk_watch: Option<WatchContext> = None;
#[cfg(target_os = "macos")]
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
match watch_offline_app() {
Ok((new_watcher, mut rx)) => {

View File

@ -1,9 +1,9 @@
use crate::entities::{
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB, SearchResultPB,
SearchSourcePB, SearchSummaryPB,
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, RepeatedSearchSummaryPB,
SearchResponsePB, SearchSourcePB, SearchSummaryPB,
};
use crate::{
entities::{IndexTypePB, ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB},
entities::{ResultIconPB, ResultIconTypePB, SearchFilterPB, SearchResponseItemPB},
services::manager::{SearchHandler, SearchType},
};
use async_stream::stream;
@ -45,7 +45,7 @@ impl SearchHandler for DocumentSearchHandler {
&self,
query: String,
filter: Option<SearchFilterPB>,
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>> {
let cloud_service = self.cloud_service.clone();
let folder_manager = self.folder_manager.clone();
@ -99,13 +99,10 @@ impl SearchHandler for DocumentSearchHandler {
for item in &result_items {
if let Some(view) = views.iter().find(|v| v.id == item.object_id.to_string()) {
items.push(SearchResponseItemPB {
index_type: IndexTypePB::Document,
id: item.object_id.to_string(),
display_name: view.name.clone(),
icon: extract_icon(view),
score: item.score,
workspace_id: item.workspace_id.to_string(),
preview: item.preview.clone(),
content: item.content.clone()}
);
} else {
@ -133,15 +130,11 @@ impl SearchHandler for DocumentSearchHandler {
let sources: Vec<SearchSourcePB> = v.sources
.iter()
.flat_map(|id| {
if let Some(view) = views.iter().find(|v| v.id == id.to_string()) {
Some(SearchSourcePB {
views.iter().find(|v| v.id == id.to_string()).map(|view| SearchSourcePB {
id: id.to_string(),
display_name: view.name.clone(),
icon: extract_icon(view),
})
} else {
None
}
})
.collect();

View File

@ -1,31 +0,0 @@
use flowy_derive::ProtoBuf_Enum;
#[derive(ProtoBuf_Enum, Eq, PartialEq, Debug, Clone)]
pub enum IndexTypePB {
View = 0,
Document = 1,
DocumentBlock = 2,
DatabaseRow = 3,
}
impl Default for IndexTypePB {
fn default() -> Self {
Self::View
}
}
impl std::convert::From<IndexTypePB> for i32 {
fn from(notification: IndexTypePB) -> Self {
notification as i32
}
}
impl std::convert::From<i32> for IndexTypePB {
fn from(notification: i32) -> Self {
match notification {
1 => IndexTypePB::View,
2 => IndexTypePB::DocumentBlock,
_ => IndexTypePB::DatabaseRow,
}
}
}

View File

@ -1,10 +1,8 @@
mod index_type;
mod notification;
mod query;
mod result;
mod search_filter;
pub use index_type::*;
pub use notification::*;
pub use query::*;
pub use result::*;

View File

@ -1,10 +1,10 @@
use super::SearchResultPB;
use super::SearchResponsePB;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchResponsePB {
pub struct SearchStatePB {
#[pb(index = 1, one_of)]
pub result: Option<SearchResultPB>,
pub response: Option<SearchResponsePB>,
#[pb(index = 2)]
pub search_id: String,

View File

@ -1,4 +1,3 @@
use super::IndexTypePB;
use collab_folder::{IconType, ViewIcon};
use derive_builder::Builder;
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
@ -7,7 +6,7 @@ use flowy_folder::entities::ViewIconPB;
#[derive(Debug, Default, ProtoBuf, Builder, Clone)]
#[builder(name = "CreateSearchResultPBArgs")]
#[builder(pattern = "mutable")]
pub struct SearchResultPB {
pub struct SearchResponsePB {
#[pb(index = 1, one_of)]
#[builder(default)]
pub search_result: Option<RepeatedSearchResponseItemPB>,
@ -15,6 +14,10 @@ pub struct SearchResultPB {
#[pb(index = 2, one_of)]
#[builder(default)]
pub search_summary: Option<RepeatedSearchSummaryPB>,
#[pb(index = 3, one_of)]
#[builder(default)]
pub local_search_result: Option<RepeatedLocalSearchResponseItemPB>,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
@ -53,43 +56,40 @@ pub struct RepeatedSearchResponseItemPB {
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct SearchResponseItemPB {
#[pb(index = 1)]
pub index_type: IndexTypePB,
#[pb(index = 2)]
pub id: String,
#[pb(index = 3)]
#[pb(index = 2)]
pub display_name: String,
#[pb(index = 4, one_of)]
#[pb(index = 3, one_of)]
pub icon: Option<ResultIconPB>,
#[pb(index = 5)]
pub score: f64,
#[pb(index = 6)]
#[pb(index = 4)]
pub workspace_id: String,
#[pb(index = 7, one_of)]
pub preview: Option<String>,
#[pb(index = 8)]
#[pb(index = 5)]
pub content: String,
}
impl SearchResponseItemPB {
pub fn with_score(&self, score: f64) -> Self {
SearchResponseItemPB {
index_type: self.index_type.clone(),
id: self.id.clone(),
display_name: self.display_name.clone(),
icon: self.icon.clone(),
score,
workspace_id: self.workspace_id.clone(),
preview: self.preview.clone(),
content: self.content.clone(),
}
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct RepeatedLocalSearchResponseItemPB {
#[pb(index = 1)]
pub items: Vec<LocalSearchResponseItemPB>,
}
#[derive(ProtoBuf, Default, Debug, Clone)]
pub struct LocalSearchResponseItemPB {
#[pb(index = 1)]
pub id: String,
#[pb(index = 2)]
pub display_name: String,
#[pb(index = 3, one_of)]
pub icon: Option<ResultIconPB>,
#[pb(index = 4)]
pub workspace_id: String,
}
#[derive(ProtoBuf_Enum, Clone, Debug, PartialEq, Eq, Default)]

View File

@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::entities::{IndexTypePB, ResultIconPB, SearchResponseItemPB};
use crate::entities::{LocalSearchResponseItemPB, ResultIconPB};
#[derive(Debug, Serialize, Deserialize)]
pub struct FolderIndexData {
@ -11,7 +11,7 @@ pub struct FolderIndexData {
pub workspace_id: String,
}
impl From<FolderIndexData> for SearchResponseItemPB {
impl From<FolderIndexData> for LocalSearchResponseItemPB {
fn from(data: FolderIndexData) -> Self {
let icon = if data.icon.is_empty() {
None
@ -23,14 +23,10 @@ impl From<FolderIndexData> for SearchResponseItemPB {
};
Self {
index_type: IndexTypePB::View,
id: data.id,
display_name: data.title,
score: 0.0,
icon,
workspace_id: data.workspace_id,
preview: None,
content: "".to_string(),
}
}
}

View File

@ -1,6 +1,6 @@
use super::indexer::FolderIndexManagerImpl;
use crate::entities::{
CreateSearchResultPBArgs, RepeatedSearchResponseItemPB, SearchFilterPB, SearchResultPB,
CreateSearchResultPBArgs, RepeatedLocalSearchResponseItemPB, SearchFilterPB, SearchResponsePB,
};
use crate::services::manager::{SearchHandler, SearchType};
use async_stream::stream;
@ -30,7 +30,7 @@ impl SearchHandler for FolderSearchHandler {
&self,
query: String,
filter: Option<SearchFilterPB>,
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>> {
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>> {
let index_manager = self.index_manager.clone();
Box::pin(stream! {
@ -48,8 +48,8 @@ impl SearchHandler for FolderSearchHandler {
}
// Build the search result.
let search_result = RepeatedSearchResponseItemPB {items};
yield Ok(CreateSearchResultPBArgs::default().search_result(Some(search_result)).build().unwrap())
let search_result = RepeatedLocalSearchResponseItemPB {items};
yield Ok(CreateSearchResultPBArgs::default().local_search_result(Some(search_result)).build().unwrap())
})
}
}

View File

@ -1,9 +1,6 @@
use crate::{
entities::SearchResponseItemPB,
folder::schema::{
FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME,
FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME,
},
use crate::folder::schema::{
FolderSchema, FOLDER_ICON_FIELD_NAME, FOLDER_ICON_TY_FIELD_NAME, FOLDER_ID_FIELD_NAME,
FOLDER_TITLE_FIELD_NAME, FOLDER_WORKSPACE_ID_FIELD_NAME,
};
use collab::core::collab::{IndexContent, IndexContentReceiver};
use collab_folder::{folder_diff::FolderViewChange, View, ViewIcon, ViewIndexContent, ViewLayout};
@ -14,9 +11,8 @@ use std::sync::{Arc, Weak};
use std::{collections::HashMap, fs};
use super::entities::FolderIndexData;
use crate::entities::ResultIconTypePB;
use crate::entities::{LocalSearchResponseItemPB, ResultIconTypePB};
use lib_infra::async_trait::async_trait;
use strsim::levenshtein;
use tantivy::{
collector::TopDocs, directory::MmapDirectory, doc, query::QueryParser, schema::Field, Document,
Index, IndexReader, IndexWriter, TantivyDocument, Term,
@ -113,11 +109,6 @@ impl FolderIndexManagerImpl {
(icon, icon_ty)
}
fn score_result(&self, query: &str, term: &str) -> f64 {
let distance = levenshtein(query, term) as f64;
1.0 / (distance + 1.0)
}
/// Simple implementation to index all given data by spawning async tasks.
fn index_all(&self, data_vec: Vec<IndexableData>) -> Result<(), FlowyError> {
for data in data_vec {
@ -130,7 +121,7 @@ impl FolderIndexManagerImpl {
}
/// Searches the index using the given query string.
pub async fn search(&self, query: String) -> Result<Vec<SearchResponseItemPB>, FlowyError> {
pub async fn search(&self, query: String) -> Result<Vec<LocalSearchResponseItemPB>, FlowyError> {
let lock = self.state.read().await;
let state = lock
.as_ref()
@ -157,8 +148,8 @@ impl FolderIndexManagerImpl {
}
if !content.is_empty() {
let s = serde_json::to_string(&content)?;
let result: SearchResponseItemPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
results.push(result.with_score(self.score_result(&query, &result.display_name)));
let result: LocalSearchResponseItemPB = serde_json::from_str::<FolderIndexData>(&s)?.into();
results.push(result);
}
}
@ -200,11 +191,11 @@ impl IndexManager for FolderIndexManagerImpl {
})
.await;
},
Err(err) => tracing::error!("FolderIndexManager error deserialize (update): {:?}", err),
Err(err) => error!("FolderIndexManager error deserialize (update): {:?}", err),
},
IndexContent::Delete(ids) => {
if let Err(e) = indexer.remove_indices(ids).await {
tracing::error!("FolderIndexManager error (delete): {:?}", e);
error!("FolderIndexManager error (delete): {:?}", e);
}
},
}

View File

@ -1,4 +1,4 @@
use crate::entities::{SearchFilterPB, SearchResponsePB, SearchResultPB};
use crate::entities::{SearchFilterPB, SearchResponsePB, SearchStatePB};
use allo_isolate::Isolate;
use flowy_error::FlowyResult;
use lib_infra::async_trait::async_trait;
@ -25,7 +25,7 @@ pub trait SearchHandler: Send + Sync + 'static {
&self,
query: String,
filter: Option<SearchFilterPB>,
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResultPB>> + Send + 'static>>;
) -> Pin<Box<dyn Stream<Item = FlowyResult<SearchResponsePB>> + Send + 'static>>;
}
/// The [SearchManager] is used to inject multiple [SearchHandler]'s
@ -34,7 +34,7 @@ pub trait SearchHandler: Send + Sync + 'static {
///
pub struct SearchManager {
pub handlers: HashMap<SearchType, Arc<dyn SearchHandler>>,
current_search: Arc<tokio::sync::Mutex<Option<String>>>, // Track current search
current_search: Arc<tokio::sync::Mutex<Option<String>>>,
}
impl SearchManager {
@ -84,23 +84,21 @@ impl SearchManager {
}
let mut stream = handler.perform_search(query.clone(), filter).await;
while let Some(result) = stream.next().await {
while let Some(Ok(search_result)) = stream.next().await {
if !is_current_search(&current_search, &search_id).await {
trace!("[Search] discard search stream: {}", query);
return;
}
if let Ok(result) = result {
let resp = SearchResponsePB {
result: Some(result),
search_id: search_id.clone(),
is_loading: true,
};
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
if let Err(err) = clone_sink.send(data).await {
error!("Failed to send search result: {}", err);
break;
}
let resp = SearchStatePB {
response: Some(search_result),
search_id: search_id.clone(),
is_loading: true,
};
if let Ok::<Vec<u8>, _>(data) = resp.try_into() {
if let Err(err) = clone_sink.send(data).await {
error!("Failed to send search result: {}", err);
break;
}
}
}
@ -110,8 +108,8 @@ impl SearchManager {
return;
}
let resp = SearchResponsePB {
result: None,
let resp = SearchStatePB {
response: None,
search_id: search_id.clone(),
is_loading: true,
};