Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

💥 Adds matchMethod to SearchQuery #104

Merged
merged 2 commits into from
Jun 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}
}