Skip to content

Commit

Permalink
💥 Adds matchMethod to SearchQuery (#104)
Browse files Browse the repository at this point in the history
* Adds MatchMethod to SearchQuery

* Fixes warnings when building documentation
  • Loading branch information
simonbs authored Jun 25, 2022
1 parent e2cbd9d commit 9f06c90
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 27 deletions.
21 changes: 7 additions & 14 deletions Sources/Runestone/TextView/SearchReplace/SearchController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ final class SearchController {
}

func search(for query: SearchQuery, replacingMatchesWith replacementText: String) -> [SearchReplaceResult] {
guard query.isRegularExpression else {
guard query.matchMethod == .regularExpression else {
return search(for: query, replacingWithPlainText: replacementText)
}
let replacementStringParser = ReplacementStringParser(string: replacementText)
Expand All @@ -46,20 +46,13 @@ private extension SearchController {
guard !query.text.isEmpty else {
return []
}
do {
let regex = try query.makeRegularExpression()
let range = NSRange(location: 0, length: stringView.string.length)
let matches = regex.matches(in: stringView.string as String, options: [], range: range)
var searchResults: [T] = []
for match in matches where match.range.length > 0 {
if let searchResult = mapper(match) {
searchResults.append(searchResult)
}
let matches = query.matches(in: stringView.string)
return matches.compactMap { textCheckingResult in
if textCheckingResult.range.length > 0, let mappedValue = mapper(textCheckingResult) {
return mappedValue
} else {
return nil
}
return searchResults
} catch {
print(error)
return []
}
}

Expand Down
61 changes: 48 additions & 13 deletions Sources/Runestone/TextView/SearchReplace/SearchQuery.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,46 @@ import Foundation
///
/// When the query contains a regular expression the capture groups can be referred in a replacement text using $0, $1, $2 etc.
public struct SearchQuery: Hashable, Equatable {
/// The text to search for. May be a regular expression if ``SearchQuery/isRegularExpression`` is `true`.
/// Strategy to use when matching the search text against the text in the text view.
public enum MatchMethod {
/// Word contains the search text.
case contains
/// Word matches the search text.
case fullWord
/// Word starts with the search text.
case startsWith
/// Word ends with the search text.
case endsWith
/// Treat the search text as a regular expression.
case regularExpression
}

/// The text to search for.
public let text: String
/// Whether the text is a regular exprssion.
public let isRegularExpression: Bool
public let matchMethod: MatchMethod
/// Whether to perform a case-sensitive search.
public let isCaseSensitive: Bool

private var regularExpressionOptions: NSRegularExpression.Options {
var options: NSRegularExpression.Options = []
if isRegularExpression {
options.insert(.anchorsMatchLines)
} else {
options.insert(.ignoreMetacharacters)
private var annotatedText: String {
switch matchMethod {
case .fullWord:
return "\\b\(escapedText)\\b"
case .startsWith:
return "\\b\(escapedText)"
case .endsWith:
return "\(escapedText)\\b"
case .contains:
return escapedText
case .regularExpression:
return text
}
}
private var escapedText: String {
return NSRegularExpression.escapedPattern(for: text)
}
private var regularExpressionOptions: NSRegularExpression.Options {
var options: NSRegularExpression.Options = [.anchorsMatchLines]
if !isCaseSensitive {
options.insert(.caseInsensitive)
}
Expand All @@ -29,15 +55,24 @@ public struct SearchQuery: Hashable, Equatable {
/// Creates a query to search for in the text view.
/// - Parameters:
/// - text: The text to search for. May be a regular expression if `isRegularExpression` is `true`.
/// - isRegularExpression: Whether the text is a regular exprssion.
/// - matchMethod: Strategy to use when matching the search text against the text in the text view. Defaults to `contains`.
/// - isCaseSensitive: Whether to perform a case-sensitive search.
public init(text: String, isRegularExpression: Bool = false, isCaseSensitive: Bool = false) {
public init(text: String, matchMethod: MatchMethod = .contains, isCaseSensitive: Bool = false) {
self.text = text
self.isRegularExpression = isRegularExpression
self.matchMethod = matchMethod
self.isCaseSensitive = isCaseSensitive
}

func makeRegularExpression() throws -> NSRegularExpression {
return try NSRegularExpression(pattern: text, options: regularExpressionOptions)
func matches(in string: NSString) -> [NSTextCheckingResult] {
do {
let regex = try NSRegularExpression(pattern: annotatedText, options: regularExpressionOptions)
let range = NSRange(location: 0, length: string.length)
return regex.matches(in: string as String, range: range)
} catch {
#if DEBUG
print(error)
#endif
return []
}
}
}
53 changes: 53 additions & 0 deletions Tests/RunestoneTests/SearchQueryTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
@testable import Runestone
import XCTest

final class SearchQueryTests: XCTestCase {
private let sampleText: NSString = """
/**
* This is a Runestone text view with syntax highlighting
* for the JavaScript programming language.
*/
let names = ["Steve Jobs", "Tim Cook", "Eddy Cue"]
let years = [1955, 1960, 1964]
printNamesAndYears(names, years)
// Print the year each person was born.
function printNamesAndYears(names, years) {
for (let i = 0; i < names.length; i++) {
console.log(names[i] + " was born in " + years[i])
}
}
"""

func testContainsMatchMethod() {
let searchQuery = SearchQuery(text: "names", matchMethod: .contains)
let ranges = searchQuery.matches(in: sampleText)
XCTAssertEqual(ranges.count, 7)
}

func testFullWordMatchMethod() {
let searchQuery = SearchQuery(text: "names", matchMethod: .fullWord)
let ranges = searchQuery.matches(in: sampleText)
XCTAssertEqual(ranges.count, 5)
}

func testStartsWithMatchMethod() {
let searchQuery = SearchQuery(text: "nam", matchMethod: .startsWith)
let ranges = searchQuery.matches(in: sampleText)
XCTAssertEqual(ranges.count, 5)
}

func testEndsWithMatchMethod() {
let searchQuery = SearchQuery(text: "rs", matchMethod: .endsWith)
let ranges = searchQuery.matches(in: sampleText)
XCTAssertEqual(ranges.count, 6)
}

func testRegularExpressionMatchMethod() {
// Matches strings containing the names.
let searchQuery = SearchQuery(text: "\"[A-Z][a-z]+ [A-Z][a-z]+\"", matchMethod: .regularExpression)
let ranges = searchQuery.matches(in: sampleText)
XCTAssertEqual(ranges.count, 3)
}
}

0 comments on commit 9f06c90

Please sign in to comment.