diff --git a/Core/BookmarksCachingSearch.swift b/Core/BookmarksCachingSearch.swift index 089eec2699..a219442954 100644 --- a/Core/BookmarksCachingSearch.swift +++ b/Core/BookmarksCachingSearch.swift @@ -133,12 +133,7 @@ public class BookmarksCachingSearch: BookmarksStringSearch { self.title = title self.url = url self.isFavorite = isFavorite - - if isFavorite { - score = 0 - } else { - score = -1 - } + self.score = 0 } init?(bookmark: [String: Any]) { @@ -191,7 +186,6 @@ public class BookmarksCachingSearch: BookmarksStringSearch { return cachedBookmarksAndFavorites } - // swiftlint:disable cyclomatic_complexity private func score(query: String, input: [ScoredBookmark]) -> [ScoredBookmark] { let query = query.lowercased() let tokens = query.split(separator: " ").filter { !$0.isEmpty }.map { String($0).lowercased() } @@ -201,55 +195,63 @@ public class BookmarksCachingSearch: BookmarksStringSearch { for index in 0.. 0 { + result.append(input[index]) } + } + return result + } - let domain = entry.url.host?.droppingWwwPrefix() ?? "" + private func score(_ query: String, _ bookmark: ScoredBookmark, _ tokens: [String]) -> Int { + let title = bookmark.title.lowercased() + let domain = bookmark.url.host?.droppingWwwPrefix() ?? "" + var score = bookmark.isFavorite ? 0 : -1 - // Tokenized matches + // Exact matches - full query + if title.leadingBoundaryStartsWith(query) { // High score for exact match from the beginning of the title + score += 200 + } else if title.contains(" \(query)") { // Exact match from the beginning of the word within string. + score += 100 + } - if tokens.count > 1 { - var matchesAllTokens = true - for token in tokens { - // Match only from the beginning of the word to avoid unintuitive matches. - if !title.starts(with: token) && !title.contains(" \(token)") && !domain.starts(with: token) { - matchesAllTokens = false - break - } + // Tokenized matches + + if tokens.count > 1 { + var matchesAllTokens = true + for token in tokens { + // Match only from the beginning of the word to avoid unintuitive matches. + if !title.leadingBoundaryStartsWith(token) && + !title.contains(" \(token)") + && !domain.starts(with: token) { + matchesAllTokens = false + break } + } - if matchesAllTokens { - // Score tokenized matches - input[index].score += 10 - - // Boost score if first token matches: - if let firstToken = tokens.first { // domain - high score boost - if domain.starts(with: firstToken) { - input[index].score += 300 - } else if title.starts(with: firstToken) { // beginning of the title - moderate score boost - input[index].score += 50 - } + if matchesAllTokens { + // Score tokenized matches + score += 10 + + // Boost score if first token matches: + if let firstToken = tokens.first { // domain - high score boost + if domain.starts(with: firstToken) { + score += 300 + } else if title.leadingBoundaryStartsWith(firstToken) { // beginning of the title - moderate score boost + score += 50 } } - } else { - // High score for matching domain in the URL - if let firstToken = tokens.first, domain.starts(with: firstToken) { - input[index].score += 300 - } } - if input[index].score > 0 { - result.append(input[index]) + } else { + // High score for matching domain in the URL + if let firstToken = tokens.first, domain.starts(with: firstToken) { + score += 300 } } - return result + + return score } - // swiftlint:enable cyclomatic_complexity public func search(query: String) -> [BookmarksStringSearchResult] { guard hasData else { @@ -265,3 +267,12 @@ public class BookmarksCachingSearch: BookmarksStringSearch { return finalResult } } + +private extension String { + + /// e.g. "Cats and Dogs" would match `Cats` or `"Cats` + func leadingBoundaryStartsWith(_ s: String) -> Bool { + return starts(with: s) || trimmingCharacters(in: .alphanumerics.inverted).starts(with: s) + } + +} diff --git a/Core/FeatureFlag.swift b/Core/FeatureFlag.swift index 763b282091..6d5b363635 100644 --- a/Core/FeatureFlag.swift +++ b/Core/FeatureFlag.swift @@ -42,6 +42,7 @@ public enum FeatureFlag: String { case syncPromotionPasswords case onboardingHighlights case autofillSurveys + case autcompleteTabs } extension FeatureFlag: FeatureFlagSourceProviding { @@ -89,6 +90,8 @@ extension FeatureFlag: FeatureFlagSourceProviding { return .internalOnly case .autofillSurveys: return .remoteReleasable(.feature(.autofillSurveys)) + case .autcompleteTabs: + return .remoteReleasable(.feature(.autocompleteTabs)) } } } diff --git a/Core/PixelEvent.swift b/Core/PixelEvent.swift index 2f67e300f1..d7ff07905b 100644 --- a/Core/PixelEvent.swift +++ b/Core/PixelEvent.swift @@ -131,9 +131,11 @@ extension Pixel { case autocompleteClickFavorite case autocompleteClickSearchHistory case autocompleteClickSiteHistory + case autocompleteClickOpenTab case autocompleteDisplayedLocalBookmark case autocompleteDisplayedLocalFavorite case autocompleteDisplayedLocalHistory + case autocompleteDisplayedOpenedTab case autocompleteSwipeToDelete case feedbackPositive @@ -946,9 +948,11 @@ extension Pixel.Event { case .autocompleteClickFavorite: return "m_autocomplete_click_favorite" case .autocompleteClickSearchHistory: return "m_autocomplete_click_history_search" case .autocompleteClickSiteHistory: return "m_autocomplete_click_history_site" + case .autocompleteClickOpenTab: return "m_autocomplete_click_switch_to_tab" case .autocompleteDisplayedLocalBookmark: return "m_autocomplete_display_local_bookmark" case .autocompleteDisplayedLocalFavorite: return "m_autocomplete_display_local_favorite" case .autocompleteDisplayedLocalHistory: return "m_autocomplete_display_local_history" + case .autocompleteDisplayedOpenedTab: return "m_autocomplete_display_switch_to_tab" case .autocompleteSwipeToDelete: return "m_autocomplete_result_deleted" case .feedbackPositive: return "mfbs_positive_submit" diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 192ec061e5..c1fa3fd356 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10927,7 +10927,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 197.0.0; + version = 198.0.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8ef30ff3b2..db741141be 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "40f2fcc23944e028e16798a784ceff7e24ba6683", - "version" : "197.0.0" + "revision" : "6e1520bd83bbcc269b0d561c51fc92b81fe6d93b", + "version" : "198.0.0" } }, { @@ -138,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json new file mode 100644 index 0000000000..e83ff99786 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "OpenTab-24.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg new file mode 100644 index 0000000000..f00822f378 --- /dev/null +++ b/DuckDuckGo/Assets.xcassets/DesignSystemIcons/24px/OpenTab-24.imageset/OpenTab-24.svg @@ -0,0 +1,3 @@ + + + diff --git a/DuckDuckGo/AutocompleteView.swift b/DuckDuckGo/AutocompleteView.swift index f138157461..3003807655 100644 --- a/DuckDuckGo/AutocompleteView.swift +++ b/DuckDuckGo/AutocompleteView.swift @@ -249,6 +249,11 @@ private struct SuggestionView: View { title: title ?? "", subtitle: url.formattedForSuggestion()) + case .openTab(title: let title, url: let url): + SuggestionListItem(icon: Image("OpenTab-24"), + title: title, + subtitle: "\(UserText.autocompleteSwitchToTab) · \(url.formattedForSuggestion())") + case .internalPage, .unknown: FailedAssertionView("Unknown or unsupported suggestion type") } @@ -336,6 +341,7 @@ private extension URL { let string = absoluteString .dropping(prefix: "https://") .dropping(prefix: "http://") + .droppingWwwPrefix() return pathComponents.isEmpty ? string : string.dropping(suffix: "/") } diff --git a/DuckDuckGo/AutocompleteViewController.swift b/DuckDuckGo/AutocompleteViewController.swift index 0c680525da..7c09c4da6a 100644 --- a/DuckDuckGo/AutocompleteViewController.swift +++ b/DuckDuckGo/AutocompleteViewController.swift @@ -58,19 +58,33 @@ class AutocompleteViewController: UIHostingController { CachedBookmarks(bookmarksDatabase) }() + private lazy var openTabs: [BrowserTab] = { + tabsModel.tabs.compactMap { + guard let url = $0.link?.url else { return nil } + return OpenTab(title: $0.link?.displayTitle ?? "", url: url) + } + }() + private var lastResults: SuggestionResult? private var loader: SuggestionLoader? - private var historyMessageManager: HistoryMessageManager + private var tabsModel: TabsModel + private var featureFlagger: FeatureFlagger init(historyManager: HistoryManaging, bookmarksDatabase: CoreDataDatabase, appSettings: AppSettings, - historyMessageManager: HistoryMessageManager = HistoryMessageManager()) { + historyMessageManager: HistoryMessageManager = HistoryMessageManager(), + tabsModel: TabsModel, + featureFlagger: FeatureFlagger) { + + self.tabsModel = tabsModel self.historyManager = historyManager self.bookmarksDatabase = bookmarksDatabase self.appSettings = appSettings self.historyMessageManager = historyMessageManager + self.featureFlagger = featureFlagger + self.model = AutocompleteViewModel(isAddressBarAtBottom: appSettings.currentAddressBarPosition == .bottom, showMessage: historyManager.isHistoryFeatureEnabled() && historyMessageManager.shouldShow()) super.init(rootView: AutocompleteView(model: model)) @@ -119,6 +133,7 @@ class AutocompleteViewController: UIHostingController { var bookmark = false var favorite = false var history = false + var openTab = false lastResults?.all.forEach { switch $0 { @@ -132,6 +147,9 @@ class AutocompleteViewController: UIHostingController { case .historyEntry: history = true + case .openTab: + openTab = true + default: break } } @@ -148,6 +166,10 @@ class AutocompleteViewController: UIHostingController { Pixel.fire(pixel: .autocompleteDisplayedLocalHistory) } + if openTab { + Pixel.fire(pixel: .autocompleteDisplayedOpenedTab) + } + } private func cancelInFlightRequests() { @@ -158,7 +180,7 @@ class AutocompleteViewController: UIHostingController { private func requestSuggestions(query: String) { model.selection = nil - loader = SuggestionLoader(dataSource: self, urlFactory: { phrase in + loader = SuggestionLoader(urlFactory: { phrase in guard let url = URL(trimmedAddressBarString: phrase), let scheme = url.scheme, scheme.description.hasPrefix("http"), @@ -169,7 +191,7 @@ class AutocompleteViewController: UIHostingController { return url }) - loader?.getSuggestions(query: query) { [weak self] result, error in + loader?.getSuggestions(query: query, usingDataSource: self) { [weak self] result, error in guard let self, error == nil else { return } let updatedResults = result ?? .empty self.lastResults = updatedResults @@ -228,6 +250,9 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate { case .website: Pixel.fire(pixel: .autocompleteClickWebsite) + case .openTab: + Pixel.fire(pixel: .autocompleteClickOpenTab) + default: // NO-OP break @@ -259,6 +284,10 @@ extension AutocompleteViewController: AutocompleteViewModelDelegate { extension AutocompleteViewController: SuggestionLoadingDataSource { + var platform: Platform { + .mobile + } + func history(for suggestionLoading: Suggestions.SuggestionLoading) -> [HistorySuggestion] { return historyCoordinator.history ?? [] } @@ -271,6 +300,13 @@ extension AutocompleteViewController: SuggestionLoadingDataSource { return [] } + func openTabs(for suggestionLoading: any SuggestionLoading) -> [BrowserTab] { + if featureFlagger.isFeatureOn(.autcompleteTabs) { + return openTabs + } + return [] + } + func suggestionLoading(_ suggestionLoading: Suggestions.SuggestionLoading, suggestionDataFromUrl url: URL, withParameters parameters: [String: String], completion: @escaping (Data?, Error?) -> Void) { var queryURL = url parameters.forEach { @@ -298,3 +334,10 @@ extension HistoryEntry: HistorySuggestion { } } + +struct OpenTab: BrowserTab { + + let title: String + let url: URL + +} diff --git a/DuckDuckGo/MainViewController.swift b/DuckDuckGo/MainViewController.swift index 12e5ae1e7b..dd9e7efeb5 100644 --- a/DuckDuckGo/MainViewController.swift +++ b/DuckDuckGo/MainViewController.swift @@ -398,7 +398,9 @@ class MainViewController: UIViewController { SuggestionTrayViewController(coder: coder, favoritesViewModel: self.favoritesViewModel, bookmarksDatabase: self.bookmarksDatabase, - historyManager: self.historyManager) + historyManager: self.historyManager, + tabsModel: self.tabManager.model, + featureFlagger: self.featureFlagger) }) else { assertionFailure() return @@ -2088,16 +2090,26 @@ extension MainViewController: AutocompleteViewControllerDelegate { } else { Logger.lifecycle.error("Couldn‘t form URL for suggestion: \(phrase, privacy: .public)") } + case .website(url: let url): if url.isBookmarklet() { executeBookmarklet(url) } else { loadUrl(url) } + case .bookmark(_, url: let url, _, _): loadUrl(url) + case .historyEntry(_, url: let url, _): loadUrl(url) + + case .openTab(title: _, url: let url): + if homeViewController != nil, let tab = tabManager.model.currentTab { + self.closeTab(tab) + } + loadUrlInNewTab(url, reuseExisting: true, inheritedAttribution: .noAttribution) + case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } @@ -2119,6 +2131,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { viewCoordinator.omniBar.textField.text = title case .historyEntry(title: let title, _, _): viewCoordinator.omniBar.textField.text = title + case .openTab: break // no-op case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } @@ -2136,7 +2149,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { } case .website(url: let url): viewCoordinator.omniBar.textField.text = url.absoluteString - case .bookmark(title: let title, _, _, _): + case .bookmark(title: let title, _, _, _), .openTab(title: let title, url: _): viewCoordinator.omniBar.textField.text = title if title.hasPrefix(query) { viewCoordinator.omniBar.selectTextToEnd(query.count) @@ -2149,6 +2162,7 @@ extension MainViewController: AutocompleteViewControllerDelegate { if (title ?? url.absoluteString).hasPrefix(query) { viewCoordinator.omniBar.selectTextToEnd(query.count) } + case .unknown(value: let value), .internalPage(title: let value, url: _): assertionFailure("Unknown suggestion: \(value)") } diff --git a/DuckDuckGo/SuggestionTrayViewController.swift b/DuckDuckGo/SuggestionTrayViewController.swift index be3bc7a0c5..f71ee490f5 100644 --- a/DuckDuckGo/SuggestionTrayViewController.swift +++ b/DuckDuckGo/SuggestionTrayViewController.swift @@ -23,6 +23,7 @@ import Bookmarks import Suggestions import Persistence import History +import BrowserServicesKit class SuggestionTrayViewController: UIViewController { @@ -50,6 +51,8 @@ class SuggestionTrayViewController: UIViewController { private let bookmarksDatabase: CoreDataDatabase private let favoritesModel: FavoritesListInteracting private let historyManager: HistoryManaging + private let tabsModel: TabsModel + private let featureFlagger: FeatureFlagger var selectedSuggestion: Suggestion? { autocompleteController?.selectedSuggestion @@ -79,10 +82,12 @@ class SuggestionTrayViewController: UIViewController { } } - required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging) { + required init?(coder: NSCoder, favoritesViewModel: FavoritesListInteracting, bookmarksDatabase: CoreDataDatabase, historyManager: HistoryManaging, tabsModel: TabsModel, featureFlagger: FeatureFlagger) { self.favoritesModel = favoritesViewModel self.bookmarksDatabase = bookmarksDatabase self.historyManager = historyManager + self.tabsModel = tabsModel + self.featureFlagger = featureFlagger super.init(coder: coder) } @@ -236,8 +241,9 @@ class SuggestionTrayViewController: UIViewController { private func installAutocompleteSuggestions() { let controller = AutocompleteViewController(historyManager: historyManager, bookmarksDatabase: bookmarksDatabase, - appSettings: appSettings) - + appSettings: appSettings, + tabsModel: tabsModel, + featureFlagger: featureFlagger) install(controller: controller) controller.delegate = autocompleteDelegate controller.presentationDelegate = self diff --git a/DuckDuckGo/UserText.swift b/DuckDuckGo/UserText.swift index b586b9f55d..c8c2fe0910 100644 --- a/DuckDuckGo/UserText.swift +++ b/DuckDuckGo/UserText.swift @@ -1251,6 +1251,7 @@ But if you *do* want a peek under the hood, you can find more information about public static let autocompleteHistoryWarningTitle = NSLocalizedString("autocomplete.history.warning.title", value: "Same privacy.\nBetter search suggestions!", comment: "Title for message show in suggestions") public static let autocompleteHistoryWarningDescription = NSLocalizedString("autocomplete.history.warning.message", value: "Search suggestions now include your recently visited sites. Turn off in Settings, or clear anytime with the 🔥 Fire Button.", comment: "The message text shown in suggestions") public static let autocompleteSearchDuckDuckGo = NSLocalizedString("autocomplete.history.search.duckduckgo", value: "Search DuckDuckGo", comment: "Subtitle for search history items") + public static let autocompleteSwitchToTab = NSLocalizedString("autocomplete.switch.to.tab", value: "Switch to Tab", comment: "Switch to tab hint") // Site not working public static let siteNotWorkingTitle = NSLocalizedString("site.not.working.title", value: "Site not working? Let DuckDuckGo know.", comment: "Prompt asking user to send report to us if we suspect site may be broken") diff --git a/DuckDuckGo/en.lproj/Localizable.strings b/DuckDuckGo/en.lproj/Localizable.strings index affd90745c..2d4b428f71 100644 --- a/DuckDuckGo/en.lproj/Localizable.strings +++ b/DuckDuckGo/en.lproj/Localizable.strings @@ -250,6 +250,9 @@ /* Title for message show in suggestions */ "autocomplete.history.warning.title" = "Same privacy.\nBetter search suggestions!"; +/* Switch to tab hint */ +"autocomplete.switch.to.tab" = "Switch to Tab"; + /* Autoconsent for Cookie Management Setting state */ "autoconsent.disabled" = "Disabled"; diff --git a/DuckDuckGoTests/BookmarksCachingSearchTests.swift b/DuckDuckGoTests/BookmarksCachingSearchTests.swift index 03f79cb6d2..21545ad57e 100644 --- a/DuckDuckGoTests/BookmarksCachingSearchTests.swift +++ b/DuckDuckGoTests/BookmarksCachingSearchTests.swift @@ -46,7 +46,8 @@ class BookmarksCachingSearchTests: XCTestCase { let simpleStore = MockBookmarksSearchStore() let urlStore = MockBookmarksSearchStore() - + let quotedTitleStore = MockBookmarksSearchStore() + enum Entry: String { case b1 = "bookmark test 1" case b2 = "test bookmark 2" @@ -61,6 +62,9 @@ class BookmarksCachingSearchTests: XCTestCase { case urlExample2 = "Test E 2" case urlNasa = "Test N 1 Duck" case urlDDG = "Test D 1" + + case quotedTitle1 = "\"Cats and Dogs\"" + case quotedTitle2 = "«Рукописи не горят»: первый замысел" } private var mockObjectID: NSManagedObjectID! @@ -75,20 +79,29 @@ class BookmarksCachingSearchTests: XCTestCase { mockObjectID = BookmarkUtils.fetchRootFolder(inMemoryStore.viewContext)?.objectID XCTAssertNotNil(mockObjectID) - simpleStore.dataSet = [BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b1.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b2.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12a.rawValue, url: url, isFavorite: false), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f1.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f2.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12.rawValue, url: url, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12a.rawValue, url: url, isFavorite: true)] - + simpleStore.dataSet = [ + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b1.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b2.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.b12a.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f1.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f2.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12.rawValue, url: url, isFavorite: true), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.f12a.rawValue, url: url, isFavorite: true), + ] + urlStore.dataSet = [ BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlExample1.rawValue, url: URL(string: "https://example.com")!, isFavorite: true), BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlExample2.rawValue, url: URL(string: "https://example.com")!, isFavorite: true), BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlNasa.rawValue, url: URL(string: "https://www.nasa.gov")!, isFavorite: true), - BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlDDG.rawValue, url: url, isFavorite: true)] + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.urlDDG.rawValue, url: url, isFavorite: true), + ] + + quotedTitleStore.dataSet = [ + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.quotedTitle1.rawValue, url: url, isFavorite: false), + BookmarksCachingSearch.ScoredBookmark(objectID: mockObjectID, title: Entry.quotedTitle2.rawValue, url: url, isFavorite: false), + ] + } override func tearDown() { @@ -98,6 +111,34 @@ class BookmarksCachingSearchTests: XCTestCase { super.tearDown() } + func testWhenSearchingForCharactersThenCharactersAtTheStartAreMatched() async throws { + let engine = BookmarksCachingSearch(bookmarksStore: quotedTitleStore) + var bookmarks = engine.search(query: "\"") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "«") + XCTAssertEqual(bookmarks.count, 1) + } + + func testWhenSearchingForWordsAtStartWithQuotesThenWordsAreMatched() async throws { + + let engine = BookmarksCachingSearch(bookmarksStore: quotedTitleStore) + var bookmarks = engine.search(query: "Cats") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Р") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Ру") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Рук") + XCTAssertEqual(bookmarks.count, 1) + + bookmarks = engine.search(query: "Nope") + XCTAssertEqual(bookmarks.count, 0) + } + func testWhenSearchingThenOnlyBeginingsOfWordsAreMatched() async throws { let engine = BookmarksCachingSearch(bookmarksStore: simpleStore)