Extract TextFind to package

This commit is contained in:
1024jp 2024-06-28 12:46:30 +09:00
parent f26dc68477
commit 7577870fb1
19 changed files with 237 additions and 161 deletions

View File

@ -136,12 +136,8 @@
2A2179F61A07093B002C4AB1 /* SyntaxMap.json in Resources */ = {isa = PBXBuildFile; fileRef = 2A2179F51A07093B002C4AB1 /* SyntaxMap.json */; };
2A21E6732BB44D5E0054C8A1 /* DonationSettings.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2A21E6722BB44D5E0054C8A1 /* DonationSettings.xcstrings */; };
2A21E6742BB44D5E0054C8A1 /* DonationSettings.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2A21E6722BB44D5E0054C8A1 /* DonationSettings.xcstrings */; };
2A231A251E7B4EDC00C2A909 /* MultipleReplace+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A241E7B4EDC00C2A909 /* MultipleReplace+Codable.swift */; };
2A231A261E7B4EDC00C2A909 /* MultipleReplace+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A241E7B4EDC00C2A909 /* MultipleReplace+Codable.swift */; };
2A231A281E7BD82700C2A909 /* Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A271E7BD82700C2A909 /* Binding.swift */; };
2A231A291E7BD82700C2A909 /* Binding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A271E7BD82700C2A909 /* Binding.swift */; };
2A231A2D1E7BE8B700C2A909 /* FindProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A2C1E7BE8B700C2A909 /* FindProgress.swift */; };
2A231A2E1E7BE8B700C2A909 /* FindProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A2C1E7BE8B700C2A909 /* FindProgress.swift */; };
2A231A361E7C30F000C2A909 /* MultipleReplaceSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A351E7C30F000C2A909 /* MultipleReplaceSplitViewController.swift */; };
2A231A371E7C30F000C2A909 /* MultipleReplaceSplitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A351E7C30F000C2A909 /* MultipleReplaceSplitViewController.swift */; };
2A231A391E7C31F400C2A909 /* MultipleReplaceListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A231A381E7C31F400C2A909 /* MultipleReplaceListViewController.swift */; };
@ -189,8 +185,6 @@
2A33D07F1D1C75B8005977B9 /* SyntaxValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A33D07D1D1C75B8005977B9 /* SyntaxValidationView.swift */; };
2A341D1A281EE23C00B85CB6 /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A341D19281EE23C00B85CB6 /* UserActivity.swift */; };
2A341D1B281EE23C00B85CB6 /* UserActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A341D19281EE23C00B85CB6 /* UserActivity.swift */; };
2A3581981E597ECE00762AA5 /* MultipleReplace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3581971E597ECE00762AA5 /* MultipleReplace.swift */; };
2A3581991E597ECE00762AA5 /* MultipleReplace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3581971E597ECE00762AA5 /* MultipleReplace.swift */; };
2A359DFE1DAE93EE00FEF7AA /* NSWindow+Responder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A359DFD1DAE93EE00FEF7AA /* NSWindow+Responder.swift */; };
2A359DFF1DAE93EE00FEF7AA /* NSWindow+Responder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A359DFD1DAE93EE00FEF7AA /* NSWindow+Responder.swift */; };
2A3643E61E7C3D2400EA3CE8 /* ReplacementManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A3643E51E7C3D2400EA3CE8 /* ReplacementManager.swift */; };
@ -715,9 +709,9 @@
2AEC48341E641E4F00FB0F89 /* Snippet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEC48321E641E4F00FB0F89 /* Snippet.swift */; };
2AEC69C41D41A1BE0089F96F /* EditorTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEC69C31D41A1BE0089F96F /* EditorTextView.swift */; };
2AEC69C51D41A1BE0089F96F /* EditorTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEC69C31D41A1BE0089F96F /* EditorTextView.swift */; };
2AED46701E425CD200751C45 /* TextFind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED466F1E425CD200751C45 /* TextFind.swift */; };
2AED46711E425CD200751C45 /* TextFind.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED466F1E425CD200751C45 /* TextFind.swift */; };
2AED46731E43942300751C45 /* TextFindTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED46721E43942300751C45 /* TextFindTests.swift */; };
2AED46701E425CD200751C45 /* TextFind.Error+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED466F1E425CD200751C45 /* TextFind.Error+Localization.swift */; };
2AED46711E425CD200751C45 /* TextFind.Error+Localization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED466F1E425CD200751C45 /* TextFind.Error+Localization.swift */; };
2AED46731E43942300751C45 /* TextFinderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED46721E43942300751C45 /* TextFinderTests.swift */; };
2AED70EE1D2E36EF006FFBCE /* DocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED70ED1D2E36EF006FFBCE /* DocumentViewController.swift */; };
2AED70EF1D2E36EF006FFBCE /* DocumentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED70ED1D2E36EF006FFBCE /* DocumentViewController.swift */; };
2AEE84B21E8158D700BA7982 /* WriteToConsoleCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AEE84B11E8158D700BA7982 /* WriteToConsoleCommand.swift */; };
@ -866,9 +860,7 @@
2A1FAD5720A74D0A00566D7C /* MutableCopying.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableCopying.swift; sourceTree = "<group>"; };
2A2179F51A07093B002C4AB1 /* SyntaxMap.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = SyntaxMap.json; sourceTree = "<group>"; };
2A21E6722BB44D5E0054C8A1 /* DonationSettings.xcstrings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json.xcstrings; path = DonationSettings.xcstrings; sourceTree = "<group>"; };
2A231A241E7B4EDC00C2A909 /* MultipleReplace+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MultipleReplace+Codable.swift"; sourceTree = "<group>"; };
2A231A271E7BD82700C2A909 /* Binding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Binding.swift; sourceTree = "<group>"; };
2A231A2C1E7BE8B700C2A909 /* FindProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FindProgress.swift; sourceTree = "<group>"; };
2A231A351E7C30F000C2A909 /* MultipleReplaceSplitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipleReplaceSplitViewController.swift; sourceTree = "<group>"; };
2A231A381E7C31F400C2A909 /* MultipleReplaceListViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipleReplaceListViewController.swift; sourceTree = "<group>"; };
2A24F90F2BEDDFEF00CB6CCF /* WhatsNewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhatsNewView.swift; sourceTree = "<group>"; };
@ -893,7 +885,6 @@
2A30C7DA2B1380BE002F6381 /* ShortcutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutView.swift; sourceTree = "<group>"; };
2A33D07D1D1C75B8005977B9 /* SyntaxValidationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SyntaxValidationView.swift; sourceTree = "<group>"; };
2A341D19281EE23C00B85CB6 /* UserActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivity.swift; sourceTree = "<group>"; };
2A3581971E597ECE00762AA5 /* MultipleReplace.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MultipleReplace.swift; sourceTree = "<group>"; };
2A359DFD1DAE93EE00FEF7AA /* NSWindow+Responder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSWindow+Responder.swift"; sourceTree = "<group>"; };
2A3643E51E7C3D2400EA3CE8 /* ReplacementManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReplacementManager.swift; sourceTree = "<group>"; };
2A36CE7B1FF654C000020702 /* NSTextView+Snippet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextView+Snippet.swift"; sourceTree = "<group>"; };
@ -1175,8 +1166,8 @@
2AEBD259246BB4C200EC97A3 /* NSAttributedStringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSAttributedStringTests.swift; sourceTree = "<group>"; };
2AEC48321E641E4F00FB0F89 /* Snippet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Snippet.swift; sourceTree = "<group>"; };
2AEC69C31D41A1BE0089F96F /* EditorTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorTextView.swift; sourceTree = "<group>"; };
2AED466F1E425CD200751C45 /* TextFind.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFind.swift; sourceTree = "<group>"; };
2AED46721E43942300751C45 /* TextFindTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFindTests.swift; sourceTree = "<group>"; };
2AED466F1E425CD200751C45 /* TextFind.Error+Localization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "TextFind.Error+Localization.swift"; sourceTree = "<group>"; };
2AED46721E43942300751C45 /* TextFinderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextFinderTests.swift; sourceTree = "<group>"; };
2AED70ED1D2E36EF006FFBCE /* DocumentViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentViewController.swift; sourceTree = "<group>"; };
2AEE84B11E8158D700BA7982 /* WriteToConsoleCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WriteToConsoleCommand.swift; sourceTree = "<group>"; };
2AF073E21D33C3AB00770BA6 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
@ -1479,8 +1470,6 @@
2A231A2A1E7BD92F00C2A909 /* Models */ = {
isa = PBXGroup;
children = (
2A3581971E597ECE00762AA5 /* MultipleReplace.swift */,
2A231A241E7B4EDC00C2A909 /* MultipleReplace+Codable.swift */,
2A3A19DE2068A76600516DE4 /* NSTextView+MultipleReplace.swift */,
);
name = Models;
@ -1849,8 +1838,7 @@
children = (
2A18560A1D47FA37008FA79E /* TextFinder.swift */,
2A6876A72963DE38006257A6 /* TextFinderSettings.swift */,
2AED466F1E425CD200751C45 /* TextFind.swift */,
2A231A2C1E7BE8B700C2A909 /* FindProgress.swift */,
2AED466F1E425CD200751C45 /* TextFind.Error+Localization.swift */,
2AB1BD21287D752300C6FEAF /* Views */,
2A009ADB1A5AB96F00C3D542 /* Text View */,
2A3581931E597AFE00762AA5 /* Multiple Replace */,
@ -2107,7 +2095,7 @@
2ACC65311C98033D000574DC /* ThemeTests.swift */,
2A9C07551CF9F982006D672D /* IncompatibleCharacterTests.swift */,
2A54BE2B1D40EB24000816B0 /* LineEndingTests.swift */,
2AED46721E43942300751C45 /* TextFindTests.swift */,
2AED46721E43942300751C45 /* TextFinderTests.swift */,
2AC72EA1253478D5001D3CA0 /* FileDropItemTests.swift */,
2ABEFB6923DC0CA0008769F4 /* EditorCounterTests.swift */,
2A1125C023F180FF006A1DB2 /* LineRangeCacheableTests.swift */,
@ -2745,7 +2733,6 @@
2AE52F291D176B8500D60A32 /* FindPanelSplitView.swift in Sources */,
2A5D13361D1FC87900D38E6A /* FindPanelTextClipView.swift in Sources */,
2A5D13331D1FB90300D38E6A /* FindPanelTextView.swift in Sources */,
2A231A2E1E7BE8B700C2A909 /* FindProgress.swift in Sources */,
2A5D13111D1EE66500D38E6A /* FindProgressView.swift in Sources */,
2AF98CAB294B9488009AD47F /* FindSettingsView.swift in Sources */,
2A65EC262B80C01B008096C5 /* FontPicker.swift in Sources */,
@ -2786,8 +2773,6 @@
2AB857E82B922D7D0079CFA2 /* ModeManager.swift in Sources */,
2AB857EB2B93050E0079CFA2 /* ModeOptions.swift in Sources */,
2A836D9F2AB1528700B4D458 /* ModeSettingsView.swift in Sources */,
2A3581991E597ECE00762AA5 /* MultipleReplace.swift in Sources */,
2A231A261E7B4EDC00C2A909 /* MultipleReplace+Codable.swift in Sources */,
2A231A3A1E7C31F400C2A909 /* MultipleReplaceListViewController.swift in Sources */,
2A6876AB29641547006257A6 /* MultipleReplacePanelController.swift in Sources */,
2AF98CAF294C0670009AD47F /* MultipleReplaceSettingsView.swift in Sources */,
@ -2904,7 +2889,7 @@
2A4714E42093A2D40093E27F /* SyntaxParser.swift in Sources */,
2A33D07F1D1C75B8005977B9 /* SyntaxValidationView.swift in Sources */,
2AFE848722AE71130001C4ED /* TextContainer.swift in Sources */,
2AED46711E425CD200751C45 /* TextFind.swift in Sources */,
2AED46711E425CD200751C45 /* TextFind.Error+Localization.swift in Sources */,
2A18560C1D47FA37008FA79E /* TextFinder.swift in Sources */,
2A6876A82963DE38006257A6 /* TextFinderSettings.swift in Sources */,
2AD69B861D3E42F700FBD998 /* TextSelection.swift in Sources */,
@ -2966,7 +2951,7 @@
2AC71DE21BF0BDBC002E1434 /* StringAdvancedCountTests.swift in Sources */,
2AE12DFE1E7DB7D200681F72 /* StringFilenameTests.swift in Sources */,
2A63CEC41D0B06D800ED8186 /* SyntaxTests.swift in Sources */,
2AED46731E43942300751C45 /* TextFindTests.swift in Sources */,
2AED46731E43942300751C45 /* TextFinderTests.swift in Sources */,
2ACC65321C98033D000574DC /* ThemeTests.swift in Sources */,
2A476CAE1D09C8C80088E37A /* URLExtensionsTests.swift in Sources */,
2AFD218D27E0442B00E83E88 /* UTTypeExtensionTests.swift in Sources */,
@ -3065,7 +3050,6 @@
2AE52F281D176B8500D60A32 /* FindPanelSplitView.swift in Sources */,
2A5D13351D1FC87900D38E6A /* FindPanelTextClipView.swift in Sources */,
2A5D13321D1FB90300D38E6A /* FindPanelTextView.swift in Sources */,
2A231A2D1E7BE8B700C2A909 /* FindProgress.swift in Sources */,
2A5D13101D1EE66500D38E6A /* FindProgressView.swift in Sources */,
2AF98CAC294B9488009AD47F /* FindSettingsView.swift in Sources */,
2A65EC272B80C01B008096C5 /* FontPicker.swift in Sources */,
@ -3106,8 +3090,6 @@
2AB857E92B922D7D0079CFA2 /* ModeManager.swift in Sources */,
2AB857EC2B93050E0079CFA2 /* ModeOptions.swift in Sources */,
2A836DA02AB1528700B4D458 /* ModeSettingsView.swift in Sources */,
2A3581981E597ECE00762AA5 /* MultipleReplace.swift in Sources */,
2A231A251E7B4EDC00C2A909 /* MultipleReplace+Codable.swift in Sources */,
2A231A391E7C31F400C2A909 /* MultipleReplaceListViewController.swift in Sources */,
2A6876AC29641547006257A6 /* MultipleReplacePanelController.swift in Sources */,
2AF98CB0294C0670009AD47F /* MultipleReplaceSettingsView.swift in Sources */,
@ -3224,7 +3206,7 @@
2A4714E32093A2D40093E27F /* SyntaxParser.swift in Sources */,
2A33D07E1D1C75B8005977B9 /* SyntaxValidationView.swift in Sources */,
2AFE848622AE71130001C4ED /* TextContainer.swift in Sources */,
2AED46701E425CD200751C45 /* TextFind.swift in Sources */,
2AED46701E425CD200751C45 /* TextFind.Error+Localization.swift in Sources */,
2A18560B1D47FA37008FA79E /* TextFinder.swift in Sources */,
2A6876A92963DE38006257A6 /* TextFinderSettings.swift in Sources */,
2AD69B851D3E42F700FBD998 /* TextSelection.swift in Sources */,

View File

@ -24,6 +24,7 @@
//
import SwiftUI
import TextFind
struct FindProgressView: View {

View File

@ -29,6 +29,7 @@ import Combine
import UniformTypeIdentifiers
import OSLog
import Defaults
import TextFind
final class MultipleReplaceListViewController: NSViewController, NSMenuItemValidation {

View File

@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2022-2023 1024jp
// © 2022-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@
//
import SwiftUI
import TextFind
struct MultipleReplaceSettingsView: View {

View File

@ -27,6 +27,7 @@ import AppKit
import Combine
import SwiftUI
import Defaults
import TextFind
final class MultipleReplaceViewController: NSViewController {

View File

@ -25,6 +25,7 @@
import AppKit
import SwiftUI
import TextFind
extension NSTextView {

View File

@ -25,6 +25,7 @@
import Foundation
import Observation
import TextFind
import UniformTypeIdentifiers
@Observable final class ReplacementManager: SettingFileManaging {

View File

@ -0,0 +1,65 @@
//
// TextFind.Error+Localization.swift
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2017-02-02.
//
// ---------------------------------------------------------------------------
//
// © 2015-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
import TextFind
extension TextFind.Error: @retroactive LocalizedError {
public var errorDescription: String? {
switch self {
case .regularExpression:
String(localized: "TextFind.Error.regularExpression.errorDescription",
defaultValue: "Invalid regular expression",
table: "TextFind")
case .emptyFindString:
String(localized: "TextFind.Error.emptyFindString.errorDescription",
defaultValue: "Empty find string",
table: "TextFind")
case .emptyInSelectionSearch:
String(localized: "TextFind.Error.emptyInSelectionSearch.errorDescription",
defaultValue: "The option “in selection” is selected, although nothing is selected.",
table: "TextFind")
}
}
public var recoverySuggestion: String? {
switch self {
case .regularExpression(let reason):
reason
case .emptyFindString:
String(localized: "TextFind.Error.emptyFindString.recoverySuggestion",
defaultValue: "Input text to find.",
table: "TextFind")
case .emptyInSelectionSearch:
String(localized: "TextFind.Error.emptyInSelectionSearch.recoverySuggestion",
defaultValue: "Select the search scope in the document or turn off the “in selection” option.",
table: "TextFind")
}
}
}

View File

@ -25,6 +25,7 @@
import AppKit
import SwiftUI
import TextFind
import ValueRange
extension NSAttributedString: @retroactive @unchecked Sendable { }

View File

@ -23,8 +23,10 @@
// limitations under the License.
//
import AppKit
import Foundation
import AppKit.NSPasteboard
import Defaults
import TextFind
@MainActor final class TextFinderSettings: NSObject {

View File

@ -22,6 +22,7 @@ let package = Package(
"Syntax",
"TextClipping",
"TextEditing",
"TextFind",
"UnicodeNormalization",
"ValueRange",
"Shortcut",
@ -37,6 +38,7 @@ let package = Package(
.library(name: "Syntax", targets: ["Syntax"]),
.library(name: "TextClipping", targets: ["TextClipping"]),
.library(name: "TextEditing", targets: ["TextEditing"]),
.library(name: "TextFind", targets: ["TextFind"]),
.library(name: "UnicodeNormalization", targets: ["UnicodeNormalization"]),
.library(name: "Shortcut", targets: ["Shortcut"]),
@ -72,6 +74,9 @@ let package = Package(
.target(name: "TextEditing", dependencies: ["StringBasics", "Syntax"]),
.testTarget(name: "TextEditingTests", dependencies: ["TextEditing"]),
.target(name: "TextFind", dependencies: ["StringBasics", "ValueRange"]),
.testTarget(name: "TextFindTests", dependencies: ["TextFind"]),
.target(name: "UnicodeNormalization"),
.testTarget(name: "UnicodeNormalizationTests", dependencies: ["UnicodeNormalization"]),

View File

@ -24,7 +24,7 @@
// limitations under the License.
//
extension Sequence where Element: Equatable {
public extension Sequence where Element: Equatable {
/// An array consists of unique elements of receiver by keeping ordering.
var uniqued: [Element] {
@ -36,3 +36,14 @@ extension Sequence where Element: Equatable {
}
}
}
public extension Array where Element: Equatable {
/// Removes duplicated elements by keeping ordering.
mutating func unique() {
self = self.uniqued
}
}

View File

@ -24,24 +24,6 @@
// limitations under the License.
//
// MARK: - Unique
extension Sequence where Element: Equatable {
/// An array consists of unique elements of receiver by keeping ordering.
var uniqued: [Element] {
self.reduce(into: []) { (unique, element) in
guard !unique.contains(element) else { return }
unique.append(element)
}
}
}
// MARK: - Sort
extension Sequence {
/// Returns the elements of the sequence, sorted using the value that the given key path refers as the comparison between elements.

View File

@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2022-2023 1024jp
// © 2022-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -26,13 +26,13 @@
import Foundation
import Observation
@Observable final class FindProgress: @unchecked Sendable {
@Observable public final class FindProgress: @unchecked Sendable {
@ObservationIgnored private(set) var count = 0
@ObservationIgnored var completedUnit = 0
@ObservationIgnored public private(set) var count = 0
@ObservationIgnored public var completedUnit = 0
private(set) var isCancelled = false
private(set) var isFinished = false
public private(set) var isCancelled = false
public private(set) var isFinished = false
private let scope: Range<Int>
@ -40,14 +40,14 @@ import Observation
/// Instantiates a progress.
///
/// - Parameter scope: The range of progress unit to work with.
init(scope: Range<Int>) {
public init(scope: Range<Int>) {
self.scope = scope
}
/// The fraction of task completed in between 0...1.0.
var fractionCompleted: Double {
public var fractionCompleted: Double {
if self.isFinished || self.scope.isEmpty {
return 1
@ -60,21 +60,21 @@ import Observation
/// Increments count.
///
/// - Parameter count: The amount to increment.
func increment(by count: Int = 1) {
public func increment(by count: Int = 1) {
self.count += count
}
/// Raise `isCancelled` flag.
func cancel() {
public func cancel() {
self.isCancelled = true
}
/// Raise `isFinished` flag.
func finish() {
public func finish() {
self.isFinished = true
}

View File

@ -8,7 +8,7 @@
//
// ---------------------------------------------------------------------------
//
// © 2017-2023 1024jp
// © 2017-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -38,7 +38,7 @@ extension MultipleReplace.Replacement: Codable {
}
init(from decoder: any Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -51,7 +51,7 @@ extension MultipleReplace.Replacement: Codable {
}
func encode(to encoder: any Encoder) throws {
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
@ -86,7 +86,7 @@ extension MultipleReplace.Settings: Codable {
}
init(from decoder: any Decoder) throws {
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
@ -102,7 +102,7 @@ extension MultipleReplace.Settings: Codable {
}
func encode(to encoder: any Encoder) throws {
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

View File

@ -25,30 +25,57 @@
import Foundation
struct MultipleReplace: Codable {
public struct MultipleReplace: Equatable, Sendable, Codable {
struct Replacement: Equatable {
public struct Replacement: Equatable, Sendable {
var findString: String = ""
var replacementString: String = ""
var usesRegularExpression: Bool = false
var ignoresCase: Bool = false
var description: String?
var isEnabled = true
public var findString: String
public var replacementString: String
public var usesRegularExpression: Bool
public var ignoresCase: Bool
public var description: String?
public var isEnabled: Bool
public init(findString: String = "", replacementString: String = "", usesRegularExpression: Bool = false, ignoresCase: Bool = false, description: String? = nil, isEnabled: Bool = true) {
self.findString = findString
self.replacementString = replacementString
self.usesRegularExpression = usesRegularExpression
self.ignoresCase = ignoresCase
self.description = description
self.isEnabled = isEnabled
}
}
struct Settings: Equatable {
public struct Settings: Equatable, Sendable {
var textualOptions: String.CompareOptions = []
var regexOptions: NSRegularExpression.Options = [.anchorsMatchLines]
var matchesFullWord: Bool = false
var unescapesReplacementString: Bool = true
public var textualOptions: String.CompareOptions
public var regexOptions: NSRegularExpression.Options
public var matchesFullWord: Bool
public var unescapesReplacementString: Bool
public init(textualOptions: String.CompareOptions = [], regexOptions: NSRegularExpression.Options = [.anchorsMatchLines], matchesFullWord: Bool = false, unescapesReplacementString: Bool = true) {
self.textualOptions = textualOptions
self.regexOptions = regexOptions
self.matchesFullWord = matchesFullWord
self.unescapesReplacementString = unescapesReplacementString
}
}
var replacements: [Replacement] = []
var settings: Settings = .init()
public var replacements: [Replacement]
public var settings: Settings
public init(replacements: [Replacement] = [], settings: Settings = .init()) {
self.replacements = replacements
self.settings = settings
}
}
@ -57,14 +84,14 @@ struct MultipleReplace: Codable {
extension MultipleReplace {
struct Result {
public struct Result: Equatable, Sendable {
var string: String
var selectedRanges: [NSRange]?
public var string: String
public var selectedRanges: [NSRange]?
}
enum Status {
public enum Status: Equatable, Sendable {
case processed
case unitChanged
@ -81,7 +108,7 @@ extension MultipleReplace {
/// - inSelection: Whether find only in selection.
/// - progress: The progress object to observe cancellation by the user and notify the find progress.
/// - Returns: The found ranges. This method will return first all search finished.
func find(string: String, ranges: [NSRange], inSelection: Bool, progress: FindProgress? = nil) throws(CancellationError) -> [NSRange] {
public func find(string: String, ranges: [NSRange], inSelection: Bool, progress: FindProgress? = nil) throws(CancellationError) -> [NSRange] {
var result: [NSRange] = []
@ -121,7 +148,7 @@ extension MultipleReplace {
/// - inSelection: Whether replace only in selection.
/// - progress: The progress object to observe cancellation by the user and notify the replacement progress.
/// - Returns: The result of the replacement. This method will return first all replacement finished.
func replace(string: String, ranges: [NSRange], inSelection: Bool, progress: FindProgress? = nil) throws(CancellationError) -> Result {
public func replace(string: String, ranges: [NSRange], inSelection: Bool, progress: FindProgress? = nil) throws(CancellationError) -> Result {
var result = Result(string: string, selectedRanges: ranges)
@ -190,7 +217,7 @@ private extension MultipleReplace.Replacement {
extension MultipleReplace.Replacement {
/// Checks if replacement rule is valid.
func validate(regexOptions: NSRegularExpression.Options = []) throws(TextFind.Error) {
public func validate(regexOptions: NSRegularExpression.Options = []) throws(TextFind.Error) {
guard !self.findString.isEmpty else {
throw TextFind.Error.emptyFindString

View File

@ -27,71 +27,35 @@ import Foundation
import StringBasics
import ValueRange
struct TextFind {
public struct TextFind: Equatable, Sendable {
typealias ReplacementItem = ValueRange<String>
public typealias ReplacementItem = ValueRange<String>
enum Mode: Equatable {
public enum Mode: Equatable, Sendable {
case textual(options: String.CompareOptions, fullWord: Bool) // don't include .backwards to options
case regularExpression(options: NSRegularExpression.Options, unescapesReplacement: Bool)
}
enum `Error`: LocalizedError {
public enum `Error`: Swift.Error, Sendable {
case regularExpression(reason: String)
case emptyFindString
case emptyInSelectionSearch
var errorDescription: String? {
switch self {
case .regularExpression:
String(localized: "TextFind.Error.regularExpression.errorDescription",
defaultValue: "Invalid regular expression",
table: "TextFind")
case .emptyFindString:
String(localized: "TextFind.Error.emptyFindString.errorDescription",
defaultValue: "Empty find string",
table: "TextFind")
case .emptyInSelectionSearch:
String(localized: "TextFind.Error.emptyInSelectionSearch.errorDescription",
defaultValue: "The option “in selection” is selected, although nothing is selected.",
table: "TextFind")
}
}
var recoverySuggestion: String? {
switch self {
case .regularExpression(let reason):
reason
case .emptyFindString:
String(localized: "TextFind.Error.emptyFindString.recoverySuggestion",
defaultValue: "Input text to find.",
table: "TextFind")
case .emptyInSelectionSearch:
String(localized: "TextFind.Error.emptyInSelectionSearch.recoverySuggestion",
defaultValue: "Select the search scope in the document or turn off the “in selection” option.",
table: "TextFind")
}
}
}
// MARK: Public Properties
let findString: String
let mode: TextFind.Mode
let inSelection: Bool
public let findString: String
public let mode: TextFind.Mode
public let inSelection: Bool
let string: String
let selectedRanges: [NSRange]
public let string: String
public let selectedRanges: [NSRange]
// MARK: Private Properties
@ -112,7 +76,7 @@ struct TextFind {
/// - mode: The settable options for the text search.
/// - inSelection: Whether find string only in selectedRanges.
/// - selectedRanges: The selected ranges in the text view.
init(for string: String, findString: String, mode: TextFind.Mode, inSelection: Bool = false, selectedRanges: [NSRange] = [NSRange()]) throws(TextFind.Error) {
public init(for string: String, findString: String, mode: TextFind.Mode, inSelection: Bool = false, selectedRanges: [NSRange] = [NSRange()]) throws(TextFind.Error) {
assert(!selectedRanges.isEmpty)
@ -151,14 +115,14 @@ struct TextFind {
// MARK: Public Methods
/// The number of capture groups in the regular expression.
var numberOfCaptureGroups: Int {
public var numberOfCaptureGroups: Int {
self.regex?.numberOfCaptureGroups ?? 0
}
/// The range large enough to contain all scope ranges.
var scopeRange: Range<Int> {
public var scopeRange: Range<Int> {
self.scopeRanges.map(\.lowerBound).min()!..<self.scopeRanges.map(\.upperBound).max()!
}
@ -167,7 +131,7 @@ struct TextFind {
/// All matched ranges.
///
/// - Throws: `CancellationError`
var matches: [NSRange] {
public var matches: [NSRange] {
get throws {
var ranges: [NSRange] = []
@ -193,7 +157,7 @@ struct TextFind {
/// - includingCurrentSelection: Whether includes the current selection to search.
/// - wraps: Whether the search wraps around.
/// - Returns: A character range and flag whether the search wrapped; or `nil` when not found.
func find(in matches: [NSRange], forward: Bool, includingSelection: Bool = false, wraps: Bool) -> (range: NSRange, wrapped: Bool)? {
public func find(in matches: [NSRange], forward: Bool, includingSelection: Bool = false, wraps: Bool) -> (range: NSRange, wrapped: Bool)? {
assert(forward || !includingSelection)
@ -231,7 +195,7 @@ struct TextFind {
/// - Parameters:
/// - replacementString: The string with which to replace.
/// - Returns: The struct of a string to replace with and a range to replace if found. Otherwise, nil.
func replace(with replacementString: String) -> ReplacementItem? {
public func replace(with replacementString: String) -> ReplacementItem? {
let string = self.string
let selectedRange = self.selectedRanges.first!
@ -262,7 +226,7 @@ struct TextFind {
/// - block: The block enumerates the matches.
/// - matches: The array of matches including group matches.
/// - stop: The `block` can set the value to true to stop further processing.
func findAll(using block: (_ matches: [NSRange], _ stop: inout Bool) -> Void) {
public func findAll(using block: (_ matches: [NSRange], _ stop: inout Bool) -> Void) {
for range in self.scopeRanges {
self.enumerateMatches(in: range) { (matchedRange, match, stop) in
@ -288,7 +252,7 @@ struct TextFind {
/// - Returns:
/// - replacementItems: ReplacementItem per selectedRange.
/// - selectedRanges: New selections for textView only if the replacement is performed within selection. Otherwise, `nil`.
func replaceAll(with replacementString: String, using block: @escaping (_ range: NSRange, _ count: Int, _ stop: inout Bool) -> Void) -> (replacementItems: [ReplacementItem], selectedRanges: [NSRange]?) {
public func replaceAll(with replacementString: String, using block: @escaping (_ range: NSRange, _ count: Int, _ stop: inout Bool) -> Void) -> (replacementItems: [ReplacementItem], selectedRanges: [NSRange]?) {
let replacementString = self.replacementString(from: replacementString)
var replacementItems: [ReplacementItem] = []

View File

@ -24,31 +24,12 @@
// limitations under the License.
//
import AppKit
import Foundation
import Testing
@testable import CotEditor
@testable import TextFind
struct TextFindTests {
@Test func finderActions() {
#expect(TextFinder.Action.showFindInterface.rawValue == NSTextFinder.Action.showFindInterface.rawValue)
#expect(TextFinder.Action.nextMatch.rawValue == NSTextFinder.Action.nextMatch.rawValue)
#expect(TextFinder.Action.previousMatch.rawValue == NSTextFinder.Action.previousMatch.rawValue)
#expect(TextFinder.Action.replaceAll.rawValue == NSTextFinder.Action.replaceAll.rawValue)
#expect(TextFinder.Action.replace.rawValue == NSTextFinder.Action.replace.rawValue)
#expect(TextFinder.Action.replaceAndFind.rawValue == NSTextFinder.Action.replaceAndFind.rawValue)
#expect(TextFinder.Action.setSearchString.rawValue == NSTextFinder.Action.setSearchString.rawValue)
#expect(TextFinder.Action.replaceAllInSelection.rawValue == NSTextFinder.Action.replaceAllInSelection.rawValue)
#expect(TextFinder.Action.selectAll.rawValue == NSTextFinder.Action.selectAll.rawValue)
#expect(TextFinder.Action.selectAllInSelection.rawValue == NSTextFinder.Action.selectAllInSelection.rawValue)
#expect(TextFinder.Action.hideFindInterface.rawValue == NSTextFinder.Action.hideFindInterface.rawValue)
#expect(TextFinder.Action.showReplaceInterface.rawValue == NSTextFinder.Action.showReplaceInterface.rawValue)
#expect(TextFinder.Action.showReplaceInterface.rawValue == NSTextFinder.Action.showReplaceInterface.rawValue)
#expect(TextFinder.Action.hideReplaceInterface.rawValue == NSTextFinder.Action.hideReplaceInterface.rawValue)
}
@Test func countCaptureGroup() throws {
var mode: TextFind.Mode

View File

@ -0,0 +1,50 @@
//
// TextFinderTests.swift
// Tests
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2017-02-03.
//
// ---------------------------------------------------------------------------
//
// © 2017-2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AppKit
import Testing
@testable import CotEditor
struct TextFinderTests {
@Test func finderActions() {
#expect(TextFinder.Action.showFindInterface.rawValue == NSTextFinder.Action.showFindInterface.rawValue)
#expect(TextFinder.Action.nextMatch.rawValue == NSTextFinder.Action.nextMatch.rawValue)
#expect(TextFinder.Action.previousMatch.rawValue == NSTextFinder.Action.previousMatch.rawValue)
#expect(TextFinder.Action.replaceAll.rawValue == NSTextFinder.Action.replaceAll.rawValue)
#expect(TextFinder.Action.replace.rawValue == NSTextFinder.Action.replace.rawValue)
#expect(TextFinder.Action.replaceAndFind.rawValue == NSTextFinder.Action.replaceAndFind.rawValue)
#expect(TextFinder.Action.setSearchString.rawValue == NSTextFinder.Action.setSearchString.rawValue)
#expect(TextFinder.Action.replaceAllInSelection.rawValue == NSTextFinder.Action.replaceAllInSelection.rawValue)
#expect(TextFinder.Action.selectAll.rawValue == NSTextFinder.Action.selectAll.rawValue)
#expect(TextFinder.Action.selectAllInSelection.rawValue == NSTextFinder.Action.selectAllInSelection.rawValue)
#expect(TextFinder.Action.hideFindInterface.rawValue == NSTextFinder.Action.hideFindInterface.rawValue)
#expect(TextFinder.Action.showReplaceInterface.rawValue == NSTextFinder.Action.showReplaceInterface.rawValue)
#expect(TextFinder.Action.showReplaceInterface.rawValue == NSTextFinder.Action.showReplaceInterface.rawValue)
#expect(TextFinder.Action.hideReplaceInterface.rawValue == NSTextFinder.Action.hideReplaceInterface.rawValue)
}
}