diff --git a/DevToys.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DevToys.xcworkspace/xcshareddata/swiftpm/Package.resolved index b40a58d..2b97697 100644 --- a/DevToys.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DevToys.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/ObuchiYuki/DiffMatchPatch", "state": { "branch": null, - "revision": "7f79f4a473e0e81d6864b425f101c78bccfd169b", - "version": "1.0.0" + "revision": "5db22e8f9ac588a130f62733105bbe5c5bdd7bc3", + "version": "1.0.3" } }, { diff --git a/DevToys/DevToys/Body/Text/TextDiffView+.swift b/DevToys/DevToys/Body/Text/TextDiffView+.swift index 1e93043..4b8eed0 100644 --- a/DevToys/DevToys/Body/Text/TextDiffView+.swift +++ b/DevToys/DevToys/Body/Text/TextDiffView+.swift @@ -5,4 +5,112 @@ // Created by yuki on 2022/02/23. // -import Foundation +import DiffMatchPatch +import CoreUtil + +final class TextDiffViewController: NSViewController { + + @RestorableState("textdiff.operation") var operation: TextCheckOperation = .characters + @RestorableState("textdiff.input1") var input1 = "" + @RestorableState("textdiff.input2") var input2 = "" + + @Observable var diffAttributedString = NSAttributedString() + + private let cell = TextDiffView() + + override func loadView() { self.view = cell } + + override func viewDidLoad() { + self.$operation + .sink{[unowned self] in self.cell.checkOperationPicker.selectedItem = $0 }.store(in: &objectBag) + self.$input1 + .sink{[unowned self] in self.cell.input1Section.string = $0 }.store(in: &objectBag) + self.$input2 + .sink{[unowned self] in self.cell.input2Section.string = $0 }.store(in: &objectBag) + self.$diffAttributedString + .sink{[unowned self] in self.cell.outputSection.textView.textView.textStorage?.setAttributedString($0) }.store(in: &objectBag) + + self.cell.input1Section.stringPublisher + .sink{[unowned self] in self.input1 = $0; updateDiff() }.store(in: &objectBag) + self.cell.input2Section.stringPublisher + .sink{[unowned self] in self.input2 = $0; updateDiff() }.store(in: &objectBag) + self.cell.checkOperationPicker.itemPublisher + .sink{[unowned self] in self.operation = $0; updateDiff() }.store(in: &objectBag) + + self.updateDiff() + } + + private func updateDiff() { + let diffs = TextDifferenceChecker.compare(input1, input2, operation: self.operation) + self.diffAttributedString = buildAttributedString(from: diffs) + } + + private func buildAttributedString(from diffs: [Difference]) -> NSAttributedString { + let attributedString = NSMutableAttributedString() + let defaultAttributes = [ + NSAttributedString.Key.foregroundColor: NSColor.textColor, + NSAttributedString.Key.font : NSFont.monospacedSystemFont(ofSize: 12, weight: .regular) + ] + + for diff in diffs { + switch diff.operation { + case .equal: + attributedString.append(NSAttributedString(string: diff.text, attributes: defaultAttributes)) + case .insert: + attributedString.append(NSAttributedString(string: diff.text, attributes: defaultAttributes.merging([ + NSAttributedString.Key.backgroundColor : NSColor.systemGreen.withAlphaComponent(0.7) + ], uniquingKeysWith: { a, _ in a }) )) + case .delete: + attributedString.append(NSAttributedString(string: diff.text, attributes: defaultAttributes.merging([ + NSAttributedString.Key.backgroundColor : NSColor.systemRed.withAlphaComponent(0.7) + ], uniquingKeysWith: { a, _ in a }) )) + } + } + + return attributedString + } +} + +extension TextCheckOperation: TextItem { + public static let allCases: [TextCheckOperation] = [.characters, .words, .lines] + + var title: String { + switch self { + case .characters: return "Characters" + case .words: return "Words" + case .lines: return "Lines" + } + } +} + +final private class TextDiffView: Page { + let checkOperationPicker = EnumPopupButton() + + let input1Section = CodeViewSection(title: "Input 1".localized(), options: .defaultInput, language: .plaintext) + let input2Section = CodeViewSection(title: "Input 2".localized(), options: .defaultInput, language: .plaintext) + let outputSection = TextViewSection(title: "Output".localized(), options: .defaultOutput) + + override func layout() { + super.layout() + + } + + override func onAwake() { + self.addSection(Section(title: "Configuration".localized(), items: [ + Area(icon: R.Image.format, title: "Diff Style", control: checkOperationPicker) + ])) + + self.addSection2(input1Section, input2Section) + self.input1Section.snp.makeConstraints{ make in + make.height.equalTo(320) + } + + self.addSection(outputSection) + self.outputSection.textView.textView.font = .monospacedSystemFont(ofSize: 12, weight: .regular) + self.outputSection.snp.makeConstraints{ make in + make.height.equalTo(320) + } + + } +} + diff --git a/DevToys/DevToys/Component/TextView.swift b/DevToys/DevToys/Component/TextView.swift index 34bdc38..f353745 100644 --- a/DevToys/DevToys/Component/TextView.swift +++ b/DevToys/DevToys/Component/TextView.swift @@ -8,7 +8,7 @@ import CoreUtil final class TextView: NSLoadView { - private class _TextView: NSTextView { + class _TextView: NSTextView { let stringPublisher = PassthroughSubject() var sendingValue = false override var string: String { @@ -38,7 +38,7 @@ final class TextView: NSLoadView { private let scrollView = _TextView.scrollableTextView() private let backgroudLayer = ControlBackgroundLayer.animationDisabled() - private lazy var textView = scrollView.documentView as! _TextView + lazy var textView = scrollView.documentView as! _TextView override func onAwake() { self.wantsLayer = true diff --git a/DevToys/DevToys/Model/Tool+Default.swift b/DevToys/DevToys/Model/Tool+Default.swift index 193cf9e..d5e7bfe 100644 --- a/DevToys/DevToys/Model/Tool+Default.swift +++ b/DevToys/DevToys/Model/Tool+Default.swift @@ -108,12 +108,18 @@ extension Tool { sidebarTitle: "tool.regex.mintitle".localized(), toolDescription: "tool.regex.description".localized(), viewController: RegexTesterViewController() ) + static let textDiff = Tool( + title: "Text Diff".localized(), identifier: "textdiff", category: .text, icon: R.Image.Tool.textInspector, + sidebarTitle: "Text Diff".localized(), toolDescription: "Compare two Text and display Diff".localized(), + viewController: TextDiffViewController() + ) static let hyphenationRemover = Tool( title: "tool.hyphenremove.title".localized(), identifier: "hyphenremove", category: .text, icon: R.Image.Tool.textInspector, sidebarTitle: "tool.hyphenremove.mintitle".localized(), toolDescription: "tool.hyphenremove.description".localized(), viewController: HyphenationRemoverViewController() ) + // MARK: - Graphic - static let imageOptimizer = Tool( title: "tool.imageoptim.title".localized(), identifier: "imageoptim", category: .graphic, icon: R.Image.Tool.imageCompressor, @@ -172,6 +178,7 @@ extension ToolManager { toolManager.registerTool(.textInspector) toolManager.registerTool(.regexTester) + toolManager.registerTool(.textDiff) toolManager.registerTool(.hyphenationRemover) toolManager.registerTool(.imageOptimizer)