diff --git a/.swiftformat b/.swiftformat index bf171b1..89c40af 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,3 +1,5 @@ +--exclude build + # version --swiftversion 6.2 @@ -16,6 +18,7 @@ --enable redundantVariable --enable wrapConditionalBodies --enable wrapEnumCases +--enable acronyms --enable blankLinesAfterGuardStatements --enable isEmpty diff --git a/.swiftlint.yml b/.swiftlint.yml index 0c7a909..d7be92d 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,3 +1,6 @@ +excluded: + - build + opt_in_rules: - array_init - attributes diff --git a/Sources/App/AppUpdater.swift b/Sources/App/AppUpdater.swift index c2465b0..e760fda 100644 --- a/Sources/App/AppUpdater.swift +++ b/Sources/App/AppUpdater.swift @@ -22,6 +22,7 @@ let applicationMenu = mainMenu.item(withTag: MainMenu.application.rawValue)?.submenu else { return } + let checkForUpdatesItem = NSMenuItem( title: NSLocalizedString("Check for Updates…", comment: ""), action: #selector(SPUStandardUpdaterController.checkForUpdates(_:)), diff --git a/Sources/Core/Common/AlignRule.swift b/Sources/Core/Common/AlignRule.swift index 1fbfb11..ad630a0 100644 --- a/Sources/Core/Common/AlignRule.swift +++ b/Sources/Core/Common/AlignRule.swift @@ -6,6 +6,8 @@ // Copyright (c) 2025 visualdiffer.com // +import os + struct AlignTemplateOptions: OptionSet { let rawValue: UInt @@ -44,6 +46,7 @@ public struct AlignRule { let template = AlignTemplate(dictionary) else { return nil } + self.init(regExp: regExp, template: template) } } @@ -69,10 +72,11 @@ extension AlignRule { guard let result = regExp.regularExpression()?.firstMatch( in: lhs, options: [], - range: NSRange(location: 0, length: lhs.count) + range: NSRange(location: 0, length: lhs.utf16.count) ) else { return false } + let replaced = lhs.replace( template: template.pattern, result: result @@ -85,7 +89,9 @@ extension AlignRule { } extension AlignRule.Pair where T == NSRegularExpression.Options { - private nonisolated(unsafe) static var cachedRegExpressions = [String: NSRegularExpression]() + private static let cachedRegExpressions = OSAllocatedUnfairLock( + initialState: [String: NSRegularExpression]() + ) init() { self.init(pattern: "", options: []) @@ -95,6 +101,7 @@ extension AlignRule.Pair where T == NSRegularExpression.Options { guard let pattern = dictionary[AlignRule.Key.regExp.rawValue] as? String else { return nil } + let options = if let rawValue = dictionary[AlignRule.Key.regExpOptions.rawValue] as? UInt { NSRegularExpression.Options(rawValue: rawValue) } else { @@ -108,18 +115,26 @@ extension AlignRule.Pair where T == NSRegularExpression.Options { } func regularExpression() -> NSRegularExpression? { - let key = String(format: "%@%ld", pattern, options.rawValue) - var cachedRE = Self.cachedRegExpressions[key] - if cachedRE == nil { - cachedRE = try? NSRegularExpression( + let pattern = pattern + let options = options + let key = "\(pattern)|\(options.rawValue)" + + return Self.cachedRegExpressions.withLock { values in + if let value = values[key] { + return value + } + + guard let value = try? NSRegularExpression( pattern: pattern, options: options - ) - if let cachedRE { - Self.cachedRegExpressions[key] = cachedRE + ) else { + return nil } + + values[key] = value + + return value } - return cachedRE } } @@ -132,6 +147,7 @@ extension AlignRule.Pair where T == AlignTemplateOptions { guard let pattern = dictionary[AlignRule.Key.template.rawValue] as? String else { return nil } + let options = if let rawValue = dictionary[AlignRule.Key.templateOptions.rawValue] as? UInt { AlignTemplateOptions(rawValue: rawValue) } else { diff --git a/Sources/Core/Common/KeyEquivalent.swift b/Sources/Core/Common/KeyEquivalent.swift index 49c17d6..e9c29c5 100644 --- a/Sources/Core/Common/KeyEquivalent.swift +++ b/Sources/Core/Common/KeyEquivalent.swift @@ -9,32 +9,31 @@ import Foundation // swiftlint:disable identifier_name -@objc class KeyEquivalent: NSObject { - @objc static let leftArrow = "\u{2190}" - @objc static let rightArrow = "\u{2192}" - @objc static let upArrow = "\u{2191}" - @objc static let downArrow = "\u{2193}" + static let leftArrow = "\u{2190}" + static let rightArrow = "\u{2192}" + static let upArrow = "\u{2191}" + static let downArrow = "\u{2193}" - @objc static let deleteBackspace = "\u{0008}" - @objc static let forwardDelete = "\u{007F}" + static let deleteBackspace = "\u{0008}" + static let forwardDelete = "\u{007F}" - @objc static let enter = "\r" - @objc static let escape = "\u{001B}" - @objc static let tab = "\t" + static let enter = "\r" + static let escape = "\u{001B}" + static let tab = "\t" - @objc static let f1 = "\u{F704}" - @objc static let f2 = "\u{F705}" - @objc static let f3 = "\u{F706}" - @objc static let f4 = "\u{F707}" - @objc static let f5 = "\u{F708}" - @objc static let f6 = "\u{F709}" - @objc static let f7 = "\u{F70A}" - @objc static let f8 = "\u{F70B}" - @objc static let f9 = "\u{F70C}" - @objc static let f10 = "\u{F70D}" - @objc static let f11 = "\u{F70E}" - @objc static let f12 = "\u{F70F}" + static let f1 = "\u{F704}" + static let f2 = "\u{F705}" + static let f3 = "\u{F706}" + static let f4 = "\u{F707}" + static let f5 = "\u{F708}" + static let f6 = "\u{F709}" + static let f7 = "\u{F70A}" + static let f8 = "\u{F70B}" + static let f9 = "\u{F70C}" + static let f10 = "\u{F70D}" + static let f11 = "\u{F70E}" + static let f12 = "\u{F70F}" } // swiftlint:enable identifier_name diff --git a/Sources/Core/Common/NSImage+SymbolCompat.swift b/Sources/Core/Common/NSImage+SymbolCompat.swift index c73307f..bec489f 100644 --- a/Sources/Core/Common/NSImage+SymbolCompat.swift +++ b/Sources/Core/Common/NSImage+SymbolCompat.swift @@ -8,7 +8,6 @@ import Foundation -@objc extension NSImage { static func imageSymbolCompat(_ name: NSImage.Name) -> NSImage? { if #available(macOS 11.0, *) { diff --git a/Sources/Core/Common/NotificationCenter+Helper.swift b/Sources/Core/Common/NotificationCenter+Helper.swift index b1c4aaa..24ba77a 100644 --- a/Sources/Core/Common/NotificationCenter+Helper.swift +++ b/Sources/Core/Common/NotificationCenter+Helper.swift @@ -6,7 +6,7 @@ // Copyright (c) 2025 visualdiffer.com // -enum FileSavedKey: String, Hashable { +enum FileUpdatedKey: String, Hashable { case leftPath case rightPath } @@ -28,7 +28,7 @@ enum PrefChangedKey: String, Hashable { extension Notification.Name { static let prefsChanged = NSNotification.Name("com.visualdiffer.notification.prefsChanges") static let appAppearanceDidChange = NSNotification.Name("com.visualdiffer.notification.appAppearanceDidChange") - static let fileSaved = NSNotification.Name("com.visualdiffer.notification.fileSaved") + static let fileUpdated = NSNotification.Name("com.visualdiffer.notification.fileUpdated") } extension NotificationCenter { @@ -39,4 +39,20 @@ extension NotificationCenter { userInfo: userInfo ) } + + func postFileUpdated(leftPath: String?, rightPath: String?) { + var userInfo = [FileUpdatedKey: String]() + + if let path = leftPath { + userInfo[.leftPath] = path + } + if let path = rightPath { + userInfo[.rightPath] = path + } + NotificationCenter.default.post( + name: .fileUpdated, + object: nil, + userInfo: userInfo + ) + } } diff --git a/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift b/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift index e4ff641..b7c034e 100644 --- a/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift +++ b/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift @@ -67,6 +67,7 @@ extension CommonPrefs { let colorSet = scheme[name.rawValue] else { return nil } + return colorSet } } diff --git a/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift b/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift index 90e919d..ed53f3d 100644 --- a/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift +++ b/Sources/Core/CommonPrefs/CommonPrefs+FolderCompare.swift @@ -171,6 +171,7 @@ extension CommonPrefs { let colorSet = scheme[name.rawValue] else { return nil } + return colorSet } } diff --git a/Sources/Core/CommonPrefs/CommonPrefs+Font.swift b/Sources/Core/CommonPrefs/CommonPrefs+Font.swift index 71e9dd9..5c293da 100644 --- a/Sources/Core/CommonPrefs/CommonPrefs+Font.swift +++ b/Sources/Core/CommonPrefs/CommonPrefs+Font.swift @@ -40,6 +40,7 @@ extension CommonPrefs { guard let font = font(forKey: .folderListingFont) else { return defaultFolderListingFont() } + return font } @@ -57,6 +58,7 @@ extension CommonPrefs { guard let font = font(forKey: .fileTextFont) else { return defaultFolderListingFont() } + return font } diff --git a/Sources/Core/CommonPrefs/CommonPrefs.swift b/Sources/Core/CommonPrefs/CommonPrefs.swift index 43f52ca..5dbeb38 100644 --- a/Sources/Core/CommonPrefs/CommonPrefs.swift +++ b/Sources/Core/CommonPrefs/CommonPrefs.swift @@ -80,6 +80,7 @@ public class CommonPrefs: @unchecked Sendable { guard let stream = InputStream(fileAtPath: configPath) else { throw FileError.openFile(path: configPath) } + defer { stream.close() } diff --git a/Sources/Core/Document/VDDocument.swift b/Sources/Core/Document/VDDocument.swift index 6576d8e..a5bac17 100644 --- a/Sources/Core/Document/VDDocument.swift +++ b/Sources/Core/Document/VDDocument.swift @@ -39,6 +39,7 @@ public class VDDocument: NSPersistentDocument { guard let moc = managedObjectContext else { fatalError("managedObjectContext is nil") } + let fetchRequest = SessionDiff.fetchRequest() do { @@ -65,6 +66,7 @@ public class VDDocument: NSPersistentDocument { guard let moc = managedObjectContext else { return } + moc.rollback() moc.updateWithoutRecordingModifications { @@ -120,6 +122,7 @@ public class VDDocument: NSPersistentDocument { guard let itemType = sessionDiff.itemType else { throw DocumentError.unknownSessionType } + try itemType.checkPaths(leftPath: leftPath, rightPath: rightPath) } @@ -155,6 +158,7 @@ public class VDDocument: NSPersistentDocument { guard let absolutePath else { return nil } + let dotSet = CharacterSet(charactersIn: ".") var url = URL(filePath: absolutePath) @@ -170,6 +174,7 @@ public class VDDocument: NSPersistentDocument { super.makeWindowControllers() return } + switch sessionDiff.itemType { case .folder: let fwc = FoldersWindowController() @@ -268,6 +273,7 @@ public class VDDocument: NSPersistentDocument { let rightPath = sessionDiff.rightPath else { return super.displayName } + if leftPath.isEmpty, rightPath.isEmpty { return super.displayName } diff --git a/Sources/Core/Document/VDDocumentController.swift b/Sources/Core/Document/VDDocumentController.swift index a77be7b..dacb97c 100644 --- a/Sources/Core/Document/VDDocumentController.swift +++ b/Sources/Core/Document/VDDocumentController.swift @@ -23,8 +23,11 @@ class VDDocumentController: NSDocumentController { private let documentWindow: DocumentWindow override class var shared: VDDocumentController { - // swiftlint:disable:next force_cast - NSDocumentController.shared as! VDDocumentController + guard let controller = NSDocumentController.shared as? VDDocumentController else { + preconditionFailure("NSDocumentController.shared is not VDDocumentController — check the nib") + } + + return controller } override init() { @@ -85,23 +88,23 @@ class VDDocumentController: NSDocumentController { } @discardableResult - func openDifferDocument(leftUrl: URL?, rightUrl: URL?) throws -> VDDocument? { + func openDifferDocument(leftURL: URL?, rightURL: URL?) throws -> VDDocument? { // at least one path must be set - if leftUrl == nil && rightUrl == nil { + if leftURL == nil && rightURL == nil { return nil } - let hasMissingPath = leftUrl == nil || rightUrl == nil + let hasMissingPath = leftURL == nil || rightURL == nil var isFolder = false var leftPathExists = false var rightPathExists = false // determine the item type (directory or file) whether one path or both paths are provided - let primaryPathUrl = leftUrl ?? rightUrl - let secondaryPathUrl = rightUrl ?? leftUrl - let canOpenDocument = if let primaryPathUrl, let secondaryPathUrl { - primaryPathUrl.matchesFileType( - of: secondaryPathUrl, + let primaryPathURL = leftURL ?? rightURL + let secondaryPathURL = rightURL ?? leftURL + let canOpenDocument = if let primaryPathURL, let secondaryPathURL { + primaryPathURL.matchesFileType( + of: secondaryPathURL, isDir: &isFolder, leftExists: &leftPathExists, rightExists: &rightPathExists @@ -117,6 +120,7 @@ class VDDocumentController: NSDocumentController { rightExists: rightPathExists ) } + // when comparing folders both paths must be set if isFolder, hasMissingPath { throw SessionTypeError.invalidAllItems(isDir: true) @@ -124,9 +128,9 @@ class VDDocumentController: NSDocumentController { return try openDocumentWithBlock { document in if let sessionDiff = document.sessionDiff { sessionDiff.itemType = isFolder ? .folder : .file - sessionDiff.leftPath = leftUrl?.standardizingPath ?? "" + sessionDiff.leftPath = leftURL?.standardizingPath ?? "" sessionDiff.leftReadOnly = false - sessionDiff.rightPath = rightUrl?.standardizingPath ?? "" + sessionDiff.rightPath = rightURL?.standardizingPath ?? "" sessionDiff.rightReadOnly = false sessionDiff.expandAllFolders = CommonPrefs.shared.bool(forKey: .expandAllFolders) } diff --git a/Sources/Core/Document/Window/DocumentWindow.swift b/Sources/Core/Document/Window/DocumentWindow.swift index a7a86c8..057fc78 100644 --- a/Sources/Core/Document/Window/DocumentWindow.swift +++ b/Sources/Core/Document/Window/DocumentWindow.swift @@ -329,7 +329,6 @@ class DocumentWindow: NSWindow, FileDropImageViewDelegate, HistoryControllerDele } } - @objc func fillSessionDiff(_ sessionDiff: SessionDiff) -> Bool { // Update all properties handled by preference sheet sessionPreferencesSheet.updateSessionDiff(sessionDiff) diff --git a/Sources/Core/Document/Window/RecentDocumentPopupMenu.swift b/Sources/Core/Document/Window/RecentDocumentPopupMenu.swift index 4062ddc..6565320 100644 --- a/Sources/Core/Document/Window/RecentDocumentPopupMenu.swift +++ b/Sources/Core/Document/Window/RecentDocumentPopupMenu.swift @@ -8,7 +8,7 @@ private let showRecentDocumentsListPrefName = "showRecentDocumentsList" -class RecentDocumentPopupMenu: PopUpButtonUrl, NSMenuDelegate { +class RecentDocumentPopupMenu: PopUpButtonURL, NSMenuDelegate { init(title _: String, target: AnyObject?, action: Selector?) { super.init( title: NSLocalizedString("Open Recent", comment: ""), diff --git a/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift b/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift index 92da9dc..acde95f 100644 --- a/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift +++ b/Sources/Core/SessionDiff/SessionDiff+AlignRule.swift @@ -14,6 +14,7 @@ extension SessionDiff { guard let fileNameAlignmentsData else { return nil } + let allowedClasses: [AnyClass] = [ NSArray.self, NSMutableDictionary.self, diff --git a/Sources/Core/SessionDiff/SessionDiff+ExtraData.swift b/Sources/Core/SessionDiff/SessionDiff+ExtraData.swift index 45fa80a..ddaeaca 100644 --- a/Sources/Core/SessionDiff/SessionDiff+ExtraData.swift +++ b/Sources/Core/SessionDiff/SessionDiff+ExtraData.swift @@ -55,6 +55,7 @@ extension SessionDiff { let dictionary = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return ExtraData() } + return ExtraData(dictionary: dictionary) } set { diff --git a/Sources/Core/SessionDiff/SessionDiff+ItemComparator.swift b/Sources/Core/SessionDiff/SessionDiff+ItemComparator.swift index e93fb40..2732061 100644 --- a/Sources/Core/SessionDiff/SessionDiff+ItemComparator.swift +++ b/Sources/Core/SessionDiff/SessionDiff+ItemComparator.swift @@ -15,6 +15,7 @@ extension SessionDiff { let rightPath else { fatalError("Both leftPath and rightPath must be set") } + let options = comparatorOptions let (isLeftCaseSensitive, isRightCaseSensitive) = options.fileNameCase( leftPath: URL(filePath: leftPath), diff --git a/Sources/Core/SessionDiff/SessionDiff+ResolvePath.swift b/Sources/Core/SessionDiff/SessionDiff+ResolvePath.swift index 3b1e5f9..cbcde81 100644 --- a/Sources/Core/SessionDiff/SessionDiff+ResolvePath.swift +++ b/Sources/Core/SessionDiff/SessionDiff+ResolvePath.swift @@ -39,19 +39,19 @@ extension SessionDiff { alwaysResolveSymlinks: alwaysResolveSymlinks ) // assign to sessionDiff only if path differs otherwise the document is considered dirty - guard let (resolvedUrl, selectedAnotherPath) = resolvedInfo else { + guard let (resolvedURL, selectedAnotherPath) = resolvedInfo else { return nil } if selectedAnotherPath { if resolveLeft { - leftPath = resolvedUrl.osPath + leftPath = resolvedURL.osPath } else { - rightPath = resolvedUrl.osPath + rightPath = resolvedURL.osPath } } - return resolvedUrl + return resolvedURL } } diff --git a/Sources/Core/SessionDiff/SessionDiff.swift b/Sources/Core/SessionDiff/SessionDiff.swift index 0b03f0e..75fbabc 100644 --- a/Sources/Core/SessionDiff/SessionDiff.swift +++ b/Sources/Core/SessionDiff/SessionDiff.swift @@ -121,7 +121,8 @@ extension SessionDiff { guard let exclusionFileFilters else { return nil } - return NSPredicate(format: exclusionFileFilters) + + return try? NSPredicate.createSafe(withFormat: exclusionFileFilters) } var leftPath: String? { @@ -226,6 +227,7 @@ extension SessionDiff { !path.isEmpty else { return nil } + let isFolder = if let itemType { itemType == .folder } else { diff --git a/Sources/Features/FilesCompare/Components/FileInfoBar.swift b/Sources/Features/FilesCompare/Components/FileInfoBar.swift index b4ae9cf..6171ff8 100644 --- a/Sources/Features/FilesCompare/Components/FileInfoBar.swift +++ b/Sources/Features/FilesCompare/Components/FileInfoBar.swift @@ -172,6 +172,7 @@ class FileInfoBar: NSView { setLabel("") return } + let dateFormatter = DateFormatter() dateFormatter.dateFormat = DateFormatter.dateFormat( @@ -196,6 +197,7 @@ class FileInfoBar: NSView { guard FileManager.default.fileExists(atPath: path) else { return changed } + let attrs = try? FileManager.default.attributesOfItem(atPath: path) if let attrs, let updatedDate = attrs[.modificationDate] as? Date, diff --git a/Sources/Features/FilesCompare/Components/FileThumbnailView.swift b/Sources/Features/FilesCompare/Components/FileThumbnailView.swift index 59261ca..58c0027 100644 --- a/Sources/Features/FilesCompare/Components/FileThumbnailView.swift +++ b/Sources/Features/FilesCompare/Components/FileThumbnailView.swift @@ -36,6 +36,7 @@ class FileThumbnailView: NSView { guard let diffResult else { return .zero } + let resultLinesCount = diffResult.leftSide.lines.count let top = getPositionFromLine(section.start, linesCount: resultLinesCount, bounds: bounds) let height = getPositionFromLine(section.end - section.start + 1, linesCount: resultLinesCount, bounds: bounds) @@ -47,6 +48,7 @@ class FileThumbnailView: NSView { guard let view else { return } + let firstRow = view.firstVisibleRow let lastRow = view.lastVisibleRow @@ -69,6 +71,7 @@ class FileThumbnailView: NSView { drawPositionBox(bounds) return } + // draw background backgroundColor.setFill() bounds.fill() @@ -107,6 +110,7 @@ class FileThumbnailView: NSView { let view else { return } + let localPoint = convert(event.locationInWindow, from: nil) let leftLinesCount = diffResult.leftSide.lines.count var line = getLineFromPosition(localPoint, linesCount: leftLinesCount, bounds: bounds) diff --git a/Sources/Features/FilesCompare/Components/FilesScopeBar.swift b/Sources/Features/FilesCompare/Components/FilesScopeBar.swift index 3a332db..eb20dde 100644 --- a/Sources/Features/FilesCompare/Components/FilesScopeBar.swift +++ b/Sources/Features/FilesCompare/Components/FilesScopeBar.swift @@ -6,21 +6,18 @@ // Copyright (c) 2011 visualdiffer.com // -// Items for scopebarFileGroupDisplayOptions -private let showWhitespacesId = "WhiteSpacesId" +// items for scopebarFileGroupDisplayOptions +private let showWhitespacesID = "WhiteSpacesId" -// Items for scopebarFileGroupFilterOptions -private let allId = "AllId" -private let differencesId = "JustDiffsId" -private let justMatchesId = "JustMatchesId" +// items for scopebarFileGroupFilterOptions +private let allID = "AllId" +private let differencesID = "JustDiffsId" +private let justMatchesID = "JustMatchesId" -@objc protocol FilesScopeBarDelegate: AnyObject { - @objc func filesScopeBar(_ filesScopeBar: FilesScopeBar, action: FilesScopeBarAction) } -@objc enum FilesScopeBarAction: Int { case showWhitespaces case showAllLines @@ -51,7 +48,7 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { } var actionDelegate: FilesScopeBarDelegate? - @objc var findView: FindText + var findView: FindText override init(frame frameRect: NSRect) { findView = FindText(frame: NSRect(x: 0, y: 0, width: 400, height: 25)) @@ -70,7 +67,6 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { fontSize = 11.0 } - @objc func initScopeBar(_ actionDelegate: FilesScopeBarDelegate) { showLinesFilter = DiffLine.Visibility.loadFromUserDefaults() showWhitespaces = CommonPrefs.shared.bool(forKey: .FileScope.showWhitespaces) @@ -82,7 +78,7 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { groupItems.append([ .selectionMode: MGScopeBarGroupSelectionMode.multiple, .items: [ - mkItem(showWhitespacesId, NSLocalizedString("Show Whitespace", comment: "")), + mkItem(showWhitespacesID, NSLocalizedString("Show Whitespace", comment: "")), ], ]) @@ -90,14 +86,14 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { .separator: true, .selectionMode: MGScopeBarGroupSelectionMode.radio, .items: [ - mkItem(allId, NSLocalizedString("All", comment: "")), - mkItem(differencesId, NSLocalizedString("Just Differences", comment: "")), - mkItem(justMatchesId, NSLocalizedString("Just Matches", comment: "")), + mkItem(allID, NSLocalizedString("All", comment: "")), + mkItem(differencesID, NSLocalizedString("Just Differences", comment: "")), + mkItem(justMatchesID, NSLocalizedString("Just Matches", comment: "")), ], ]) - // Dictionary doesn't preserve order so we can't use it to fill the array - // So first fill the array then labels + // dictionaries do not preserve order, so we cannot use one to fill the array + // so we fill the array first, then the labels labels.removeAll() for group in groupItems { if let groupItems = group[.items] as? [[ScopeBarItem: String]] { @@ -132,8 +128,9 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { func scopeBar(_: MGScopeBar, itemIdentifiersForGroup groupNumber: Int) -> [Any] { guard let items = groupItems[groupNumber][.items], let itemIdentifiers = items as? [[ScopeBarItem: String]] else { - fatalError("Unexpected data format in groupItems") + return [] } + return itemIdentifiers.compactMap { $0[.identifier] } } @@ -153,7 +150,7 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { } func scopeBar(_: MGScopeBar, showSeparatorBeforeGroup groupNumber: Int) -> Bool { - // Optional method. If not implemented, all groups except the first will have a separator before them. + // optional method, if not implemented all groups except the first have a separator before them groupItems[groupNumber][.separator] as? Bool ?? false } @@ -170,20 +167,21 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { let group = FileScopeGroupOptions(rawValue: groupNumber) else { return } + switch group { case .display: - if identifier == showWhitespacesId { + if identifier == showWhitespacesID { showWhitespaces.toggle() actionDelegate.filesScopeBar(self, action: .showWhitespaces) } case .filter: - if identifier == allId { + if identifier == allID { showLinesFilter = .all actionDelegate.filesScopeBar(self, action: .showAllLines) - } else if identifier == justMatchesId { + } else if identifier == justMatchesID { showLinesFilter = .matches actionDelegate.filesScopeBar(self, action: .showJustMatchingLines) - } else if identifier == differencesId { + } else if identifier == differencesID { showLinesFilter = .differences actionDelegate.filesScopeBar(self, action: .showJustDifferentLines) } @@ -211,7 +209,7 @@ class FilesScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { showWhitespaces = show setSelected( show, - forItem: showWhitespacesId, + forItem: showWhitespacesID, inGroup: FileScopeGroupOptions.display.rawValue, informDelegate: informDelegate ) @@ -242,11 +240,11 @@ extension DiffLine.Visibility { var identifier: String { switch self { case .all: - allId + allID case .matches: - justMatchesId + justMatchesID case .differences: - differencesId + differencesID } } } diff --git a/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift b/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift index 938d306..21a867e 100644 --- a/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift +++ b/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift @@ -6,14 +6,12 @@ // Copyright (c) 2020 visualdiffer.com // -@objc @MainActor class FilesTableViewFindTextDelegate: NSObject, @preconcurrency FindTextDelegate { let view: FilesTableView private var lines = [Int]() - @objc init(view: FilesTableView) { self.view = view } @@ -34,14 +32,14 @@ class FilesTableViewFindTextDelegate: NSObject, @preconcurrency FindTextDelegate var range = re.rangeOfFirstMatch( in: line, options: [], - range: NSRange(location: 0, length: line.count) + range: NSRange(location: 0, length: line.utf16.count) ) if range.location == NSNotFound { line = right[i].text range = re.rangeOfFirstMatch( in: line, options: [], - range: NSRange(location: 0, length: line.count) + range: NSRange(location: 0, length: line.utf16.count) ) } diff --git a/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift b/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift index 4d7bf75..57f52a0 100644 --- a/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift +++ b/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift @@ -59,6 +59,7 @@ class RowHeightCalculator { let text = dataSource.line(at: row, side: side)?.text else { return 0 } + let font = dataSource.tableFont if !isWordWrapEnabled { diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Clipboard.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Clipboard.swift index d2d784e..c192e00 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Clipboard.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Clipboard.swift @@ -18,6 +18,7 @@ extension FilesWindowController { guard let diffSide = lastUsedView.diffSide else { return } + let selectedRows = lastUsedView.selectedRowIndexes let arr = diffSide.lines var lines = selectedRows.map { arr[$0].text } @@ -37,6 +38,7 @@ extension FilesWindowController { guard let diffResult else { return } + let row = max(-1, lastUsedView.selectedRow) let diffSide = diffResult.diffSide(for: lastUsedView.side) diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesScopeBar.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesScopeBar.swift index 75d5422..75a5361 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesScopeBar.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesScopeBar.swift @@ -25,26 +25,23 @@ extension FilesWindowController: @preconcurrency FilesScopeBarDelegate { } } - @objc func showAllLines(_: AnyObject) { refreshLinesStatus() } - @objc func showJustMatchingLines(_: AnyObject) { refreshLinesStatus() } - @objc func showJustDifferentLines(_: AnyObject) { refreshLinesStatus() } - @objc func refreshLinesStatus() { guard let diffResult else { return } + let diffResultInUse: DiffResult? // always use the current selected filter @@ -81,6 +78,7 @@ extension FilesWindowController: @preconcurrency FilesScopeBarDelegate { let currentDiffResult else { return 0 } + // determine the current line number let currentVisibleRow = leftView.firstVisibleRow diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift index 740194d..52f164e 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift @@ -44,6 +44,7 @@ extension FilesWindowController: @preconcurrency FilesTableViewContextMenu { guard let path = lastUsedView.side == .left ? sessionDiff.leftPath : sessionDiff.rightPath else { return } + NSWorkspace.shared.show(inFinder: [path]) } @@ -53,6 +54,7 @@ extension FilesWindowController: @preconcurrency FilesTableViewContextMenu { let editorData = lastUsedView.editorData(sessionDiff) else { return } + openWith(app: URL(filePath: app), attributes: [editorData]) } @@ -66,6 +68,7 @@ extension FilesWindowController: @preconcurrency FilesTableViewContextMenu { guard let editorData = lastUsedView.editorData(sessionDiff) else { return } + openWithOtherApp(editorData) } @@ -119,6 +122,7 @@ extension FilesWindowController: @preconcurrency FilesTableViewContextMenu { guard let indexes = currentDiffResult?.findSectionIndexSet(with: lastUsedView.selectedRow) else { return } + lastUsedView.selectRowIndexes(indexes, byExtendingSelection: false) } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+JumpLine.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+JumpLine.swift index 375b9dc..7fe06f8 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+JumpLine.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+JumpLine.swift @@ -13,6 +13,7 @@ extension FilesWindowController { let currentDiffResult else { return } + let jumpToLineWindow = JumpToLineWindow.createSheet() jumpToLineWindow.lineNumber = findStartJumpLine() @@ -35,6 +36,7 @@ extension FilesWindowController { guard let arr = lastUsedView.diffSide?.lines else { return 1 } + let lineNumber = arr[row].number if lineNumber < 0 { @@ -51,6 +53,7 @@ extension FilesWindowController { returnCode == .OK else { return } + var row = -1 var view: FilesTableView switch jumpToLineWindow.side { diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift index 8ee8641..0819f61 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift @@ -32,6 +32,7 @@ extension FilesWindowController: NSWindowDelegate { guard let diffResult else { return } + differenceCounters.update(counters: DiffCountersItem.diffCounter(withResult: diffResult)) } @@ -49,6 +50,10 @@ extension FilesWindowController: NSWindowDelegate { leftView.isDirty = false rightView.isDirty = false reload(nil) + NotificationCenter.default.postFileUpdated( + leftPath: sessionDiff.leftPath, + rightPath: sessionDiff.rightPath + ) if leftChanged, rightChanged { differenceCounters.stringValue = NSLocalizedString("Reloaded left and right files", comment: "") } else if leftChanged { diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift index af98305..d0c5092 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift @@ -55,6 +55,11 @@ extension FilesWindowController { navigateToFile(gotoNext, showAnim: showAnim) return } + if showAnim, + !CommonPrefs.shared.fileWrapsAroundDifferences, + (document as? VDDocument)?.parentSession != nil { + showNoFileOSD(gotoNext) + } } guard CommonPrefs.shared.fileWrapsAroundDifferences else { return @@ -92,7 +97,7 @@ extension FilesWindowController { let block: DiffOpenerDelegateBlock = { leftPath, rightPath in if leftPath == nil, rightPath == nil { - self.showNoFileOSD(!navigateToNext) + self.showNoFileOSD(navigateToNext) return false } if !self.alertSaveDirtyFiles() { @@ -127,11 +132,11 @@ extension FilesWindowController { } } - func showNoFileOSD(_ noPrevFile: Bool) { - if noPrevFile { - showOSD(image: NSImage(named: VDImageNameTop), text: NSLocalizedString("No Previous File", comment: "")) - } else { + func showNoFileOSD(_ noNextFile: Bool) { + if noNextFile { showOSD(image: NSImage(named: VDImageNameBottom), text: NSLocalizedString("No Next File", comment: "")) + } else { + showOSD(image: NSImage(named: VDImageNameTop), text: NSLocalizedString("No Previous File", comment: "")) } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift index 928ddb1..66fcd61 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift @@ -11,6 +11,7 @@ extension FilesWindowController: PathControlDelegate { guard let url = pathControl.url else { return } + menu.setSubmenu( NSMenu.appsMenuForFile( url, @@ -25,7 +26,7 @@ extension FilesWindowController: PathControlDelegate { ) } - func pathControl(_: PathControl, chosenUrl _: URL) { + func pathControl(_: PathControl, chosenURL _: URL) { // no need to check which path is changed (left or right) because // the binding value has already set sessionDiff.Path reloadAllMove(toFirstDifference: false) @@ -40,6 +41,7 @@ extension FilesWindowController: PathControlDelegate { guard let editorData = editorDataFrom(pathControl) else { return } + openWith(app: app, attributes: [editorData]) } @@ -47,6 +49,7 @@ extension FilesWindowController: PathControlDelegate { guard let editorData = editorDataFrom(pathControl) else { return } + openWithOtherApp(editorData) } @@ -56,6 +59,6 @@ extension FilesWindowController: PathControlDelegate { } else if pathControl === rightPanelView.pathView.pathControl { return rightView.editorData(sessionDiff) } - fatalError("Unable to determine which editor data to return") + return nil } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+RowHeightDataSource.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+RowHeightDataSource.swift index 23435b3..fed3830 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+RowHeightDataSource.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+RowHeightDataSource.swift @@ -25,6 +25,7 @@ extension FilesWindowController: RowHeightDataSource { guard let currentDiffResult else { return nil } + let diffSide = side == .left ? currentDiffResult.leftSide : currentDiffResult.rightSide return diffSide.lines[row] diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift index d6ed719..19ea8ac 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift @@ -7,7 +7,6 @@ // extension FilesWindowController { - @objc func alertSaveDirtyFiles() -> Bool { if !leftView.isDirty, !rightView.isDirty { return true @@ -96,18 +95,9 @@ extension FilesWindowController { fileThumbnail.needsDisplay = true window?.toolbar?.validateVisibleItems() - var userInfo = [FileSavedKey: String]() - - if let path = sessionDiff.leftPath { - userInfo[.leftPath] = path - } - if let path = sessionDiff.rightPath { - userInfo[.rightPath] = path - } - NotificationCenter.default.post( - name: .fileSaved, - object: nil, - userInfo: userInfo + NotificationCenter.default.postFileUpdated( + leftPath: sessionDiff.leftPath, + rightPath: sessionDiff.rightPath ) } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift index f1e2966..07bbd56 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift @@ -12,6 +12,7 @@ extension FilesWindowController { guard let window else { return } + sessionPreferencesSheet.beginSheet( window, preferences: preferences, diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift index 7ac8236..fe2f208 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift @@ -16,11 +16,11 @@ extension FilesWindowController { rightView.reloadData(restoreSelection: true) } - @objc func setSliderMaxValue() { guard let diffResult else { return } + leftPanelView.setSliderMaxValue( diffResult.leftSide.lines, right: diffResult.rightSide.lines diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift index 32c4292..0f45c16 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift @@ -7,7 +7,6 @@ // extension FilesWindowController { - @objc func initAllViews() { setupWindowLayout() @@ -41,6 +40,7 @@ extension FilesWindowController { guard let contentView = window?.contentView else { return } + var leadingMargin: CGFloat = 7 var trailingMargin: CGFloat = 5 if #available(macOS 26, *) { @@ -89,6 +89,7 @@ extension FilesWindowController { guard let window else { return } + window.delegate = self window.toolbar = NSToolbar(identifier: "FilesToolbar", delegate: self) window.makeFirstResponder(leftPanelView.treeView) @@ -96,7 +97,6 @@ extension FilesWindowController { window.collectionBehavior = [window.collectionBehavior, .fullScreenPrimary] } - @objc func updateUI() { updateTreeViewFont() @@ -233,11 +233,11 @@ extension FilesWindowController { leftView.reloadData() rightView.reloadData() - if let diffResult { - differenceCounters.update(counters: DiffCountersItem.diffCounter(withResult: diffResult)) - } else { - fatalError("diffResult is nil, why???") + guard let diffResult else { + return } + + differenceCounters.update(counters: DiffCountersItem.diffCounter(withResult: diffResult)) } // MARK: - File and view scroll diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController.swift index 202107c..4d6df0a 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController.swift @@ -48,8 +48,7 @@ class FilesWindowController: NSWindowController { lazy var rightDetailsTextView = createLineDetailTextView() lazy var topBottomView: WindowOSD = .init( - // swiftlint:disable:next force_unwrapping - image: NSImage(named: VDImageNameBottom)!, + image: NSImage.required(named: VDImageNameBottom), parent: window ) diff --git a/Sources/Features/FilesCompare/DiffResult/DiffLineComponent.swift b/Sources/Features/FilesCompare/DiffResult/DiffLineComponent.swift index 3d431c3..0c833f4 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffLineComponent.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffLineComponent.swift @@ -38,6 +38,7 @@ struct DiffLineComponent { guard substringRange.upperBound < wholeRange.upperBound else { return .missing } + return EndOfLine.from(character: text[substringRange.upperBound]) } } diff --git a/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift b/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift index ebd6d73..1412ff6 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift @@ -29,6 +29,7 @@ Logger.debug.error("Unable to open file ' \(fullPath)'") return } + defer { fileHandle.closeFile() } diff --git a/Sources/Features/FilesCompare/DiffResult/DiffResult.swift b/Sources/Features/FilesCompare/DiffResult/DiffResult.swift index 98c3ee4..3a27d27 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffResult.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffResult.swift @@ -56,11 +56,13 @@ class DiffResult { rightLines: [DiffLineComponent], options: Options = [] ) { - // swiftlint:disable force_cast let stringifier: (Any) -> String = { - options.applyTransformations(component: $0 as! DiffLineComponent) + guard let component = $0 as? DiffLineComponent else { + preconditionFailure("Expected a DiffLineComponent") + } + + return options.applyTransformations(component: component) } - // swiftlint:enable force_cast let udiff = UnifiedDiff( originalLines: leftLines, diff --git a/Sources/Features/FilesCompare/DiffResult/DiffSide.swift b/Sources/Features/FilesCompare/DiffResult/DiffSide.swift index b96e4f4..158acaf 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffSide.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffSide.swift @@ -38,21 +38,17 @@ class DiffSide { path: URL, encoding: String.Encoding ) throws { - let fm = FileManager.default - let osPath = path.osPath - - if !fm.fileExists(atPath: osPath) { - // NSFileHandle needs an existing file - fm.createFile(atPath: osPath, contents: nil) + // validate that all lines can be encoded before truncating the file; + // temp-file approach was discarded due to symlink and sandbox issues + for line in lines where line.type != .missing { + _ = try encodedData(for: line, encoding: encoding) } - let fileHandle = try FileHandle(forWritingTo: path) + + let fileHandle = try openFileHandle(forWritingTo: path) defer { fileHandle.closeFile() } - fileHandle.truncateFile(atOffset: 0) for line in lines where line.type != .missing { - if let data = line.component.withEol.data(using: encoding) { - fileHandle.write(data) - } + try fileHandle.write(encodedData(for: line, encoding: encoding)) } } @@ -70,4 +66,29 @@ class DiffSide { line.type == .missing ? nil : line.component } } + + private func encodedData( + for line: DiffLine, + encoding: String.Encoding + ) throws -> Data { + guard let data = line.component.withEol.data(using: encoding) else { + throw FileError.encodingFailed(encoding: encoding) + } + + return data + } + + private func openFileHandle(forWritingTo url: URL) throws -> FileHandle { + let fm = FileManager.default + let osPath = url.osPath + + if !fm.fileExists(atPath: osPath) { + // FileHandle needs an existing file + fm.createFile(atPath: osPath, contents: nil) + } + let fileHandle = try FileHandle(forWritingTo: url) + fileHandle.truncateFile(atOffset: 0) + + return fileHandle + } } diff --git a/Sources/Features/FilesCompare/Extensions/FilesTableView+EditorData.swift b/Sources/Features/FilesCompare/Extensions/FilesTableView+EditorData.swift index 1fa23b7..c5ec1b2 100644 --- a/Sources/Features/FilesCompare/Extensions/FilesTableView+EditorData.swift +++ b/Sources/Features/FilesCompare/Extensions/FilesTableView+EditorData.swift @@ -17,6 +17,7 @@ extension FilesTableView { guard let path else { return nil } + var editorData = OpenEditorAttribute(path: path) if let diffSide, selectedRow >= 0 { diff --git a/Sources/Features/FilesCompare/Windows/JumpToLineWindow.swift b/Sources/Features/FilesCompare/Windows/JumpToLineWindow.swift index 0eb4d19..6630930 100644 --- a/Sources/Features/FilesCompare/Windows/JumpToLineWindow.swift +++ b/Sources/Features/FilesCompare/Windows/JumpToLineWindow.swift @@ -114,6 +114,7 @@ class JumpToLineWindow: NSWindow, NSWindowDelegate, NSSearchFieldDelegate { guard let contentView else { return } + NSLayoutConstraint.activate([ searchField.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), searchField.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 10), @@ -141,8 +142,11 @@ class JumpToLineWindow: NSWindow, NSWindowDelegate, NSSearchFieldDelegate { private func enableJumpButton() { guard let side = DisplaySide(rawValue: jumpSide.selectedSegment) else { - fatalError("invalid jump side \(jumpSide.selectedSegment)") + maxLineNumber.stringValue = "" + standardButtons.primaryButton.isEnabled = false + return } + let value = searchField.intValue var isEnabled = false @@ -160,8 +164,9 @@ class JumpToLineWindow: NSWindow, NSWindowDelegate, NSSearchFieldDelegate { @objc func closeSheet(_ sender: AnyObject) { guard let side = DisplaySide(rawValue: jumpSide.selectedSegment) else { - fatalError("invalid jump side \(jumpSide.selectedSegment)") + return } + self.side = side lineNumber = searchField.integerValue sheetParent?.endSheet(self, returnCode: NSApplication.ModalResponse(rawValue: sender.tag)) diff --git a/Sources/Features/FilesCompare/Windows/SessionPreferences/FileSessionPreferencesWindow.swift b/Sources/Features/FilesCompare/Windows/SessionPreferences/FileSessionPreferencesWindow.swift index 45959d6..0c1d2c6 100644 --- a/Sources/Features/FilesCompare/Windows/SessionPreferences/FileSessionPreferencesWindow.swift +++ b/Sources/Features/FilesCompare/Windows/SessionPreferences/FileSessionPreferencesWindow.swift @@ -90,6 +90,7 @@ class FileSessionPreferencesWindow: NSWindowController, NSTabViewDelegate { guard let contentView = window?.contentView else { return } + NSLayoutConstraint.activate([ tabView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), tabView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), @@ -114,6 +115,7 @@ class FileSessionPreferencesWindow: NSWindowController, NSTabViewDelegate { guard let window else { return } + fill(with: preferences) tabView.selectTabViewItem(at: selectedTab.rawValue) @@ -126,6 +128,7 @@ class FileSessionPreferencesWindow: NSWindowController, NSTabViewDelegate { guard let window else { return } + let response = NSApplication.ModalResponse(sender.tag) if response == .OK { window.endEditing() @@ -190,6 +193,7 @@ class FileSessionPreferencesWindow: NSWindowController, NSTabViewDelegate { guard let window else { return 0 } + let windowFrame = NSWindow.contentRect(forFrameRect: window.frame, styleMask: window.styleMask) return windowFrame.size.height - (window.contentView?.frame.size.height ?? 0) } @@ -205,6 +209,7 @@ class FileSessionPreferencesWindow: NSWindowController, NSTabViewDelegate { window.isVisible else { return } + let newSize = NSSize( width: window.contentView?.frame.size.width ?? 0, height: minWindowHeight() diff --git a/Sources/Features/FoldersCompare/CompareItem/ComparatorOptions/ComparatorOptions+Helper.swift b/Sources/Features/FoldersCompare/CompareItem/ComparatorOptions/ComparatorOptions+Helper.swift index a83cfd0..87aae21 100644 --- a/Sources/Features/FoldersCompare/CompareItem/ComparatorOptions/ComparatorOptions+Helper.swift +++ b/Sources/Features/FoldersCompare/CompareItem/ComparatorOptions/ComparatorOptions+Helper.swift @@ -45,7 +45,7 @@ public extension ComparatorOptions { func changeAlign(_ newValue: Self) -> Self { guard newValue.isSubset(of: .alignMask) else { - fatalError("Invalid options: \(newValue)") + return self } var changed = withoutAlignFlags diff --git a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Accessors.swift b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Accessors.swift index 9642f28..8616749 100644 --- a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Accessors.swift +++ b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Accessors.swift @@ -45,6 +45,7 @@ extension CompareItem { guard let linkedItem else { return false } + return isFolder && !linkedItem.isValidFile } @@ -52,6 +53,7 @@ extension CompareItem { guard let linkedItem else { return false } + return type == .changed && linkedItem.type == .old } diff --git a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Clone.swift b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Clone.swift index 011e692..bbbfc85 100644 --- a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Clone.swift +++ b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Clone.swift @@ -87,7 +87,7 @@ extension CompareItem { dupItem.addSubfoldersSize(subfoldersSize) if recursive { - if let url = dupItem.toUrl() { + if let url = dupItem.toURL() { for item in children where item.isValidFile { if let fileName = item.fileName { let childPath = url.appendingPathComponent(fileName).osPath diff --git a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Comparison.swift b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Comparison.swift index 5cb4bf2..7839aa5 100644 --- a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Comparison.swift +++ b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Comparison.swift @@ -54,6 +54,7 @@ extension CompareItem { guard let srcRight = linkedItem else { return } + var leftFileCount = CompareSummary() var rightFileCount = CompareSummary() @@ -71,6 +72,7 @@ extension CompareItem { guard let right = left.linkedItem else { continue } + let isFiltered = isFiltered( leftItem: left, rightItem: right, @@ -176,6 +178,7 @@ extension CompareItem { guard let rhsFileName = $1.fileName else { return .orderedDescending } + return insensitiveCompare ? lhsFileName.localizedCaseInsensitiveCompare(rhsFileName) : lhsFileName.localizedCompare(rhsFileName) @@ -196,6 +199,7 @@ extension CompareItem { guard let rhsFileName = $1.fileName else { return .orderedDescending } + return lhsFileName.localizedCompare(rhsFileName) } } @@ -204,6 +208,7 @@ extension CompareItem { guard isValidFile else { return false } + var dict = [String: Any]() dict["fileName"] = fileName diff --git a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Path.swift b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Path.swift index a0142e2..69b4f82 100644 --- a/Sources/Features/FoldersCompare/CompareItem/CompareItem+Path.swift +++ b/Sources/Features/FoldersCompare/CompareItem/CompareItem+Path.swift @@ -11,13 +11,19 @@ extension CompareItem { guard let currentPath = path else { return nil } + var parent = parent - var rootPath = currentPath + var rootPath: String? while let p = parent, let parentPath = p.path { rootPath = parentPath parent = p.parent } + + guard let rootPath else { + return nil + } + // skip path separator let start = currentPath.index(currentPath.startIndex, offsetBy: rootPath.count + 1) return String(currentPath[start...]) @@ -30,6 +36,7 @@ extension CompareItem { guard root.isValidFile else { return nil } + if root.path == path { return root } @@ -61,17 +68,18 @@ extension CompareItem { } func buildDestinationPath( - from srcBaseUrl: URL, - to destBaseUrl: URL + from srcBaseURL: URL, + to destBaseURL: URL ) -> URL { - guard let srcUrl = toUrl() else { + guard let srcURL = toURL() else { fatalError("Path is not present on \(self)") } - let linkedUrl = linkedItem?.toUrl() - return URL.buildDestinationPath(srcUrl, linkedUrl, srcBaseUrl, destBaseUrl) + + let linkedURL = linkedItem?.toURL() + return URL.buildDestinationPath(srcURL, linkedURL, srcBaseURL, destBaseURL) } - func toUrl() -> URL? { + func toURL() -> URL? { if let path { URL(filePath: path, directoryHint: isFolder ? .isDirectory : .notDirectory) } else { diff --git a/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift b/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift index 35e6ace..35a6ab1 100644 --- a/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift +++ b/Sources/Features/FoldersCompare/Components/CompareItemTableCellView.swift @@ -170,6 +170,7 @@ class CompareItemTableCellView: NSView { guard let fileName = item.fileName else { return nil } + if item.isSymbolicLink, let path = item.path, let destination = try? FileManager.default.destinationOfSymbolicLink(atPath: path) { diff --git a/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift b/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift index 07f12d7..7375234 100644 --- a/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift +++ b/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift @@ -10,7 +10,6 @@ enum DisplayFiltersScopeBarAttributeKey: String { case filterFlagsDisplayFilters = "filterFlags" } -@objc enum DisplayFiltersScopeBarAction: Int { case selectFilter case showFiltered @@ -26,9 +25,9 @@ protocol DisplayFiltersScopeBarDelegate: AnyObject { ) } -private let showFilteredId = "FilteredId" -private let showEmptyFoldersId = "EmptyFoldersId" -private let showNoOrphansFoldersId = "NoOrphansFoldersId" +private let showFilteredID = "FilteredId" +private let showEmptyFoldersID = "EmptyFoldersId" +private let showNoOrphansFoldersID = "NoOrphansFoldersId" private enum ScopeGroupOptions: Int { case displayFilters @@ -36,14 +35,13 @@ private enum ScopeGroupOptions: Int { case displayFlags } -@objc @MainActor class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { private var groupItems = [[ScopeBarGroupKey: Any]]() private var labels = [String: String]() var actionDelegate: DisplayFiltersScopeBarDelegate? - @objc var findView: FindText + var findView: FindText override init(frame frameRect: NSRect) { findView = FindText(frame: NSRect(x: 0, y: 0, width: 400, height: 25)) @@ -79,28 +77,28 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { ], ]) - // Folders related group + // folders related group groupItems.append([ .label: NSLocalizedString("Folders:", comment: ""), .separator: true, .selectionMode: MGScopeBarGroupSelectionMode.multiple, .items: [ - mkItem(showEmptyFoldersId, NSLocalizedString("Empty", comment: "")), - mkItem(showNoOrphansFoldersId, NSLocalizedString("No Orphans", comment: "")), + mkItem(showEmptyFoldersID, NSLocalizedString("Empty", comment: "")), + mkItem(showNoOrphansFoldersID, NSLocalizedString("No Orphans", comment: "")), ], ]) - // Filtered group + // filtered group groupItems.append([ .separator: true, .selectionMode: MGScopeBarGroupSelectionMode.multiple, .items: [ - mkItem(showFilteredId, NSLocalizedString("Filtered", comment: "")), + mkItem(showFilteredID, NSLocalizedString("Filtered", comment: "")), ], ]) - // Dictionary doesn't preserve order so we can't use it to fill the array - // So first fill the array then labels + // dictionaries do not preserve order, so we cannot use one to fill the array + // so we fill the array first, then the labels labels.removeAll() for group in groupItems { if let groupItems = group[.items] as? [[ScopeBarItem: String]] { @@ -136,8 +134,9 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { func scopeBar(_: MGScopeBar, itemIdentifiersForGroup groupNumber: Int) -> [Any] { guard let items = groupItems[groupNumber][.items], let itemIdentifiers = items as? [[ScopeBarItem: String]] else { - fatalError("Unexpected data format in groupItems") + return [] } + return itemIdentifiers.compactMap { $0[.identifier] } } @@ -157,7 +156,7 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { } func scopeBar(_: MGScopeBar, showSeparatorBeforeGroup groupNumber: Int) -> Bool { - // Optional method. If not implemented, all groups except the first will have a separator before them. + // optional method, if not implemented all groups except the first have a separator before them groupItems[groupNumber][.separator] as? Bool ?? false } @@ -174,6 +173,7 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { let group = ScopeGroupOptions(rawValue: groupNumber) else { return } + switch group { case .displayFilters: if let filterValue = Int(identifier) { @@ -184,7 +184,7 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { ) } case .displayFlags: - if identifier == showFilteredId { + if identifier == showFilteredID { actionDelegate.displayFiltersScopeBar( self, action: .showFiltered, @@ -192,13 +192,13 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { ) } case .displayFolders: - if identifier == showNoOrphansFoldersId { + if identifier == showNoOrphansFoldersID { actionDelegate.displayFiltersScopeBar( self, action: .showNoOrphansFolders, options: nil ) - } else if identifier == showEmptyFoldersId { + } else if identifier == showEmptyFoldersID { actionDelegate.displayFiltersScopeBar( self, action: .showEmptyFolders, @@ -210,22 +210,20 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { // MARK: - Actions - @objc func hideEmptyFolders(_ hideEmptyFolders: Bool, informDelegate _: Bool) { - // The logic to select the 'empty folders' button is inverted so we pass the negated value + // the logic to select the 'empty folders' button is inverted, so we pass the negated value setSelected( !hideEmptyFolders, - forItem: showEmptyFoldersId, + forItem: showEmptyFoldersID, inGroup: ScopeGroupOptions.displayFolders.rawValue, informDelegate: false ) } - @objc func showFilteredFiles(_ showFilteredFiles: Bool, informDelegate: Bool) { setSelected( showFilteredFiles, - forItem: showFilteredId, + forItem: showFilteredID, inGroup: ScopeGroupOptions.displayFolders.rawValue, informDelegate: informDelegate ) @@ -240,11 +238,10 @@ class DisplayFiltersScopeBar: MGScopeBar, @preconcurrency MGScopeBarDelegate { ) } - @objc func noOrphansFolders(_ noOrphansFolders: Bool, informDelegate: Bool) { setSelected( noOrphansFolders, - forItem: showNoOrphansFoldersId, + forItem: showNoOrphansFoldersID, inGroup: ScopeGroupOptions.displayFolders.rawValue, informDelegate: informDelegate ) diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/ColoredFoldersManager.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/ColoredFoldersManager.swift index 065001d..75a05b4 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/ColoredFoldersManager.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/ColoredFoldersManager.swift @@ -10,7 +10,7 @@ class ColoredFoldersManager: NSObject, @unchecked Sendable { private let queue = DispatchQueue(label: "com.visualdiffer.colored.folders", attributes: .concurrent) private var foldersColors: [String: NSImage] - @objc static let shared = ColoredFoldersManager() + static let shared = ColoredFoldersManager() override private init() { foldersColors = Self.buildColoredFolders() @@ -35,7 +35,6 @@ class ColoredFoldersManager: NSObject, @unchecked Sendable { return icon(folderName: imageFileName) } - @objc func refresh() { queue.async(flags: .barrier) { self.foldersColors = Self.buildColoredFolders() @@ -60,7 +59,6 @@ class ColoredFoldersManager: NSObject, @unchecked Sendable { let mismatchingLabelsColor = prefs.changeTypeColor(.mismatchingLabels)?.text else { fatalError("Unable to get colors for colored folders") } - guard let maskFull = NSImage(named: "mask-full"), let maskBackWhite = NSImage(named: "mask-back-white"), let maskBack = NSImage(named: "mask-back"), diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/CompareItem+LeafPath.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/CompareItem+LeafPath.swift index f6f2579..b176272 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/CompareItem+LeafPath.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/CompareItem+LeafPath.swift @@ -46,18 +46,18 @@ extension CompareItem { /// - Returns: The longest common ancestor path, or `nil` if any item has no parent /// or the items list is empty. static func commonAncestorPath(_ items: [CompareItem]) -> String? { - let parentUrls = items.compactMap { $0.parent?.toUrl() } + let parentURLs = items.compactMap { $0.parent?.toURL() } - guard parentUrls.count == items.count, - let first = parentUrls.first else { + guard parentURLs.count == items.count, + let first = parentURLs.first else { return nil } let firstComponents = first.pathComponents var commonCount = firstComponents.count - for parentUrl in parentUrls.dropFirst() { - let components = parentUrl.pathComponents + for parentURL in parentURLs.dropFirst() { + let components = parentURL.pathComponents let maxCount = min(commonCount, components.count) var index = 0 @@ -68,6 +68,7 @@ extension CompareItem { guard index > 0 else { return nil } + commonCount = index } @@ -89,15 +90,15 @@ extension CompareItem { } func isAncestor(of child: CompareItem) -> Bool { - guard let parentUrl = toUrl(), - let childUrl = child.toUrl() else { + guard let parentURL = toURL(), + let childURL = child.toURL() else { return false } // pathComponents returns leading "/" and any extra trailing "/" // e.g. /a//b/c/// returns ["/", "a", "b", "c", "/"] so we filter out any "/" manually - let parentComponents = parentUrl.pathComponents.filter { $0 != "/" } - let childComponents = childUrl.pathComponents.filter { $0 != "/" } + let parentComponents = parentURL.pathComponents.filter { $0 != "/" } + let childComponents = childURL.pathComponents.filter { $0 != "/" } return childComponents.starts(with: parentComponents) } diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift index 57c0fdd..b49d876 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift @@ -94,7 +94,8 @@ extension FoldersOutlineView { guard let vi = item(atRow: row) as? VisibleItem else { continue } - let res = URL.compare(path: vi.item.toUrl(), with: focusItem.toUrl()) + + let res = URL.compare(path: vi.item.toURL(), with: focusItem.toURL()) if res == .orderedSame || res == .orderedDescending { focusRow = row diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift index 732f879..836b85c 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift @@ -19,11 +19,12 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona private var _selectionInfo: FolderSelectionInfo? var selectionInfo: FolderSelectionInfo { - if _selectionInfo == nil { - _selectionInfo = FolderSelectionInfo(view: self) + if let selectionInfo = _selectionInfo { + return selectionInfo } - // swiftlint:disable:next force_unwrapping - return _selectionInfo! + let selectionInfo = FolderSelectionInfo(view: self) + _selectionInfo = selectionInfo + return selectionInfo } private var lockExpand = false @@ -37,9 +38,9 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona setupViews() } - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + @available(*, unavailable, message: "use init(frame:)") + required init?(coder _: NSCoder) { + nil } private func setupViews() { @@ -61,7 +62,7 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona intercellSpacing = NSSize(width: 3, height: 2) allowsMultipleSelection = true - // Needed by drop + // needed by drag and drop registerForDraggedTypes([NSPasteboard.PasteboardType.fileURL]) setDraggingSourceOperationMask(.every, forLocal: true) setDraggingSourceOperationMask(.every, forLocal: false) @@ -93,6 +94,7 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona guard var itemRow = view.item(atRow: selectedRow) as? VisibleItem else { return } + var item = itemRow.item if !item.isValidFile { @@ -100,6 +102,7 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona let linkedItemRow = linkedView.item(atRow: selectedRow) as? VisibleItem else { return } + view = linkedView itemRow = linkedItemRow item = itemRow.item @@ -128,6 +131,7 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona super.keyDown(with: event) return } + let key = str[str.startIndex].asciiValue ?? 0 if key == NSCarriageReturnCharacter || key == NSEnterCharacter { @@ -219,6 +223,7 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona guard let delegate = delegate as? OutlineViewItemDelegate else { return } + if lockExpand { return } @@ -238,6 +243,7 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona guard let delegate = delegate as? OutlineViewItemDelegate else { return } + if lockExpand { return } diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift index a85eaa2..a0e2da4 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/IconUtils+CompareItemIconUtils.swift @@ -17,7 +17,7 @@ extension IconUtils { var icon: NSImage? if item.isValidFile, - let url = item.toUrl() { + let url = item.toURL() { if item.isLocked { if item.isFolder { let name = ColoredFoldersManager.shared.iconName( diff --git a/Sources/Features/FoldersCompare/Controller/CopyFilesTag.swift b/Sources/Features/FoldersCompare/Controller/CopyFilesTag.swift index 5043bba..e67323b 100644 --- a/Sources/Features/FoldersCompare/Controller/CopyFilesTag.swift +++ b/Sources/Features/FoldersCompare/Controller/CopyFilesTag.swift @@ -10,22 +10,23 @@ enum CopyFilesTag: Int { case fileContents = 0 case finderMetadataOnly = 1 - // swiftlint:disable void_function_in_ternary static func localizedTag(side: DisplaySide, tag: Int) -> String { switch side { case .left: - tag == finderMetadataOnly.rawValue - ? NSLocalizedString("Copy Metadata to Right...", comment: "") - : NSLocalizedString("Copy to Right...", comment: "") + if tag == finderMetadataOnly.rawValue { + NSLocalizedString("Copy Metadata to Right...", comment: "") + } else { + NSLocalizedString("Copy to Right...", comment: "") + } case .right: - tag == finderMetadataOnly.rawValue - ? NSLocalizedString("Copy Metadata to Left...", comment: "") - : NSLocalizedString("Copy to Left...", comment: "") + if tag == finderMetadataOnly.rawValue { + NSLocalizedString("Copy Metadata to Left...", comment: "") + } else { + NSLocalizedString("Copy to Left...", comment: "") + } } } - // swiftlint:enable void_function_in_ternary - @MainActor static func isCopyFinderMetadataOnly(sender: AnyObject?) -> Bool { let tag = if let menuItem = sender as? NSMenuItem { diff --git a/Sources/Features/FoldersCompare/Controller/ExternalFileOperationContext.swift b/Sources/Features/FoldersCompare/Controller/ExternalFileOperationContext.swift index fa9e933..b20cf10 100644 --- a/Sources/Features/FoldersCompare/Controller/ExternalFileOperationContext.swift +++ b/Sources/Features/FoldersCompare/Controller/ExternalFileOperationContext.swift @@ -21,11 +21,10 @@ struct ExternalFileOperationContext { ) -> ExternalFileOperationContext? { guard let vi = view.dataSource?.outlineView?(view, child: 0, ofItem: nil) as? VisibleItem, let root = vi.item.parent, - let srcBaseDir = root.toUrl() else { + let srcBaseDir = root.toURL() else { return nil } - - guard let destBaseDir = srcBaseDir.promptUrl( + guard let destBaseDir = srcBaseDir.promptURL( at: srcBaseDir, title: NSLocalizedString("Select destination folder", comment: ""), chooseDirectories: true, @@ -71,9 +70,10 @@ struct ExternalFileOperationContext { guard let itemPath = item.path else { continue } - guard let itemURL = item.toUrl() else { + guard let itemURL = item.toURL() else { continue } + let normalizedItemPath = URL( filePath: itemPath, directoryHint: item.isFolder ? .isDirectory : .notDirectory diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift index bd5fe61..215acb9 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemController.swift @@ -209,6 +209,7 @@ public class FileSystemController: NSWindowCon guard let contentView = window?.contentView else { return } + NSLayoutConstraint.activate([ mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), @@ -225,6 +226,7 @@ public class FileSystemController: NSWindowCon guard let window else { return } + if itemsCount() == 0 { return } @@ -247,6 +249,7 @@ public class FileSystemController: NSWindowCon let sheetParent = window.sheetParent else { return } + let tag = NSApplication.ModalResponse(sender.tag) sheetParent.endSheet(window, returnCode: tag) } @@ -266,6 +269,7 @@ public class FileSystemController: NSWindowCon guard let prefName = executor.prefName else { return } + let isSuppressed = checkboxSuppressDialog.state == .off CommonPrefs.shared.set(isSuppressed, forKey: prefName) diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemTestHelper.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemTestHelper.swift index affd16a..1b8117a 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemTestHelper.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/FileSystemTestHelper.swift @@ -167,6 +167,7 @@ let fs = vi.item.parent else { return } + let testHelper = FileSystemTestHelper(sessionDiff: sessionDiff) testHelper.generateTest(fs) testHelper.generateTest(fs.visibleItem!) @@ -356,6 +357,7 @@ guard fs.isValidFile else { return } + let path = fs.path! if let labelNumber = URL(filePath: path).labelNumber() { let linkedString = linked ? ".linkedItem" : "" @@ -386,6 +388,7 @@ let path = fs.path else { return } + let startIndex = path.index(path.startIndex, offsetBy: pathIndex) let subPath = String(path[startIndex ..< path.endIndex]) if fs.isFolder { @@ -418,6 +421,7 @@ !tags.isEmpty else { return } + strFiles.append(String( format: "try add(tags: [%@], fullPath: appendFolder(\"%@\"))\n", tags.joined(separator: ","), @@ -431,6 +435,7 @@ labelNumber != 0 else { return } + strFiles.append(String(format: "try add(labelNumber: %ld, fullPath: appendFolder(\"%@\"))\n", labelNumber, subPath)) } diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift index 80b2dc6..6334e42 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift @@ -61,6 +61,7 @@ class FileOperationManagerDelegateImpl: FileOperationManagerDelegate { guard let pic = progressIndicatorController else { return } + let path = item.path ?? "" DispatchQueue.main.async { @@ -90,6 +91,7 @@ class FileOperationManagerDelegateImpl: FileOperationManagerDelegate { guard let pic = progressIndicatorController else { return } + let path = item.path ?? "" DispatchQueue.main.async { @@ -101,6 +103,7 @@ class FileOperationManagerDelegateImpl: FileOperationManagerDelegate { guard let pic = progressIndicatorController else { return } + let fileSize = item.fileSize DispatchQueue.main.async { diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift index e3fa687..f149d18 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift @@ -202,6 +202,7 @@ class ProgressIndicatorController: NSWindowController { guard let sender = sender as? NSButton else { return } + if !running { closeSheet(sender) return diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileController.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileController.swift index d4d2984..e504ecd 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileController.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileController.swift @@ -60,6 +60,7 @@ class SyncFileController: FileSystemController { let root = vi.item.parent else { fatalError("Unable to get root") } + self.root = root createEmptyFolders = true @@ -114,6 +115,7 @@ class SyncFileController: FileSystemController { guard let contentView = window?.contentView else { return } + NSLayoutConstraint.activate([ mainStackView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), mainStackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), @@ -207,6 +209,7 @@ class SyncFileController: FileSystemController { nodes.children.isEmpty else { return } + let text = if view.side == .left { NSLocalizedString("No files to copy on the right", comment: "") } else { @@ -229,6 +232,7 @@ class SyncFileController: FileSystemController { guard let progressIndicatorController else { return } + progressIndicatorController.beginSheetModal( for: callerWindow, processingItemsCount: 0, @@ -250,6 +254,7 @@ class SyncFileController: FileSystemController { let rootLinkedPath = root.linkedItem?.path else { return } + prepareExecute() let capturedManager = fileOperationManager diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileOperationExecutor.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileOperationExecutor.swift index a87d7f6..d699a85 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileOperationExecutor.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncFileOperationExecutor.swift @@ -60,6 +60,7 @@ class SyncFileOperationExecutor: FileOperationExecutor, @unchecked Sendable { guard let payload else { fatalError("Missing payload") } + let srcBaseDir = payload.srcBaseDir let destBaseDir = payload.destBaseDir let items = payload.copyDestFiles ? itemsInfo.linkedInfo : itemsInfo diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncItemsInfo.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncItemsInfo.swift index 39660da..e965d8b 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncItemsInfo.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncItemsInfo.swift @@ -7,22 +7,21 @@ // class SyncItemsInfo: NSObject { - @objc var totalSize: Int64 = 0 - @objc var nodes: DescriptionOutlineNode? - @objc var emptyFoldersNodes: DescriptionOutlineNode? - @objc var linkedInfo: SyncItemsInfo? + var totalSize: Int64 = 0 + var nodes: DescriptionOutlineNode? + var emptyFoldersNodes: DescriptionOutlineNode? + var linkedInfo: SyncItemsInfo? - @objc func removeAll() { nodes?.children.removeAll() } - @objc func add(_ syncNode: DescriptionOutlineNode) { guard let syncDataSource = syncNode.items, let nodes else { return } + if !syncDataSource.isEmpty { nodes.children.append(syncNode) } diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncOutlineView.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncOutlineView.swift index 2d8b3e3..037bd9a 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncOutlineView.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Sync/SyncOutlineView.swift @@ -6,11 +6,9 @@ // Copyright (c) 2025 visualdiffer.com // -@objc class SyncOutlineView: NSOutlineView, NSOutlineViewDataSource, NSOutlineViewDelegate { var items: SyncItemsInfo - @objc init(items: SyncItemsInfo) { self.items = items super.init(frame: .zero) @@ -65,6 +63,7 @@ class SyncOutlineView: NSOutlineView, NSOutlineViewDataSource, NSOutlineViewDele let identifier = tableColumn?.identifier else { return nil } + let cell = outlineView.makeView( withIdentifier: identifier, owner: self diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchController.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchController.swift index a269e63..9d069bf 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchController.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchController.swift @@ -156,6 +156,7 @@ class TouchController: FileSystemController { let source = TouchDateFromSource(rawValue: sender.tag) else { return } + pickers.isEnabled = switch source { case .otherSide: false diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchPickersStackView.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchPickersStackView.swift index 846ab58..284580a 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchPickersStackView.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Touch/TouchPickersStackView.swift @@ -26,6 +26,7 @@ class TouchPickersStackView: NSStackView { guard let gregorian = NSCalendar(calendarIdentifier: .gregorian) else { return nil } + var dateComponents = datePickers.components(calendar: gregorian) let timeComponents = timePickers.components(calendar: gregorian) diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift index e1e7f70..05b6b21 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift @@ -123,6 +123,7 @@ public extension FoldersWindowController { guard let tag = sender?.selectedItem?.tag as? Int else { return } + var compareFlags = sessionDiff.comparatorOptions.withoutMethodFlags compareFlags.insert(ComparatorOptions(rawValue: tag)) sessionDiff.comparatorOptions = compareFlags diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+DisplayFiltersScopeBarDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+DisplayFiltersScopeBarDelegate.swift index fa8c41b..a8b2046 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+DisplayFiltersScopeBarDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+DisplayFiltersScopeBarDelegate.swift @@ -24,6 +24,7 @@ extension FoldersWindowController: @preconcurrency DisplayFiltersScopeBarDelegat guard let newFlags = newFlags?.intValue else { return } + let displayOptions = sessionDiff.displayOptions.changeWithoutMethod(newFlags) sessionDiff.displayOptions = displayOptions let refreshInfo = RefreshInfo( diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift index e5a5686..9aeaa09 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift @@ -50,7 +50,7 @@ extension FoldersWindowController { let predicateTemplate = NSPredicate(format: "fileName ENDSWITH $name") lastUsedView.enumerateSelectedValidFiles { item, _ in // use extension - if let path = item.toUrl() { + if let path = item.toURL() { let pathExtension = String(format: ".%@", path.pathExtension) let bindVariables = ["name": pathExtension] let result = predicateTemplate.withSubstitutionVariables(bindVariables) diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FileSystemControllerDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FileSystemControllerDelegate.swift index eedbda4..fd0ed7f 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FileSystemControllerDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FileSystemControllerDelegate.swift @@ -18,6 +18,7 @@ extension FoldersWindowController: @preconcurrency FileSystemControllerDelegate let destBaseDir = root.linkedItem?.path else { return } + let executor = CopyFileOperationExecutor( srcBaseDir: srcBaseDir, destination: .linkedSide(baseDir: URL(filePath: destBaseDir, directoryHint: .isDirectory)), @@ -79,6 +80,7 @@ extension FoldersWindowController: @preconcurrency FileSystemControllerDelegate let srcBaseDir = root.path else { return } + let executor = DeleteFileOperationExecutor( srcBaseDir: srcBaseDir, items: lastUsedView.selectedItems() @@ -94,6 +96,7 @@ extension FoldersWindowController: @preconcurrency FileSystemControllerDelegate let destBaseDir = root.linkedItem?.path else { return } + let executor = MoveFileOperationExecutor( srcBaseDir: srcBaseDir, destination: .linkedSide(baseDir: URL(filePath: destBaseDir, directoryHint: .isDirectory)), @@ -188,6 +191,7 @@ extension FoldersWindowController: @preconcurrency FileSystemControllerDelegate guard let window else { return } + let pic = ProgressIndicatorController() progressIndicatorController = pic let delegate = FileOperationManagerDelegateImpl(progressIndicatorController: pic) @@ -209,6 +213,7 @@ extension FoldersWindowController: @preconcurrency FileSystemControllerDelegate guard let comparator else { fatalError("Comparator not found") } + let config = FilterConfig( from: sessionDiff, showFilteredFiles: showFilteredFiles, @@ -221,6 +226,7 @@ extension FoldersWindowController: @preconcurrency FileSystemControllerDelegate delegate: delegate ) } + return builder(config, comparator, delegate) } } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift index f2238da..dd71e43 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift @@ -12,11 +12,11 @@ extension FoldersWindowController: NSOutlineViewDelegate, NSOutlineViewDataSource, FoldersOutlineViewDelegate, OutlineViewItemDelegate { - @objc var leftView: FoldersOutlineView { + var leftView: FoldersOutlineView { leftPanelView.treeView } - @objc var rightView: FoldersOutlineView { + var rightView: FoldersOutlineView { rightPanelView.treeView } @@ -188,12 +188,10 @@ extension FoldersWindowController: NSOutlineViewDelegate, rightView.reloadData() } - @objc func sortBySessionColumn() { guard let leftVisibleItems else { return } - guard let vi = sessionDiff.currentSortSide == .left ? leftVisibleItems : leftVisibleItems.linkedItem else { return } @@ -228,6 +226,7 @@ extension FoldersWindowController: NSOutlineViewDelegate, guard let arr = pasteboard.readObjects(forClasses: [NSURL.self]) as? [URL] else { return result } + let path1 = arr[0].osPath var isDir = ObjCBool(false) let isValidPath1 = FileManager.default.fileExists(atPath: path1, isDirectory: &isDir) && isDir.boolValue @@ -260,6 +259,7 @@ extension FoldersWindowController: NSOutlineViewDelegate, guard let arr = pasteboard.readObjects(forClasses: [NSURL.self]) as? [URL] else { return false } + if arr.count < 2 { if let path = arr.last?.osPath { if view.side == .left { @@ -285,7 +285,7 @@ extension FoldersWindowController: NSOutlineViewDelegate, public func outlineView(_: NSOutlineView, pasteboardWriterForItem item: Any) -> (any NSPasteboardWriting)? { guard let vi = item as? VisibleItem, - let url = vi.item.toUrl() else { + let url = vi.item.toURL() else { return nil } @@ -298,6 +298,7 @@ extension FoldersWindowController: NSOutlineViewDelegate, guard let itemRow = view.item(atRow: clickedRow) as? VisibleItem else { return } + let leftItem = view.side == .left ? itemRow.item : itemRow.item.linkedItem guard let leftItem, let rightItem = leftItem.linkedItem else { @@ -306,8 +307,8 @@ extension FoldersWindowController: NSOutlineViewDelegate, do { if let document = try VDDocumentController.shared.openDifferDocument( - leftUrl: leftItem.toUrl(), - rightUrl: rightItem.toUrl() + leftURL: leftItem.toURL(), + rightURL: rightItem.toURL() ) { addChildDocument(document) } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift index 079abb6..f3867b3 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift @@ -29,8 +29,8 @@ extension FoldersWindowController: FoldersOutlineViewContextMenu { } do { _ = try VDDocumentController.shared.openDifferDocument( - leftUrl: leftItem?.toUrl(), - rightUrl: rightItem?.toUrl() + leftURL: leftItem?.toURL(), + rightURL: rightItem?.toURL() ) } catch { NSAlert(error: error).runModal() diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift index 434e309..2d3ed45 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift @@ -439,7 +439,6 @@ extension FoldersWindowController { return menu } - @objc static func switchMenu() { @MainActor enum StaticMenus { @@ -450,6 +449,7 @@ extension FoldersWindowController { guard let mainMenu = NSApp.mainMenu else { return } + mainMenu.item(withTag: MainMenu.edit.rawValue)?.submenu = StaticMenus.edit mainMenu.item(withTag: MainMenu.actions.rawValue)?.submenu = StaticMenus.actions mainMenu.item(withTag: MainMenu.view.rawValue)?.submenu = StaticMenus.view diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift index b9b77a7..f72cf35 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift @@ -160,7 +160,7 @@ extension FoldersWindowController: NSMenuDelegate, if row >= 0, let vi = lastUsedView.item(atRow: row) as? VisibleItem { menu.addMenuItemsForFile( - vi.item.toUrl(), + vi.item.toURL(), openAppAction: #selector(openWithApp), openOtherAppAction: #selector(openWithOther) ) @@ -177,6 +177,7 @@ extension FoldersWindowController: NSMenuDelegate, guard let folderView = tableView as? FoldersOutlineView else { return false } + let action = menuItem.action hide = true diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift index 4dee513..a48aaab 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift @@ -77,6 +77,7 @@ extension FoldersWindowController: NSToolbarDelegate, NSToolbarItemValidation { guard let item = notification.userInfo?["item"] as? NSToolbarItem else { return } + updateToolbarButton(item) } @@ -326,6 +327,7 @@ extension FoldersWindowController: NSToolbarDelegate, NSToolbarItemValidation { let visibleItems = toolbar.visibleItems else { return } + for item in visibleItems { // swiftlint:disable:next for_where if item.itemIdentifier == .Folders.comparison { diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Observers.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Observers.swift index 8e51132..b278571 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Observers.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Observers.swift @@ -33,7 +33,7 @@ extension FoldersWindowController { NotificationCenter.default.addObserver( self, selector: #selector(refreshCompareItem), - name: .fileSaved, + name: .fileUpdated, object: nil ) @@ -63,7 +63,7 @@ extension FoldersWindowController { ) NotificationCenter.default.removeObserver( self, - name: .fileSaved, + name: .fileUpdated, object: nil ) NotificationCenter.default.removeObserver( @@ -89,7 +89,7 @@ extension FoldersWindowController { @objc func refreshCompareItem(_ notification: Notification) { guard let comparator, - let userInfo = notification.userInfo as? [FileSavedKey: String], + let userInfo = notification.userInfo as? [FileUpdatedKey: String], let (item, itemSide) = resolveCompareItem(fromUserInfo: userInfo) else { return } @@ -134,7 +134,7 @@ extension FoldersWindowController { } private func resolveCompareItem( - fromUserInfo userInfo: [FileSavedKey: String?] + fromUserInfo userInfo: [FileUpdatedKey: String?] ) -> (item: CompareItem, itemSide: DisplaySide)? { guard let leftItemOriginal, let rightItemOriginal, diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+PathControlDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+PathControlDelegate.swift index daebcc2..fa866f8 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+PathControlDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+PathControlDelegate.swift @@ -28,7 +28,7 @@ extension FoldersWindowController: PathControlDelegate { ).tag = pathControlMenu } - func pathControl(_: PathControl, chosenUrl _: URL) { + func pathControl(_: PathControl, chosenURL _: URL) { // no need to check witch path is changed (left or right) because // the binding value has already set sessionDiff.Path reloadAll(RefreshInfo( @@ -44,7 +44,6 @@ extension FoldersWindowController: PathControlDelegate { openPanel.canChooseFiles = false } - @objc func isPathControlMenu(_ tag: Int) -> Bool { (tag & pathControlMenu) == pathControlMenu } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Select.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Select.swift index c193171..a6f2e92 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Select.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Select.swift @@ -28,6 +28,7 @@ extension FoldersWindowController { guard let sender = sender as? NSMenuItem else { return } + let side = SelectionSide(menuItem: sender) if side.contains(.left) { @@ -43,6 +44,7 @@ extension FoldersWindowController { guard let sender = sender as? NSMenuItem else { return } + let side = SelectionSide(menuItem: sender) if side.contains(.left) { @@ -64,6 +66,7 @@ extension FoldersWindowController { guard let sender = sender as? NSMenuItem else { return } + let side = SelectionSide(menuItem: sender) let isShiftDown = NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false @@ -82,6 +85,7 @@ extension FoldersWindowController { guard let sender = sender as? NSMenuItem else { return } + let side = SelectionSide(menuItem: sender) let isShiftDown = NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false @@ -100,6 +104,7 @@ extension FoldersWindowController { guard let sender = sender as? NSMenuItem else { return } + let side = SelectionSide(menuItem: sender) if side.contains(.both) { diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+SessionPreferences.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+SessionPreferences.swift index 9fdc293..b99fdd9 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+SessionPreferences.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+SessionPreferences.swift @@ -12,6 +12,7 @@ extension FoldersWindowController { guard let window else { return } + sessionPreferencesSheet.beginSheet( window, sessionDiff: sessionDiff, @@ -26,6 +27,7 @@ extension FoldersWindowController { guard let window else { return } + sessionPreferencesSheet.beginSheet( window, sessionDiff: sessionDiff, diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift index ce0fcf3..e076963 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift @@ -12,7 +12,6 @@ extension FoldersWindowController { setupFoldersLayout() leftPanelView.treeView.nextKeyView = rightView - lastUsedView = leftPanelView.treeView updateTreeViewFont() diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift index af984e3..3a5f45b 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift @@ -33,10 +33,9 @@ public class FoldersWindowController: NSWindowController, var hideEmptyFolders = false var showFilteredFiles = false - // swiftlint:disable:next implicitly_unwrapped_optional - var lastUsedView: FoldersOutlineView! + lazy var lastUsedView = leftPanelView.treeView - // ComparatorPopUpButtonCell uses the tag property to select item but + // comparatorPopUpButtonCell uses the tag property to select the item, but // sessionDiff.comparatorFlags bitmask should not match the tag value, so // sessionDiff.comparatorFlags is bit-masked with comparatorMethod in selection action methods var comparatorMethod: ComparatorOptions = [] { @@ -48,8 +47,7 @@ public class FoldersWindowController: NSWindowController, } } - // swiftlint:disable:next implicitly_unwrapped_optional - var currentFont: NSFont! + var currentFont = CommonPrefs.shared.folderListingFont var comparator: ItemComparator? lazy var sessionPreferencesSheet: SessionPreferencesWindow = .init() @@ -124,9 +122,9 @@ public class FoldersWindowController: NSWindowController, initAllViews() } - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + @available(*, unavailable, message: "use init()") + required init?(coder _: NSCoder) { + nil } public nonisolated func userNotificationCenter(_: UNUserNotificationCenter, willPresent _: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { @@ -140,6 +138,7 @@ public class FoldersWindowController: NSWindowController, guard let path = sender.representedObject as? String else { return } + let application = URL(filePath: path) lastUsedView.openSelected(with: application) } @@ -194,6 +193,7 @@ public class FoldersWindowController: NSWindowController, guard let window else { return } + let folderCompareInfoWindow = FolderCompareInfoWindow.createSheet() folderCompareInfoWindow.leftRoot = leftItemOriginal @@ -277,7 +277,7 @@ public class FoldersWindowController: NSWindowController, #if DEBUG override public func keyDown(with event: NSEvent) { - // Cmd + F12 pressed, create the test code + // cmd + F12 pressed, create the test code if event.modifierFlags.contains(.command), event.charactersIgnoringModifiers?.unicodeScalars.first?.value == UInt32(NSF12FunctionKey) { FileSystemTestHelper.createTestCode(leftView, sessionDiff: sessionDiff) diff --git a/Sources/Features/FoldersCompare/Extensions/DiffCountersItem+CompareItem.swift b/Sources/Features/FoldersCompare/Extensions/DiffCountersItem+CompareItem.swift index 7dca38c..42b42fc 100644 --- a/Sources/Features/FoldersCompare/Extensions/DiffCountersItem+CompareItem.swift +++ b/Sources/Features/FoldersCompare/Extensions/DiffCountersItem+CompareItem.swift @@ -118,6 +118,7 @@ extension DiffCountersItem { guard fileCount > 0 else { return } + let text = String.localizedStringWithFormat(format, fileCount, side.rawValue) if let color = CommonPrefs.shared.changeTypeColor(type)?.text { items.append(diffCounterItem(withText: text, color: color)) diff --git a/Sources/Features/FoldersCompare/FileManager/CompareItem+Metadata.swift b/Sources/Features/FoldersCompare/FileManager/CompareItem+Metadata.swift index c8ae306..311bad7 100644 --- a/Sources/Features/FoldersCompare/FileManager/CompareItem+Metadata.swift +++ b/Sources/Features/FoldersCompare/FileManager/CompareItem+Metadata.swift @@ -49,7 +49,7 @@ extension CompareItem { toPath destPath: inout URL, options: DirectoryOptions = [] ) throws { - guard let url = toUrl() else { + guard let url = toURL() else { return } diff --git a/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift b/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift index 3e37e5e..d3bd62a 100644 --- a/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift +++ b/Sources/Features/FoldersCompare/FileManager/CopyCompareItem.swift @@ -20,6 +20,7 @@ class CopyCompareItem { guard let value = operationManager.copyFinderMetadataOnly else { return false } + return value } @@ -66,6 +67,7 @@ class CopyCompareItem { ) return } + var srcCount = CompareSummary() var destCount = CompareSummary() @@ -84,8 +86,8 @@ class CopyCompareItem { var parent = srcRoot.parent while let item = parent, - let fsUrl = item.toUrl() { - let destFullPath = URL.buildDestinationPath(fsUrl, nil, srcBaseDir, context.baseDir) + let fsURL = item.toURL() { + let destFullPath = URL.buildDestinationPath(fsURL, nil, srcBaseDir, context.baseDir) item.addOlderFiles(-srcCount.olderFiles) item.addChangedFiles(-srcCount.changedFiles) @@ -135,16 +137,16 @@ class CopyCompareItem { guard delegate.isRunning(operationManager) else { return false } - guard srcRoot.isValidFile else { return true } + let isFiltered = srcRoot.isFiltered || !srcRoot.isDisplayed if !operationManager.includesFiltered, isFiltered { return true } guard let srcRootPath = srcRoot.path, - let srcUrl = srcRoot.toUrl() else { + let srcURL = srcRoot.toURL() else { return true } @@ -167,7 +169,7 @@ class CopyCompareItem { destFullPath = try context.destinationPath( srcRoot: srcRoot, srcBaseDir: srcBaseDir, - srcUrl: srcUrl + srcURL: srcURL ) } catch { delegate.fileManager(operationManager, addError: error, forItem: srcRoot) @@ -243,6 +245,7 @@ class CopyCompareItem { guard let srcRootPath = srcRoot.path else { return } + let delegate = operationManager.delegate var srcCount = srcRoot.summary var destCount = destRoot?.summary ?? .init() @@ -250,11 +253,11 @@ class CopyCompareItem { let destBaseDir = context.baseDir do { let srcAttrs = try fm.attributesOfItem(atPath: srcRootPath) - let srcUrl = srcRoot.toUrl() + let srcURL = srcRoot.toURL() let destFullPath = try context.destinationPath( srcRoot: srcRoot, srcBaseDir: srcBaseDir, - srcUrl: srcUrl + srcURL: srcURL ) var destAttrs: [FileAttributeKey: Any]? var destError: NSError? @@ -353,16 +356,17 @@ class CopyCompareItem { lastPathTimestamps: PathTimestamps?, volumeType: String ) throws { - guard let srcUrl = srcRoot.toUrl() else { + guard let srcURL = srcRoot.toURL() else { return } + #if DEBUG && __VD_FAKE_FS_OP__ Logger.debug.info("Fake copy, no files are really copied - \(path.localPath)") #else if isBigFile { try bigFileManager.copy(srcRoot, destFullPath: destFullPath.osPath) } else { - try fm.copyItem(at: srcUrl, to: destFullPath) + try fm.copyItem(at: srcURL, to: destFullPath) } if let lastPathTimestamps { try fm.setFileAttributes( @@ -452,6 +456,7 @@ class CopyCompareItem { guard context.isLinkedSide, let destRoot else { return } + var srcCount = srcRoot.summary var destCount = destRoot.summary var destFullPath = destFullPath @@ -473,6 +478,7 @@ class CopyCompareItem { guard let fsSrcPath = (srcRoot.path as? NSString)?.fileSystemRepresentation else { throw FolderManagerError.nilPath } + let fsDestPath = (destFullPath.osPath as NSString).fileSystemRepresentation copyfile(fsSrcPath, fsDestPath, nil, copyfile_flags_t(COPYFILE_METADATA)) diff --git a/Sources/Features/FoldersCompare/FileManager/DeleteCompareItem.swift b/Sources/Features/FoldersCompare/FileManager/DeleteCompareItem.swift index fe2a121..76d6aea 100644 --- a/Sources/Features/FoldersCompare/FileManager/DeleteCompareItem.swift +++ b/Sources/Features/FoldersCompare/FileManager/DeleteCompareItem.swift @@ -315,6 +315,7 @@ class DeleteCompareItem { guard let destRoot = srcRoot.linkedItem else { return nil } + if informDelegate { delegate.fileManager(operationManager, initForItem: srcRoot) } @@ -386,6 +387,7 @@ class DeleteCompareItem { guard let path = item.path else { throw FolderManagerError.nilPath } + #if __VD_FAKE_FS_OP__ Logger.debug.info("Fake delete, no files are really deleted - \(path)") #else diff --git a/Sources/Features/FoldersCompare/FileManager/FileOperationDestination.swift b/Sources/Features/FoldersCompare/FileManager/FileOperationDestination.swift index 0243309..e527dce 100644 --- a/Sources/Features/FoldersCompare/FileManager/FileOperationDestination.swift +++ b/Sources/Features/FoldersCompare/FileManager/FileOperationDestination.swift @@ -27,11 +27,12 @@ public struct FileDestinationContext { case let .external(baseDir): isLinkedSide = false isExternal = true - resolvePath = { _, srcBaseDir, srcUrl in - guard let srcUrl else { + resolvePath = { _, srcBaseDir, srcURL in + guard let srcURL else { throw FolderManagerError.nilPath } - return URL.buildDestinationPath(srcUrl, nil, srcBaseDir, baseDir) + + return URL.buildDestinationPath(srcURL, nil, srcBaseDir, baseDir) } } } @@ -39,9 +40,9 @@ public struct FileDestinationContext { func destinationPath( srcRoot: CompareItem, srcBaseDir: URL, - srcUrl: URL? + srcURL: URL? ) throws -> URL { - try resolvePath(srcRoot, srcBaseDir, srcUrl) + try resolvePath(srcRoot, srcBaseDir, srcURL) } func destinationRoot(for srcRoot: CompareItem) -> CompareItem? { @@ -86,6 +87,7 @@ extension FileOperationDestination { guard isExternal else { return srcBaseDir } + if items.count == 1 { return items[0].parent?.path ?? srcBaseDir } diff --git a/Sources/Features/FoldersCompare/FileManager/FileOperationManager+Util.swift b/Sources/Features/FoldersCompare/FileManager/FileOperationManager+Util.swift index a9f4604..224fbc5 100644 --- a/Sources/Features/FoldersCompare/FileManager/FileOperationManager+Util.swift +++ b/Sources/Features/FoldersCompare/FileManager/FileOperationManager+Util.swift @@ -81,9 +81,10 @@ extension FileOperationManager { destFullPath: URL ) throws { if !filterConfig.followSymLinks, srcRoot.isSymbolicLink { - guard let srcRootPath = srcRoot.toUrl() else { + guard let srcRootPath = srcRoot.toURL() else { throw FolderManagerError.nilPath } + try createSymLink( atPath: destFullPath, destinationOfSymLinkAtPath: srcRootPath diff --git a/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift b/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift index 194c7b9..c39c584 100644 --- a/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift +++ b/Sources/Features/FoldersCompare/FileManager/MoveCompareItem.swift @@ -50,6 +50,7 @@ public class MoveCompareItem: NSObject { guard srcRoot.isValidFile else { return } + let context = FileDestinationContext(destination: destination) if context.isExternal { moveToExternal( @@ -97,9 +98,9 @@ public class MoveCompareItem: NSObject { var parent = srcRoot.parent while let item = parent, - let itemUrl = item.toUrl() { - let destUrl = URL.buildDestinationPath(itemUrl, nil, srcBaseDir, destBaseDir) - let destFullPath = destUrl.osPath + let itemURL = item.toURL() { + let destURL = URL.buildDestinationPath(itemURL, nil, srcBaseDir, destBaseDir) + let destFullPath = destURL.osPath item.addOlderFiles(-srcCount.olderFiles) item.addChangedFiles(-srcCount.changedFiles) @@ -143,6 +144,7 @@ public class MoveCompareItem: NSObject { ) return } + _ = doMoveToExternal( srcRoot, srcBaseDir: srcBaseDir, @@ -167,21 +169,23 @@ public class MoveCompareItem: NSObject { guard srcRoot.isValidFile else { return true } + let isFiltered = srcRoot.isFiltered || !srcRoot.isDisplayed if !operationManager.includesFiltered, isFiltered { return true } guard let srcRootPath = srcRoot.path, - let srcUrl = srcRoot.toUrl() else { + let srcURL = srcRoot.toURL() else { return false } + let destBaseDir = context.baseDir let destFullPath: URL do { destFullPath = try context.destinationPath( srcRoot: srcRoot, srcBaseDir: srcBaseDir, - srcUrl: srcUrl + srcURL: srcURL ) } catch { delegate.fileManager(operationManager, addError: error, forItem: srcRoot) @@ -285,11 +289,9 @@ public class MoveCompareItem: NSObject { guard delegate.isRunning(operationManager) else { return false } - guard srcRoot.isValidFile else { return true } - guard let destRoot = srcRoot.linkedItem else { throw FolderManagerError.nilPath } @@ -487,9 +489,10 @@ public class MoveCompareItem: NSObject { #if DEBUG && __VD_FAKE_FS_OP__ Logger.debug.info("Fake move, no files are really moved - \(srcRoot.path)") #else - guard let url = srcRoot.toUrl() else { + guard let url = srcRoot.toURL() else { throw FolderManagerError.nilPath } + if isBigFile { try bigFileManager.move(srcRoot, destFullPath: destFullPath.osPath) } else { @@ -575,10 +578,10 @@ public class MoveCompareItem: NSObject { guard operationManager.canRemoveDirectory(srcRoot) else { return } - guard let srcPath = srcRoot.path else { throw FolderManagerError.nilPath } + let fsSrcPath = (srcPath as NSString).fileSystemRepresentation let fsDestPath = (destFullPath.osPath as NSString).fileSystemRepresentation diff --git a/Sources/Features/FoldersCompare/FileManager/PathTimestamps.swift b/Sources/Features/FoldersCompare/FileManager/PathTimestamps.swift index 6ed0e8e..e3e74b2 100644 --- a/Sources/Features/FoldersCompare/FileManager/PathTimestamps.swift +++ b/Sources/Features/FoldersCompare/FileManager/PathTimestamps.swift @@ -48,7 +48,7 @@ func createDirectory( var directories: [CompareItem] = [] while let localItem = item, - let url = localItem.toUrl(), + let url = localItem.toURL(), url != srcBaseDir { directories.insert(localItem, at: 0) item = localItem.parent @@ -63,6 +63,7 @@ func createDirectory( let fsFileName = fsDir.fileName else { break } + path = path.appending(path: fsFileName, directoryHint: fsDir.isFolder ? .isDirectory : .notDirectory) if fm.fileExists(atPath: path.osPath) { @@ -77,11 +78,11 @@ func createDirectory( try attrs?.applyTo(itemAtPath: path.deletingLastPathComponent()) if options.contains(.copyLabels) { - try fsDir.toUrl()?.copyLabel(to: &path) + try fsDir.toURL()?.copyLabel(to: &path) } if options.contains(.copyTags) { - try fsDir.toUrl()?.copyTags(to: &path) + try fsDir.toURL()?.copyTags(to: &path) } attrs = try PathTimestamps(fromFileAttributes: fm.attributesOfItem(atPath: fsPath)) diff --git a/Sources/Features/FoldersCompare/FileManager/RenameCompareItem.swift b/Sources/Features/FoldersCompare/FileManager/RenameCompareItem.swift index 8444227..1b1a94f 100644 --- a/Sources/Features/FoldersCompare/FileManager/RenameCompareItem.swift +++ b/Sources/Features/FoldersCompare/FileManager/RenameCompareItem.swift @@ -21,12 +21,11 @@ class RenameCompareItem { srcRoot: CompareItem, toName: String ) { - guard let srcUrl = srcRoot.toUrl(), + guard let srcURL = srcRoot.toURL(), srcRoot.isValidFile else { return } - - guard let volumeType = srcUrl.volumeType() else { + guard let volumeType = srcURL.volumeType() else { operationManager.delegate.fileManager( operationManager, addError: FileError.unknownVolumeType, @@ -89,22 +88,21 @@ class RenameCompareItem { return true } - guard let srcUrl = srcRoot.toUrl() else { + guard let srcURL = srcRoot.toURL() else { throw FolderManagerError.nilPath } - guard let destRoot = srcRoot.linkedItem else { throw FolderManagerError.nilPath } - let toPath = srcUrl + let toPath = srcURL .deletingLastPathComponent() .appendingPathComponent(toName) var renamedSrcRoot: CompareItem? do { - try fm.moveItem(at: srcUrl, to: toPath) + try fm.moveItem(at: srcURL, to: toPath) // search fileName on other side let fileIndex = destRoot.parent?.findChildFileNameIndex( toName, @@ -257,11 +255,12 @@ class RenameCompareItem { renamedSrcRoot.linkedItem = item item.linkedItem = renamedSrcRoot - operationManager.comparator.alignItem( - renamedSrcRoot, + let context = AlignContext( + leftRoot: renamedSrcRoot, rightRoot: item, - alignConfig: AlignConfig(recursive: true, followSymLinks: operationManager.filterConfig.followSymLinks) + config: AlignConfig(recursive: true, followSymLinks: operationManager.filterConfig.followSymLinks) ) + operationManager.comparator.alignItem(context) // TODO: expensive renamedSrcRoot.applyComparison( @@ -313,11 +312,12 @@ class RenameCompareItem { renamedSrcRoot.linkedItem = item item.linkedItem = renamedSrcRoot - operationManager.comparator.alignItem( - renamedSrcRoot, + let context = AlignContext( + leftRoot: renamedSrcRoot, rightRoot: item, - alignConfig: AlignConfig(recursive: true, followSymLinks: operationManager.filterConfig.followSymLinks) + config: AlignConfig(recursive: true, followSymLinks: operationManager.filterConfig.followSymLinks) ) + operationManager.comparator.alignItem(context) parentSrcCount.orphanFiles -= renamedSrcRoot.orphanFiles diff --git a/Sources/Features/FoldersCompare/FileManager/TouchCompareItem.swift b/Sources/Features/FoldersCompare/FileManager/TouchCompareItem.swift index 43beee9..f85021c 100644 --- a/Sources/Features/FoldersCompare/FileManager/TouchCompareItem.swift +++ b/Sources/Features/FoldersCompare/FileManager/TouchCompareItem.swift @@ -22,12 +22,11 @@ class TouchCompareItem { includeSubfolders: Bool, touchDate: Date? ) { - guard let srcUrl = srcRoot.toUrl(), + guard let srcURL = srcRoot.toURL(), srcRoot.isValidFile else { return } - - guard let volumeType = srcUrl.volumeType() else { + guard let volumeType = srcURL.volumeType() else { operationManager.delegate.fileManager( operationManager, addError: FileError.unknownVolumeType, @@ -75,6 +74,7 @@ class TouchCompareItem { guard let date else { return nil } + return [.modificationDate: date] } @@ -93,10 +93,10 @@ class TouchCompareItem { guard delegate.isRunning(operationManager) else { return false } - guard srcRoot.isValidFile else { return true } + let isFiltered = srcRoot.isFiltered || !srcRoot.isDisplayed if !operationManager.includesFiltered, isFiltered { return true @@ -107,10 +107,10 @@ class TouchCompareItem { guard let dateDict = buildTouchDateAttributes(attrs: attrs, item: linkedItem) else { return true } - guard let srcRootPath = srcRoot.path else { return true } + delegate.fileManager(operationManager, initForItem: srcRoot) do { try fm.setFileAttributes( @@ -203,6 +203,7 @@ class TouchCompareItem { guard let fileFilters = operationManager.filterConfig.predicate else { return } + let isFiltered = lhs.evaluate(filter: fileFilters) || rhs.evaluate(filter: fileFilters) lhs.isFiltered = isFiltered rhs.isFiltered = isFiltered @@ -215,6 +216,7 @@ class TouchCompareItem { guard !operationManager.filterConfig.showFilteredFiles, lhs.isFiltered else { return } + if let parentVI = lhs.parent?.visibleItem, let vi = lhs.visibleItem { parentVI.remove(vi) @@ -238,6 +240,7 @@ class TouchCompareItem { guard includeSubfolders, srcRoot.isFolder else { return true } + var srcCount = CompareSummary() var destCount = CompareSummary() var retVal = true diff --git a/Sources/Features/FoldersCompare/Navigator/CompareItem+DifferenceNavigator.swift b/Sources/Features/FoldersCompare/Navigator/CompareItem+DifferenceNavigator.swift index c606b9a..5ef08c2 100644 --- a/Sources/Features/FoldersCompare/Navigator/CompareItem+DifferenceNavigator.swift +++ b/Sources/Features/FoldersCompare/Navigator/CompareItem+DifferenceNavigator.swift @@ -6,7 +6,6 @@ // Copyright (c) 2021 visualdiffer.com // -@objc extension CompareItem { var hasDifferences: Bool { !isValidFile || diff --git a/Sources/Features/FoldersCompare/Navigator/FoldersOutlineView+DifferenceNavigator.swift b/Sources/Features/FoldersCompare/Navigator/FoldersOutlineView+DifferenceNavigator.swift index 5a5b811..41ff7d5 100644 --- a/Sources/Features/FoldersCompare/Navigator/FoldersOutlineView+DifferenceNavigator.swift +++ b/Sources/Features/FoldersCompare/Navigator/FoldersOutlineView+DifferenceNavigator.swift @@ -31,6 +31,7 @@ extension FoldersOutlineView { guard let foundItem else { return nil } + expandParents(of: foundItem) select( visibleItems: [foundItem], @@ -56,6 +57,7 @@ extension FoldersOutlineView { guard let from = item(atRow: row) as? VisibleItem else { return nil } + var count: Int if wrapAround { diff --git a/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader+Log.swift b/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader+Log.swift index da3447a..6b7f186 100644 --- a/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader+Log.swift +++ b/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader+Log.swift @@ -26,6 +26,7 @@ import os.log guard let fileHandle else { return } + defer { fileHandle.closeFile() } diff --git a/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift b/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift index 78824ae..40c0154 100644 --- a/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift +++ b/Sources/Features/FoldersCompare/Services/FolderReader/FolderReader.swift @@ -115,11 +115,12 @@ public class FolderReader: @unchecked Sendable { recursive: false, followSymLinks: filterConfig.followSymLinks ) - comparator.alignItem( - leftItem, + let context = AlignContext( + leftRoot: leftItem, rightRoot: rightItem, - alignConfig: alignConfig + config: alignConfig ) + comparator.alignItem(context) } leftItem.applyComparison( @@ -210,6 +211,7 @@ public class FolderReader: @unchecked Sendable { guard let parentPath else { return nil } + do { // symlink root must be traversed so allocate it after checking for symlink let root = try createParentIfNil(path: parentPath, parent: parent) @@ -247,13 +249,13 @@ public class FolderReader: @unchecked Sendable { throw error } - let secureUrl = SecureBookmark.shared.secure( + let secureURL = SecureBookmark.shared.secure( fromBookmark: URL(filePath: destination), startSecured: true ) defer { - SecureBookmark.shared.stopAccessing(url: secureUrl) + SecureBookmark.shared.stopAccessing(url: secureURL) } return try fileManager.contentsOfDirectory(atPath: path) @@ -343,8 +345,8 @@ public class FolderReader: @unchecked Sendable { readFolders( leftItem: item, rightItem: li, - leftPath: item.toUrl(), - rightPath: li.toUrl() + leftPath: item.toURL(), + rightPath: li.toURL() ) } } diff --git a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Align.swift b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Align.swift index da1a06e..cf9a783 100644 --- a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Align.swift +++ b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Align.swift @@ -13,6 +13,25 @@ public struct AlignConfig { let followSymLinks: Bool } +public struct AlignContext { + let leftRoot: CompareItem + let rightRoot: CompareItem + let config: AlignConfig +} + +public struct AlignPosition { + var leftIndex = 0 + var rightIndex = 0 + + func leftChild(in context: AlignContext) -> CompareItem { + context.leftRoot.child(at: leftIndex) + } + + func rightChild(in context: AlignContext) -> CompareItem { + context.rightRoot.child(at: rightIndex) + } +} + // Both files are invalid but their path string contains a valid value @inline(__always) func bothInvalidWithPath(_ lfs: CompareItem, _ rfs: CompareItem) -> Bool { @@ -40,93 +59,87 @@ public extension ComparatorOptions { } } -// swiftlint:disable function_parameter_count public extension ItemComparator { func alignItem( - _ leftRoot: CompareItem, - rightRoot: CompareItem, - alignConfig: AlignConfig + _ context: AlignContext ) { - var lIndex = 0 - var rIndex = 0 - var leftChildrenCount = leftRoot.children.count - var rightChildrenCount = rightRoot.children.count + var position = AlignPosition() + var leftChildrenCount = context.leftRoot.children.count + var rightChildrenCount = context.rightRoot.children.count - while (lIndex < leftChildrenCount) || (rIndex < rightChildrenCount) { + while (position.leftIndex < leftChildrenCount) || (position.rightIndex < rightChildrenCount) { var pos: ComparisonResult - if lIndex >= leftChildrenCount { + if position.leftIndex >= leftChildrenCount { pos = .orderedDescending - } else if rIndex >= rightChildrenCount { + } else if position.rightIndex >= rightChildrenCount { pos = .orderedAscending - } else if (leftRoot.child(at: lIndex).isValidFile && rightRoot.child(at: rIndex).isValidFile) - || bothInvalidWithPath(leftRoot.child(at: lIndex), rightRoot.child(at: rIndex)) { - pos = align( - leftRoot, - rightRoot: rightRoot, - alignConfig: alignConfig, - leftIndex: &lIndex, - rightIndex: &rIndex - ) } else { - if !leftRoot.child(at: lIndex).isValidFile { - lIndex += 1 - } - if !rightRoot.child(at: rIndex).isValidFile { - rIndex += 1 + let lChild = position.leftChild(in: context) + let rChild = position.rightChild(in: context) + + if (lChild.isValidFile && rChild.isValidFile) || bothInvalidWithPath(lChild, rChild) { + pos = align(context, position: &position) + } else { + if !lChild.isValidFile { + position.leftIndex += 1 + } + if !rChild.isValidFile { + position.rightIndex += 1 + } + continue } - continue } if pos == .orderedSame { - if leftRoot.child(at: lIndex).isFile, rightRoot.child(at: rIndex).isFile { + // both indices are valid here: orderedSame is only set from the else branch above + let lChild = position.leftChild(in: context) + let rChild = position.rightChild(in: context) + if lChild.isFile, rChild.isFile { // ignore this case } else { - if alignConfig.recursive { - alignItem( - leftRoot.child(at: lIndex), - rightRoot: rightRoot.child(at: rIndex), - alignConfig: alignConfig - ) + if context.config.recursive { + alignItem(AlignContext( + leftRoot: lChild, + rightRoot: rChild, + config: context.config + )) } } - leftRoot.child(at: lIndex).linkedItem = rightRoot.child(at: rIndex) - rightRoot.child(at: rIndex).linkedItem = leftRoot.child(at: lIndex) + lChild.linkedItem = rChild + rChild.linkedItem = lChild - lIndex += 1 - rIndex += 1 + position.leftIndex += 1 + position.rightIndex += 1 } else if pos == .orderedAscending { // insert left orphan insert( - orphan: leftRoot.child(at: lIndex), - otherSide: rightRoot, - alignConfig: alignConfig, - leftIndex: &lIndex, - rightIndex: &rIndex + orphan: position.leftChild(in: context), + otherSide: context.rightRoot, + context: context, + position: &position ) } else if pos == .orderedDescending { // insert right orphan insert( - orphan: rightRoot.child(at: rIndex), - otherSide: leftRoot, - alignConfig: alignConfig, - leftIndex: &lIndex, - rightIndex: &rIndex + orphan: position.rightChild(in: context), + otherSide: context.leftRoot, + context: context, + position: &position ) } else { Logger.general.error("Invalid pos value \(pos.rawValue)") } - leftChildrenCount = leftRoot.children.count - rightChildrenCount = rightRoot.children.count + leftChildrenCount = context.leftRoot.children.count + rightChildrenCount = context.rightRoot.children.count } } func insert( orphan: CompareItem, otherSide: CompareItem, - alignConfig: AlignConfig, - leftIndex: inout Int, - rightIndex: inout Int + context: AlignContext, + position: inout AlignPosition ) { let newItem = CompareItem( path: nil, @@ -138,146 +151,119 @@ public extension ItemComparator { orphan.linkedItem = newItem newItem.linkedItem = orphan newItem.linkedItemIsFolder(orphan.isFolder) - otherSide.insert(child: newItem, at: leftIndex) + otherSide.insert(child: newItem, at: position.leftIndex) - if alignConfig.recursive { - alignItem( - orphan, - rightRoot: otherSide.child(at: leftIndex), - alignConfig: alignConfig - ) + if context.config.recursive { + alignItem(AlignContext( + leftRoot: orphan, + rightRoot: otherSide.child(at: position.leftIndex), + config: context.config + )) } - leftIndex += 1 - rightIndex += 1 + position.leftIndex += 1 + position.rightIndex += 1 } func align( - _ leftRoot: CompareItem, - rightRoot: CompareItem, - alignConfig: AlignConfig, - leftIndex: inout Int, - rightIndex: inout Int + _ context: AlignContext, + position: inout AlignPosition ) -> ComparisonResult { if let fileNameAlignments, !fileNameAlignments.isEmpty { return alignByRegularExpression( - leftRoot, - rightRoot: rightRoot, - alignConfig: alignConfig, - leftIndex: &leftIndex, - rightIndex: &rightIndex + context, + position: &position ) } return alignByFileName( - leftRoot, - rightRoot: rightRoot, - alignConfig: alignConfig, - leftIndex: &leftIndex, - rightIndex: &rightIndex + context, + position: &position ) } // MARK: - Filenames alignment func alignByFileName( - _ leftRoot: CompareItem, - rightRoot: CompareItem, - alignConfig: AlignConfig, - leftIndex: inout Int, - rightIndex: inout Int + _ context: AlignContext, + position: inout AlignPosition ) -> ComparisonResult { var pos: ComparisonResult = .orderedSame - var l = leftIndex - var r = rightIndex - let rightChildren = rightRoot.children - let followSymLinks = alignConfig.followSymLinks + let rightChildren = context.rightRoot.children + let followSymLinks = context.config.followSymLinks if isLeftCaseSensitive, isRightCaseSensitive { - pos = leftRoot.child(at: l).compare( - forAlign: rightRoot.child(at: r), + pos = position.leftChild(in: context).compare( + forAlign: position.rightChild(in: context), followSymLinks: followSymLinks, insensitiveCompare: false ) } else if !isLeftCaseSensitive, !isRightCaseSensitive { - pos = leftRoot.child(at: l).compare( - forAlign: rightRoot.child(at: r), + pos = position.leftChild(in: context).compare( + forAlign: position.rightChild(in: context), followSymLinks: followSymLinks, insensitiveCompare: true ) } else { - let leftChild = leftRoot.child(at: l) + let leftChild = position.leftChild(in: context) let index = findInsertIndex( left: leftChild, - right: rightRoot, - startIndex: r, + right: context.rightRoot, + startIndex: position.rightIndex, followSymLinks: followSymLinks ) // left name doesn't exist on right so determine the insertion point using a match case if index == -1 { pos = leftChild.compare( - forAlign: rightRoot.child(at: r), + forAlign: position.rightChild(in: context), followSymLinks: followSymLinks, insensitiveCompare: false ) } else { - while r < index { + while position.rightIndex < index { insert( - orphan: rightRoot.child(at: r), - otherSide: leftRoot, - alignConfig: alignConfig, - leftIndex: &l, - rightIndex: &r + orphan: position.rightChild(in: context), + otherSide: context.leftRoot, + context: context, + position: &position ) } - let leftFileName = leftRoot.child(at: l).fileName ?? "" - let (isExactlyMatch, rightIndex) = findExactlyMatchIndex( + let leftFileName = position.leftChild(in: context).fileName ?? "" + let (isExactlyMatch, matchIndex) = findExactlyMatchIndex( leftFileName, subfolders: rightChildren, followSymLinks: followSymLinks, - startIndex: r + startIndex: position.rightIndex ) if isExactlyMatch { pos = insertOrphans( - atExactIndex: rightIndex, - leftRoot: leftRoot, - rightRoot: rightRoot, - alignConfig: alignConfig, - leftIndex: &l, - rightIndex: &r + atExactIndex: matchIndex, + context: context, + position: &position ) } else { pos = insertOrphans( - atClosestIndex: rightIndex, - leftRoot: leftRoot, - rightRoot: rightRoot, - alignConfig: alignConfig, - leftIndex: &l, - rightIndex: &r + atClosestIndex: matchIndex, + context: context, + position: &position ) } } } - leftIndex = l - rightIndex = r return pos } func insertOrphans( atExactIndex exactMatchRightIndex: Int, - leftRoot: CompareItem, - rightRoot: CompareItem, - alignConfig: AlignConfig, - leftIndex l: inout Int, - rightIndex r: inout Int + context: AlignContext, + position: inout AlignPosition ) -> ComparisonResult { - while r < exactMatchRightIndex { + while position.rightIndex < exactMatchRightIndex { insert( - orphan: rightRoot.child(at: r), - otherSide: leftRoot, - alignConfig: alignConfig, - leftIndex: &l, - rightIndex: &r + orphan: position.rightChild(in: context), + otherSide: context.leftRoot, + context: context, + position: &position ) } return .orderedSame @@ -285,30 +271,27 @@ public extension ItemComparator { func insertOrphans( atClosestIndex closestRightIndex: Int, - leftRoot: CompareItem, - rightRoot: CompareItem, - alignConfig: AlignConfig, - leftIndex l: inout Int, - rightIndex r: inout Int + context: AlignContext, + position: inout AlignPosition ) -> ComparisonResult { - let leftChildren = leftRoot.children + let leftChildren = context.leftRoot.children + let l = position.leftIndex let hasLeftMoreSameNames = (l + 1) < leftChildren.count && leftChildren[l].compare( forAlign: leftChildren[l + 1], - followSymLinks: alignConfig.followSymLinks, + followSymLinks: context.config.followSymLinks, insensitiveCompare: true ) == .orderedSame if hasLeftMoreSameNames { return .orderedAscending } - while r < closestRightIndex { + while position.rightIndex < closestRightIndex { insert( - orphan: rightRoot.child(at: r), - otherSide: leftRoot, - alignConfig: alignConfig, - leftIndex: &l, - rightIndex: &r + orphan: position.rightChild(in: context), + otherSide: context.leftRoot, + context: context, + position: &position ) } return .orderedSame @@ -390,5 +373,3 @@ func enumerateWithSameFileName( insensitiveCompare: true ) == .orderedSame } - -// swiftlint:enable function_parameter_count diff --git a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift index 82345e8..f08cbe9 100644 --- a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift +++ b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+AlignRegularExpression.swift @@ -8,63 +8,44 @@ extension ItemComparator { func alignByRegularExpression( - _ leftRoot: CompareItem, - rightRoot: CompareItem, - alignConfig: AlignConfig, - leftIndex: inout Int, - rightIndex: inout Int + _ context: AlignContext, + position: inout AlignPosition ) -> ComparisonResult { - let leftChild = leftRoot.child(at: leftIndex) - let rightChild = rightRoot.child(at: rightIndex) + let leftChild = position.leftChild(in: context) + let rightChild = position.rightChild(in: context) - var lIndex = leftIndex - var rIndex = rightIndex - - let result = leftChild.compare( + return leftChild.compare( rightChild, - followSymLinks: alignConfig.followSymLinks - ) { self.compareByRegularExpression( - lhs: $0, - rhs: $1, - leftRoot: leftRoot, - rightRoot: rightRoot, - alignConfig: alignConfig, - leftIndex: &lIndex, - rightIndex: &rIndex - ) + followSymLinks: context.config.followSymLinks + ) { + self.compareByRegularExpression( + lhs: $0, + rhs: $1, + context: context, + position: &position + ) } - - leftIndex = lIndex - rightIndex = rIndex - - return result } - // swiftlint:disable:next function_parameter_count private func compareByRegularExpression( lhs: CompareItem, rhs: CompareItem, - leftRoot: CompareItem, - rightRoot: CompareItem, - alignConfig: AlignConfig, - leftIndex: inout Int, - rightIndex: inout Int + context: AlignContext, + position: inout AlignPosition ) -> ComparisonResult { guard let lhsName = lhs.fileName, let rhsName = rhs.fileName else { return .orderedSame } + if let fileNameAlignments { for rule in fileNameAlignments where rule.matches(name: lhsName, with: rhsName) { return .orderedSame } } return alignByFileName( - leftRoot, - rightRoot: rightRoot, - alignConfig: alignConfig, - leftIndex: &leftIndex, - rightIndex: &rightIndex + context, + position: &position ) } } diff --git a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift index 171dd00..a8ae74e 100644 --- a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift +++ b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator+Compare.swift @@ -68,8 +68,8 @@ extension ItemComparator { } private func compareFinderLabel(_ lhs: CompareItem, _ rhs: CompareItem) -> ComparisonResult { - guard let lhsPath = lhs.toUrl(), - let rhsPath = rhs.toUrl(), + guard let lhsPath = lhs.toURL(), + let rhsPath = rhs.toURL(), let leftLabelNumber = lhsPath.labelNumber(), let rightLabelNumber = rhsPath.labelNumber() else { return .orderedSame @@ -91,8 +91,8 @@ extension ItemComparator { } private func compareFinderTag(_ lhs: CompareItem, _ rhs: CompareItem) -> ComparisonResult { - guard let lhsPath = lhs.toUrl(), - let rhsPath = rhs.toUrl(), + guard let lhsPath = lhs.toURL(), + let rhsPath = rhs.toURL(), let leftTags = lhsPath.tagNames(sorted: true), let rightTags = rhsPath.tagNames(sorted: true) else { return .orderedSame @@ -169,8 +169,8 @@ extension ItemComparator { return lhs.fileSize < rhs.fileSize ? .orderedAscending : .orderedDescending } } - guard let lhsPath = lhs.toUrl(), - let rhsPath = rhs.toUrl() else { + guard let lhsPath = lhs.toURL(), + let rhsPath = rhs.toURL() else { return .orderedSame } @@ -226,8 +226,8 @@ extension ItemComparator { } private func compareAsText(_ lhs: CompareItem, _ rhs: CompareItem) -> ComparisonResult { - guard let lhsPath = lhs.toUrl(), - let rhsPath = rhs.toUrl() else { + guard let lhsPath = lhs.toURL(), + let rhsPath = rhs.toURL() else { return .orderedSame } diff --git a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator.swift b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator.swift index 1c57ea7..b89e948 100644 --- a/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator.swift +++ b/Sources/Features/FoldersCompare/Services/ItemComparator/ItemComparator.swift @@ -10,7 +10,6 @@ public protocol ItemComparatorDelegate: AnyObject { func isRunning(_ comparator: ItemComparator) -> Bool } -@objc public class ItemComparator: NSObject { let options: ComparatorOptions weak var delegate: ItemComparatorDelegate? diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift index 325144d..c5514e6 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift @@ -12,6 +12,7 @@ extension FolderSelectionInfo { guard let linkedSelInfo = view.linkedView?.selectionInfo else { return false } + if selType == .file, linkedSelInfo.selType.isEmpty { return filesCount == 2 } @@ -25,6 +26,7 @@ extension FolderSelectionInfo { guard let linkedSelInfo = view.linkedView?.selectionInfo else { return false } + if selType == .folder, linkedSelInfo.selType.isEmpty { return foldersCount == 2 } diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift index 41cce07..b1e2e59 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift @@ -39,9 +39,10 @@ extension FolderSelectionInfo { let vi = view.item(atRow: row) as? VisibleItem else { return false } + let item = vi.item - guard let path = item.toUrl() else { + guard let path = item.toURL() else { return false } @@ -51,7 +52,7 @@ extension FolderSelectionInfo { for row in indexes.dropFirst() where allFilesWithSameExt { if let viAtRow = view.item(atRow: row) as? VisibleItem { let itemAtRow = viAtRow.item - if let path = itemAtRow.toUrl() { + if let path = itemAtRow.toURL() { let tempExt = path.pathExtension allFilesWithSameExt = tempExt == fileExt } diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift index 9590a51..a8f25a2 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift @@ -16,6 +16,7 @@ extension FolderSelectionInfo { guard let itemRow = view.item(atRow: view.selectedRow) as? VisibleItem else { return false } + let item = itemRow.item if let path = item.path, outSelectedPath != nil { diff --git a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift index d6a5849..e1e26d9 100644 --- a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift +++ b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift @@ -20,10 +20,11 @@ extension VisibleItem { guard let fileName else { return } + if regex.firstMatch( in: fileName, options: [], - range: NSRange(location: 0, length: fileName.count) + range: NSRange(location: 0, length: fileName.utf16.count) ) != nil { items.append(self) } diff --git a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+QLPreviewItem.swift b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+QLPreviewItem.swift index 40193ad..408e371 100644 --- a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+QLPreviewItem.swift +++ b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+QLPreviewItem.swift @@ -11,6 +11,6 @@ import Quartz extension VisibleItem: QLPreviewItem { // swiftlint:disable:next implicitly_unwrapped_optional public var previewItemURL: URL! { - item.toUrl() + item.toURL() } } diff --git a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Sort.swift b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Sort.swift index db0c542..9457da8 100644 --- a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Sort.swift +++ b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Sort.swift @@ -10,7 +10,6 @@ func compare(_ lhs: T, _ rhs: T) -> ComparisonResult { lhs == rhs ? .orderedSame : lhs < rhs ? .orderedAscending : .orderedDescending } -@objc extension VisibleItem { func sort( byFileName ascending: Bool, @@ -33,6 +32,7 @@ extension VisibleItem { guard let date2 = fs2.fileModificationDate else { return .orderedAscending } + return date1.compare(date2) } } diff --git a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift index 1e43c1b..0c2f01d 100644 --- a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift +++ b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift @@ -52,6 +52,7 @@ public class VisibleItem: NSObject { guard let linkedItem else { return } + let tempItem = item item = linkedItem.item linkedItem.item = tempItem @@ -100,11 +101,7 @@ public class VisibleItem: NSObject { var result = comparator(fs1, fs2) if result == .orderedSame { - // swiftlint:disable force_unwrapping - result = ignoreCase - ? fs1.fileName!.localizedCaseInsensitiveCompare(fs2.fileName!) - : fs1.fileName!.localizedCompare(fs2.fileName!) - // swiftlint:enable force_unwrapping + result = compareFileName(lhs: fs1, rhs: fs2, ignoreCase: ignoreCase) } if !ascending, result != .orderedSame { result = result == .orderedAscending ? .orderedDescending : .orderedAscending @@ -118,7 +115,25 @@ public class VisibleItem: NSObject { guard let li = vi.linkedItem else { fatalError("LinkedItem must be set for all visible items") } + linkedItem?.children[index] = li } } + + private func compareFileName( + lhs: CompareItem, + rhs: CompareItem, + ignoreCase: Bool + ) -> ComparisonResult { + guard let lhsFileName = lhs.fileName else { + return .orderedDescending + } + guard let rhsFileName = rhs.fileName else { + return .orderedAscending + } + + return ignoreCase + ? lhsFileName.localizedCaseInsensitiveCompare(rhsFileName) + : lhsFileName.localizedCompare(rhsFileName) + } } diff --git a/Sources/Features/FoldersCompare/Windows/FolderCompareInfoWindow/FolderCompareInfoWindow.swift b/Sources/Features/FoldersCompare/Windows/FolderCompareInfoWindow/FolderCompareInfoWindow.swift index 4cffc48..ace0255 100644 --- a/Sources/Features/FoldersCompare/Windows/FolderCompareInfoWindow/FolderCompareInfoWindow.swift +++ b/Sources/Features/FoldersCompare/Windows/FolderCompareInfoWindow/FolderCompareInfoWindow.swift @@ -114,6 +114,7 @@ class FolderCompareInfoWindow: NSWindow, NSOutlineViewDataSource, NSOutlineViewD guard let contentView else { return } + NSLayoutConstraint.activate([ title.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 19), title.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -19), @@ -201,6 +202,7 @@ class FolderCompareInfoWindow: NSWindow, NSOutlineViewDataSource, NSOutlineViewD guard let range = path.range(of: root) else { return nil } + // skip path separator let startIndex = path.index(after: range.upperBound) return String(path[startIndex ..< path.endIndex]) @@ -219,6 +221,7 @@ class FolderCompareInfoWindow: NSWindow, NSOutlineViewDataSource, NSOutlineViewD guard let children = node?.children else { return 0 } + return children.count } @@ -238,6 +241,7 @@ class FolderCompareInfoWindow: NSWindow, NSOutlineViewDataSource, NSOutlineViewD guard let children = node?.children else { fatalError("Children cannot be nil") } + return children[index] } diff --git a/Sources/Features/HistoryController/HistoryController.swift b/Sources/Features/HistoryController/HistoryController.swift index 0b074f4..a849409 100644 --- a/Sources/Features/HistoryController/HistoryController.swift +++ b/Sources/Features/HistoryController/HistoryController.swift @@ -112,6 +112,7 @@ class HistoryController: NSObject, NSTableViewDelegate, NSTableViewDataSource, T let fetchedObjects = results.fetchedObjects else { return } + var entities = [HistoryEntity]() for idx in tableView.selectedRowIndexes { @@ -125,6 +126,7 @@ class HistoryController: NSObject, NSTableViewDelegate, NSTableViewDataSource, T guard let fetchedObjects = results.fetchedObjects else { return 0 } + return fetchedObjects.count } @@ -133,6 +135,7 @@ class HistoryController: NSObject, NSTableViewDelegate, NSTableViewDataSource, T let entity = results.fetchedObjects?[row] else { return nil } + let cell = tableView.makeView(withIdentifier: identifier, owner: nil) as? HistoryEntityTableCellView ?? HistoryEntityTableCellView(identifier: identifier) @@ -215,6 +218,7 @@ class HistoryController: NSObject, NSTableViewDelegate, NSTableViewDataSource, T guard let fetchedObjects = results.fetchedObjects else { return } + for row in indexes.reversed() { HistorySessionManager.shared.historyMOC.delete(fetchedObjects[row]) } @@ -240,6 +244,7 @@ class HistoryController: NSObject, NSTableViewDelegate, NSTableViewDataSource, T guard let fetchedObjects = results.fetchedObjects else { return } + let fm = FileManager.default var indexSet = IndexSet() diff --git a/Sources/Features/HistoryController/HistoryEntityTableCellView.swift b/Sources/Features/HistoryController/HistoryEntityTableCellView.swift index bdc0a79..ac6d5a0 100644 --- a/Sources/Features/HistoryController/HistoryEntityTableCellView.swift +++ b/Sources/Features/HistoryController/HistoryEntityTableCellView.swift @@ -79,6 +79,7 @@ class HistoryEntityTableCellView: NSTableCellView { guard let leftText = leftPath.textField else { fatalError("Can't happen") } + NSLayoutConstraint.activate([ leftPath.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4), leftPath.trailingAnchor.constraint(equalTo: trailingAnchor, constant: 4), @@ -105,6 +106,7 @@ class HistoryEntityTableCellView: NSTableCellView { timeDescription.stringValue = "" return } + leftPath.update(path: entityLeftPath) rightPath.update(path: entityRightPath) diff --git a/Sources/Features/HistoryController/HistoryFetchedResultsControllerDelegate.swift b/Sources/Features/HistoryController/HistoryFetchedResultsControllerDelegate.swift index 2c9fc1b..ae3ff27 100644 --- a/Sources/Features/HistoryController/HistoryFetchedResultsControllerDelegate.swift +++ b/Sources/Features/HistoryController/HistoryFetchedResultsControllerDelegate.swift @@ -10,14 +10,12 @@ import os.log typealias IndexPathPair = (IndexPath, IndexPath?) -@objc class HistoryFetchedResultsControllerDelegate: NSObject, @preconcurrency NSFetchedResultsControllerDelegate { private(set) var tableView: NSTableView - @objc var pattern: String? + var pattern: String? private var objectChanges = [NSFetchedResultsChangeType: [IndexPathPair]]() - @objc init(tableView: NSTableView) { self.tableView = tableView @@ -96,6 +94,7 @@ class HistoryFetchedResultsControllerDelegate: NSObject, @preconcurrency NSFetch !array.isEmpty else { return } + var indexes = IndexSet() for (from, _) in array { indexes.insert(from.item) @@ -115,6 +114,7 @@ class HistoryFetchedResultsControllerDelegate: NSObject, @preconcurrency NSFetch !array.isEmpty else { return } + for (from, _) in array { let row = from.item let cell = tableView.view(atColumn: 0, row: row, makeIfNecessary: true) @@ -136,6 +136,7 @@ class HistoryFetchedResultsControllerDelegate: NSObject, @preconcurrency NSFetch !array.isEmpty else { return } + var indexes = IndexSet() for (from, _) in array { indexes.insert(from.item) @@ -155,6 +156,7 @@ class HistoryFetchedResultsControllerDelegate: NSObject, @preconcurrency NSFetch !array.isEmpty else { return } + for (fromIndex, toIndex) in array { if let toIndex { tableView.removeRows( diff --git a/Sources/Features/HistoryController/HistorySearchField.swift b/Sources/Features/HistoryController/HistorySearchField.swift index 91316b5..6ca6999 100644 --- a/Sources/Features/HistoryController/HistorySearchField.swift +++ b/Sources/Features/HistoryController/HistorySearchField.swift @@ -8,9 +8,8 @@ import Foundation -@objc class HistorySearchField: NSSearchField, NSSearchFieldDelegate { - @objc var historyController: HistoryController? + var historyController: HistoryController? override init(frame frameRect: NSRect) { super.init(frame: frameRect) @@ -46,6 +45,7 @@ class HistorySearchField: NSSearchField, NSSearchFieldDelegate { guard let historyController else { return false } + let row = historyController.tableView.selectedRow if commandSelector == #selector(moveUp) { diff --git a/Sources/Features/Preferences/Box/PreferencesBox.swift b/Sources/Features/Preferences/Box/PreferencesBox.swift index e6e7676..7aad6b3 100644 --- a/Sources/Features/Preferences/Box/PreferencesBox.swift +++ b/Sources/Features/Preferences/Box/PreferencesBox.swift @@ -66,6 +66,7 @@ class PreferencesBox: NSBox { guard let delegate else { return } + for checkbox in checkboxes.values { let value = delegate.preferenceBox(self, boolForKey: checkbox.prefName) checkbox.state = value ? .on : .off diff --git a/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift b/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift index 340c282..75f0754 100644 --- a/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift +++ b/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift @@ -108,6 +108,7 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, guard let contentView = prefPanel.contentView else { return } + NSLayoutConstraint.activate([ tabView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), tabView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), diff --git a/Sources/Features/Preferences/Main/Panels/Font/FontBox.swift b/Sources/Features/Preferences/Main/Panels/Font/FontBox.swift index 31b6972..3c2169f 100644 --- a/Sources/Features/Preferences/Main/Panels/Font/FontBox.swift +++ b/Sources/Features/Preferences/Main/Panels/Font/FontBox.swift @@ -57,6 +57,7 @@ class FontBox: PreferencesBox { guard let contentView else { return } + NSLayoutConstraint.activate([ selectFont.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), selectFont.topAnchor.constraint(equalTo: contentView.topAnchor), diff --git a/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift index 7c2d91d..8abbb85 100644 --- a/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift @@ -89,6 +89,7 @@ class FontPreferencesPanel: NSView, NSFontChanging, PreferencesPanelDataSource { let selectedFont = sender.selectedFont else { return } + let newFont = sender.convert(selectedFont) if selectedFont == fileFontBox.previewFont { diff --git a/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift b/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift index 09a4312..1281c1c 100644 --- a/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift +++ b/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift @@ -43,6 +43,7 @@ class AppearanceBox: PreferencesBox { guard let contentView else { return } + NSLayoutConstraint.activate([ appearancePopup.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5), appearancePopup.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), @@ -62,6 +63,7 @@ class AppearanceBox: PreferencesBox { guard let tag = appearancePopup.selectedItem?.tag else { return } + switch tag { case 0: if UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" { diff --git a/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift b/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift index 3c26b21..3a4b1fe 100644 --- a/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift +++ b/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift @@ -118,6 +118,7 @@ class FolderComparisonBox: PreferencesBox { guard let comparatorFlags = comparisonPopup.selectedItem?.tag else { return } + delegate?.preferenceBox( self, setInteger: comparatorFlags, @@ -131,6 +132,7 @@ class FolderComparisonBox: PreferencesBox { guard let tag = displayFiltersPopup.selectedItem?.tag else { return } + delegate?.preferenceBox( self, setInteger: tag, diff --git a/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardDocumentBox.swift b/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardDocumentBox.swift index f4f66ee..a96cb92 100644 --- a/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardDocumentBox.swift +++ b/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardDocumentBox.swift @@ -33,6 +33,7 @@ class KeyboardDocumentBox: PreferencesBox { guard let contentView else { return } + NSLayoutConstraint.activate([ stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), diff --git a/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift index cb34b51..aa530b1 100644 --- a/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift @@ -266,6 +266,7 @@ class TrustedPathsPreferencesPanel: NSView, NSTableViewDataSource, NSTableViewDe guard let arr = SecureBookmark.shared.securedPaths?.keys else { return } + trustedPaths = arr.map(\.self) trustedPaths.sort { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } @@ -286,6 +287,7 @@ class TrustedPathsPreferencesPanel: NSView, NSTableViewDataSource, NSTableViewDe guard let identifier = tableColumn?.identifier else { return nil } + let cell = tableView.makeView( withIdentifier: identifier, owner: self diff --git a/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift b/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift index 6229df2..06ee33e 100644 --- a/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift +++ b/Sources/Features/Preferences/Session/Controllers/SessionPreferencesWindow.swift @@ -97,6 +97,7 @@ class SessionPreferencesWindow: NSWindowController, NSTabViewDelegate, @preconcu guard let contentView = window?.contentView else { return } + NSLayoutConstraint.activate([ tabView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), tabView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -10), @@ -121,6 +122,7 @@ class SessionPreferencesWindow: NSWindowController, NSTabViewDelegate, @preconcu guard let window else { return } + if let sessionDiff { fillWithSessionDiff(sessionDiff) } @@ -149,6 +151,7 @@ class SessionPreferencesWindow: NSWindowController, NSTabViewDelegate, @preconcu guard let window else { return } + let response = NSApplication.ModalResponse(sender.tag) if response == .OK { updatePendingData() @@ -345,6 +348,7 @@ class SessionPreferencesWindow: NSWindowController, NSTabViewDelegate, @preconcu guard let window else { return 0 } + let windowFrame = NSWindow.contentRect(forFrameRect: window.frame, styleMask: window.styleMask) return windowFrame.size.height - (window.contentView?.frame.size.height ?? 0) } @@ -360,6 +364,7 @@ class SessionPreferencesWindow: NSWindowController, NSTabViewDelegate, @preconcu window.isVisible else { return } + let newSize = NSSize( width: window.contentView?.frame.size.width ?? 0, height: minWindowHeight() diff --git a/Sources/Features/Preferences/Session/Panels/AlignRule/AlignRuleWindow.swift b/Sources/Features/Preferences/Session/Panels/AlignRule/AlignRuleWindow.swift index 78627ce..56857f3 100644 --- a/Sources/Features/Preferences/Session/Panels/AlignRule/AlignRuleWindow.swift +++ b/Sources/Features/Preferences/Session/Panels/AlignRule/AlignRuleWindow.swift @@ -82,6 +82,7 @@ class AlignRuleWindow: NSWindow, NSTextFieldDelegate { guard let contentView else { return } + NSLayoutConstraint.activate([ leftExpressionBox.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), leftExpressionBox.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), @@ -190,6 +191,7 @@ class AlignRuleWindow: NSWindow, NSTextFieldDelegate { guard let sender = sender as? NSButton else { return } + editedRule = nil if sender === standardButtons.primaryButton { do { @@ -271,10 +273,11 @@ extension AlignRuleWindow { var errorDescription: String? { switch self { case let .emptyExpression(isLeft): - isLeft - // swiftlint:disable:next void_function_in_ternary - ? NSLocalizedString("Left expression can't be empty", comment: "") - : NSLocalizedString("Right expression can't be empty", comment: "") + if isLeft { + NSLocalizedString("Left expression can't be empty", comment: "") + } else { + NSLocalizedString("Right expression can't be empty", comment: "") + } case .ruleAlreadyExists: NSLocalizedString("An identical rule already exists", comment: "") case let .invalidRegularExpression(error): diff --git a/Sources/Features/Preferences/Session/Panels/AlignRule/AlignTestResultBox.swift b/Sources/Features/Preferences/Session/Panels/AlignRule/AlignTestResultBox.swift index 5b770ba..f7b61c9 100644 --- a/Sources/Features/Preferences/Session/Panels/AlignRule/AlignTestResultBox.swift +++ b/Sources/Features/Preferences/Session/Panels/AlignRule/AlignTestResultBox.swift @@ -13,11 +13,10 @@ class AlignTestResultBox: NSBox, NSTextFieldDelegate { private let output: NSTextField private let errorMessage: NSTextField - @objc var leftExpression: String - @objc var rightExpression: String - @objc var regularExpressionOptions: NSRegularExpression.Options + var leftExpression: String + var rightExpression: String + var regularExpressionOptions: NSRegularExpression.Options - @objc convenience init(title: String) { self.init(frame: .zero) @@ -109,7 +108,6 @@ class AlignTestResultBox: NSBox, NSTextFieldDelegate { reloadData() } - @objc func reloadData() { output.stringValue = "" errorMessage.stringValue = "" @@ -126,7 +124,7 @@ class AlignTestResultBox: NSBox, NSTextFieldDelegate { let result = re.firstMatch( in: fileName.stringValue, options: [], - range: NSRange(location: 0, length: fileName.stringValue.count) + range: NSRange(location: 0, length: fileName.stringValue.utf16.count) ) if let result { let rightExpr = rightExpression @@ -141,7 +139,6 @@ class AlignTestResultBox: NSBox, NSTextFieldDelegate { } } - @objc func clear() { fileName.stringValue = "" output.stringValue = "" diff --git a/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox+Menu.swift b/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox+Menu.swift index 5964170..02f736b 100644 --- a/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox+Menu.swift +++ b/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox+Menu.swift @@ -7,7 +7,7 @@ // extension ExpressionBox { - @objc static var defaultRightExpressionMenu: NSMenu { + static var defaultRightExpressionMenu: NSMenu { let menu = NSMenu() // the button title image @@ -44,7 +44,7 @@ extension ExpressionBox { return menu } - @objc static var defaultRegExpMenu: NSMenu { + static var defaultRegExpMenu: NSMenu { let menu = NSMenu() // the button title image diff --git a/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox.swift b/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox.swift index 418e5c3..7cb72f2 100644 --- a/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox.swift +++ b/Sources/Features/Preferences/Session/Panels/AlignRule/ExpressionBox.swift @@ -80,6 +80,7 @@ class ExpressionBox: NSBox { guard let contentView else { return } + NSLayoutConstraint.activate([ expression.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), expression.trailingAnchor.constraint(equalTo: popup.leadingAnchor, constant: -4), @@ -104,6 +105,7 @@ class ExpressionBox: NSBox { let editor = expression.currentEditor() else { return } + let ellipsis = "..." let pattern = expression.stringValue diff --git a/Sources/Features/Preferences/Session/Panels/AlignRule/FileNameCaseBox.swift b/Sources/Features/Preferences/Session/Panels/AlignRule/FileNameCaseBox.swift index 3ff896d..d69fa3d 100644 --- a/Sources/Features/Preferences/Session/Panels/AlignRule/FileNameCaseBox.swift +++ b/Sources/Features/Preferences/Session/Panels/AlignRule/FileNameCaseBox.swift @@ -35,6 +35,7 @@ class FileNameCaseBox: PreferencesBox { guard let contentView else { return } + NSLayoutConstraint.activate([ alignPopup.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 4), alignPopup.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -4), @@ -48,6 +49,7 @@ class FileNameCaseBox: PreferencesBox { guard let item = alignPopup.selectedItem else { return } + let alignFlags = item.tag delegate?.preferenceBox(self, setInteger: alignFlags, forKey: .virtualAlignFlags) } diff --git a/Sources/Features/Preferences/Session/Panels/AlignRule/UserDefinedRulesBox.swift b/Sources/Features/Preferences/Session/Panels/AlignRule/UserDefinedRulesBox.swift index 2654d2a..b738650 100644 --- a/Sources/Features/Preferences/Session/Panels/AlignRule/UserDefinedRulesBox.swift +++ b/Sources/Features/Preferences/Session/Panels/AlignRule/UserDefinedRulesBox.swift @@ -145,6 +145,7 @@ class UserDefinedRulesBox: PreferencesBox, guard let contentView else { return } + NSLayoutConstraint.activate([ tableScrollView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), tableScrollView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), @@ -218,6 +219,7 @@ class UserDefinedRulesBox: PreferencesBox, guard let window else { return } + editedIndex = index alignRuleWindow.alignRules = alignRules alignRuleWindow.beginSheet( diff --git a/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersBox.swift b/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersBox.swift index f14113c..1e74391 100644 --- a/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersBox.swift +++ b/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersBox.swift @@ -121,6 +121,7 @@ class SessionPreferencesFiltersBox: PreferencesBox, NSMenuItemValidation { guard let contentView else { return } + NSLayoutConstraint.activate([ actionMenu.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), actionMenu.topAnchor.constraint(equalTo: contentView.topAnchor), diff --git a/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersPanel.swift b/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersPanel.swift index f69e694..2194db1 100644 --- a/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersPanel.swift +++ b/Sources/Features/Preferences/Session/Panels/Filters/SessionPreferencesFiltersPanel.swift @@ -49,7 +49,6 @@ class SessionPreferencesFiltersPanel: NSView, PreferencesPanelDataSource { filterBox.reloadData() } - @objc func updatePendingData() { filterBox.updatePendingData() } diff --git a/Sources/Services/AppleScript/OpenDiffCommand.swift b/Sources/Services/AppleScript/OpenDiffCommand.swift index dc39de8..6208ed8 100644 --- a/Sources/Services/AppleScript/OpenDiffCommand.swift +++ b/Sources/Services/AppleScript/OpenDiffCommand.swift @@ -21,15 +21,15 @@ class OpenDiffCommand: NSScriptCommand { return nil } - let leftUrl = URL(filePath: leftPath) - let rightUrl = URL(filePath: rightPath) + let leftURL = URL(filePath: leftPath) + let rightURL = URL(filePath: rightPath) var isDir = false var leftExists = false var rightExists = false - let matches = leftUrl.matchesFileType( - of: rightUrl, + let matches = leftURL.matchesFileType( + of: rightURL, isDir: &isDir, leftExists: &leftExists, rightExists: &rightExists @@ -37,8 +37,8 @@ class OpenDiffCommand: NSScriptCommand { if matches { return openDocument( - leftUrl: leftUrl, - rightUrl: rightUrl + leftURL: leftURL, + rightURL: rightURL ) } else { let message = SessionTypeError.invalidPathMessage( @@ -52,14 +52,14 @@ class OpenDiffCommand: NSScriptCommand { } func openDocument( - leftUrl: URL, - rightUrl: URL + leftURL: URL, + rightURL: URL ) -> String? { do { return try MainActor.assumeIsolated { try VDDocumentController.shared.openDifferDocument( - leftUrl: leftUrl, - rightUrl: rightUrl + leftURL: leftURL, + rightURL: rightURL )?.uuid } } catch { diff --git a/Sources/SharedKit/Extensions/Appearance/NSAppearance+DarkMode.swift b/Sources/SharedKit/Extensions/Appearance/NSAppearance+DarkMode.swift index d1ce0fd..3252b8e 100644 --- a/Sources/SharedKit/Extensions/Appearance/NSAppearance+DarkMode.swift +++ b/Sources/SharedKit/Extensions/Appearance/NSAppearance+DarkMode.swift @@ -21,7 +21,6 @@ extension NSAppearance { return UserDefaults.standard.string(forKey: "AppleInterfaceStyle") == "Dark" } - @objc @MainActor static func change() { if #available(macOS 10.14, *) { diff --git a/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift b/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift index 0697b8f..9c9e518 100644 --- a/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift +++ b/Sources/SharedKit/Extensions/Color/NSColor+Hex.swift @@ -53,6 +53,7 @@ extension NSColor { guard scanner.scanHexInt64(&hex) else { return false } + switch hexDigitsCount { case 3: red = UInt((hex >> 8) & 15) diff --git a/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift b/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift index 3a94498..0efb1bb 100644 --- a/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift +++ b/Sources/SharedKit/Extensions/Image/NSImage+Tint.swift @@ -42,8 +42,7 @@ public extension NSImage { return tintedImage } - // swiftlint:disable:next force_cast - return copy() as! NSImage + return copy() as? NSImage ?? self } /** @@ -69,6 +68,7 @@ public extension NSImage { let colorFilter = CIFilter(name: "CIColorControls") else { return nil } + colorGenerator.setValue(color, forKey: kCIInputColorKey) colorFilter.setValue(colorGenerator.value(forKey: kCIOutputImageKey), forKey: kCIInputImageKey) @@ -119,6 +119,7 @@ public extension NSImage { guard let image = copy() as? NSImage else { return self } + image.lockFocus() color.set() let imageRect = NSRect(origin: .zero, size: image.size) @@ -126,4 +127,12 @@ public extension NSImage { image.unlockFocus() return image } + + static func required(named name: NSImage.Name) -> NSImage { + guard let image = NSImage(named: name) else { + preconditionFailure("missing required image '\(name)'") + } + + return image + } } diff --git a/Sources/SharedKit/Extensions/Menu/NSMenu+File.swift b/Sources/SharedKit/Extensions/Menu/NSMenu+File.swift index cb214fb..caaa1a6 100644 --- a/Sources/SharedKit/Extensions/Menu/NSMenu+File.swift +++ b/Sources/SharedKit/Extensions/Menu/NSMenu+File.swift @@ -116,36 +116,36 @@ extension NSMenu { ) -> [AppNameAttributeKey: String] { var appNames = [AppNameAttributeKey: String]() - let (defaultAppUrl, defaultAppName) = getSystemDefaultAppForFile(path) - let (preferredAppUrl, preferredAppName) = getPreferredAppForFile(path) + let (defaultAppURL, defaultAppName) = getSystemDefaultAppForFile(path) + let (preferredAppURL, preferredAppName) = getPreferredAppForFile(path) - if let defaultAppUrl, let preferredAppUrl, defaultAppUrl == preferredAppUrl { + if let defaultAppURL, let preferredAppURL, defaultAppURL == preferredAppURL { addItem( defaultAppName ?? "", description: NSLocalizedString(" (System Default)", comment: ""), descriptionColor: descriptionColor, - appPath: defaultAppUrl, + appPath: defaultAppURL, openAppAction: openAppAction ) appNames[.preferred] = preferredAppName appNames[.system] = defaultAppName } else { - if let preferredAppName, let preferredAppUrl { + if let preferredAppName, let preferredAppURL { addItem( preferredAppName, description: NSLocalizedString(" (App Default)", comment: ""), descriptionColor: descriptionColor, - appPath: preferredAppUrl, + appPath: preferredAppURL, openAppAction: openAppAction ) appNames[.preferred] = preferredAppName } - if let defaultAppName, let defaultAppUrl { + if let defaultAppName, let defaultAppURL { addItem( defaultAppName, description: NSLocalizedString(" (System Default)", comment: ""), descriptionColor: descriptionColor, - appPath: defaultAppUrl, + appPath: defaultAppURL, openAppAction: openAppAction ) appNames[.system] = defaultAppName @@ -201,40 +201,40 @@ extension NSMenu { return item } - func getSystemDefaultAppForFile(_ path: URL) -> (appUrl: URL?, appName: String?) { - guard let appUrl = NSWorkspace.shared.urlForApplication(toOpen: path) else { + func getSystemDefaultAppForFile(_ path: URL) -> (appURL: URL?, appName: String?) { + guard let appURL = NSWorkspace.shared.urlForApplication(toOpen: path) else { return (nil, nil) } - guard let values = try? appUrl.resourceValues(forKeys: [URLResourceKey.localizedNameKey]), + guard let values = try? appURL.resourceValues(forKeys: [URLResourceKey.localizedNameKey]), let appName = values.localizedName else { - return (appUrl, nil) + return (appURL, nil) } - return (appUrl, appName) + return (appURL, appName) } - func getPreferredAppForFile(_: URL) -> (appUrl: URL?, appName: String?) { + func getPreferredAppForFile(_: URL) -> (appURL: URL?, appName: String?) { guard let appPath = UserDefaults.standard.string(forKey: Self.preferredEditorPrefName) else { return (nil, nil) } - let appUrl = URL(filePath: appPath) + let appURL = URL(filePath: appPath) - guard let values = try? appUrl.resourceValues(forKeys: [URLResourceKey.localizedNameKey]), + guard let values = try? appURL.resourceValues(forKeys: [URLResourceKey.localizedNameKey]), let appName = values.localizedName else { - return (appUrl, nil) + return (appURL, nil) } - return (appUrl, appName) + return (appURL, appName) } func mapAppNameToPath( - _ appUrls: [URL], + _ appURLs: [URL], excludeAppNames: [String] ) -> [String: URL] { var dictAppNames = [String: URL]() - for url in appUrls { + for url in appURLs { if let values = try? url.resourceValues(forKeys: [.localizedNameKey]), let displayName = values.localizedName { dictAppNames[displayName] = url diff --git a/Sources/SharedKit/Extensions/String/String+RegularExpression.swift b/Sources/SharedKit/Extensions/String/String+RegularExpression.swift index 2d2f19b..be93ee1 100644 --- a/Sources/SharedKit/Extensions/String/String+RegularExpression.swift +++ b/Sources/SharedKit/Extensions/String/String+RegularExpression.swift @@ -20,13 +20,16 @@ private struct GlobConverter { ] for (regexString, templateString) in globs { - // swiftlint:disable:next force_try - let re = try! NSRegularExpression( - pattern: regexString, - options: .caseInsensitive - ) - regex.append(re) - templates.append(templateString) + do { + let re = try NSRegularExpression( + pattern: regexString, + options: .caseInsensitive + ) + regex.append(re) + templates.append(templateString) + } catch { + preconditionFailure("invalid static regex pattern '\(regexString)': \(error)") + } } } @@ -37,7 +40,7 @@ private struct GlobConverter { return re.stringByReplacingMatches( in: str, options: [], - range: NSRange(location: 0, length: str.count), + range: NSRange(location: 0, length: str.utf16.count), withTemplate: template ) } @@ -79,8 +82,11 @@ public extension String { inString.append(chars[position]) i += 1 } else { + let maxSupportedCaptureGroupIndex = 100 while i < size, let num = chars[position].wholeNumberValue { - value = value * 10 + num + if value < maxSupportedCaptureGroupIndex { + value = value * 10 + num + } i += 1 position = chars.index(chars.startIndex, offsetBy: i) } diff --git a/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift b/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift index 6b641dc..923ad93 100644 --- a/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift +++ b/Sources/SharedKit/Extensions/TableView/NSTableView+Row.swift @@ -46,6 +46,7 @@ extension NSTableView { guard let superview else { return -1 } + let bounds = superview.bounds return row(at: bounds.origin) @@ -55,6 +56,7 @@ extension NSTableView { guard let superview else { return -1 } + var bounds = superview.bounds bounds.origin.y += bounds.size.height - 1 diff --git a/Sources/SharedKit/Extensions/URL/URL+Finder.swift b/Sources/SharedKit/Extensions/URL/URL+Finder.swift index 5ff2025..7ec1bf5 100644 --- a/Sources/SharedKit/Extensions/URL/URL+Finder.swift +++ b/Sources/SharedKit/Extensions/URL/URL+Finder.swift @@ -11,6 +11,7 @@ extension URL { guard let resources = try? resourceValues(forKeys: [.labelNumberKey]) else { return nil } + return resources.labelNumber } diff --git a/Sources/SharedKit/Extensions/URL/URL+Metadata.swift b/Sources/SharedKit/Extensions/URL/URL+Metadata.swift index 40e4502..9a7a789 100644 --- a/Sources/SharedKit/Extensions/URL/URL+Metadata.swift +++ b/Sources/SharedKit/Extensions/URL/URL+Metadata.swift @@ -7,13 +7,13 @@ // public extension URL { - func copyTags(to toUrl: inout URL) throws { + func copyTags(to toURL: inout URL) throws { let resources = try resourceValues(forKeys: [.tagNamesKey]) - try toUrl.setResourceValues(resources) + try toURL.setResourceValues(resources) } - func copyLabel(to toUrl: inout URL) throws { + func copyLabel(to toURL: inout URL) throws { let resources = try resourceValues(forKeys: [.labelNumberKey]) - try toUrl.setResourceValues(resources) + try toURL.setResourceValues(resources) } } diff --git a/Sources/SharedKit/Extensions/URL/URL+Path.swift b/Sources/SharedKit/Extensions/URL/URL+Path.swift index 840a303..fb507b2 100644 --- a/Sources/SharedKit/Extensions/URL/URL+Path.swift +++ b/Sources/SharedKit/Extensions/URL/URL+Path.swift @@ -66,7 +66,7 @@ public extension URL { * Check that self is the same type as the passed item (both are files or both are folders) */ func matchesFileType( - of rightUrl: URL, + of rightURL: URL, isDir: inout Bool, leftExists: inout Bool, rightExists: inout Bool @@ -76,7 +76,7 @@ public extension URL { var isRightDir = ObjCBool(false) let leftPath = osPath - let rightPath = rightUrl.osPath + let rightPath = rightURL.osPath let isLeftEmpty = leftPath == "." let isRightEmpty = rightPath == "." @@ -107,13 +107,13 @@ public extension URL { return leftExists && rightExists && (isLeftDir.boolValue == isRightDir.boolValue) } - func matchesFileType(of rightUrl: URL) -> Bool { + func matchesFileType(of rightURL: URL) -> Bool { var isDir = false var leftExists = false var rightExists = false return matchesFileType( - of: rightUrl, + of: rightURL, isDir: &isDir, leftExists: &leftExists, rightExists: &rightExists @@ -126,7 +126,7 @@ public extension URL { chooseFiles: Bool, chooseDirectories: Bool ) -> URL? { - let url = promptUrl( + let url = promptURL( at: findNearestExistingDirectory(), title: panelTitle, chooseDirectories: chooseDirectories, @@ -183,8 +183,8 @@ public extension URL { } @MainActor - func promptUrl( - at startUrl: URL, + func promptURL( + at startURL: URL, title: String, chooseDirectories: Bool, chooseFiles: Bool, @@ -198,7 +198,7 @@ public extension URL { openPanel.canCreateDirectories = canCreateDirectories // since 10.11 the title is no longer shown so we use the message property openPanel.message = title - openPanel.directoryURL = startUrl + openPanel.directoryURL = startURL if openPanel.runModal() == .OK { return openPanel.urls[0] @@ -220,6 +220,7 @@ public extension URL { guard let path2 else { return .orderedDescending } + let components1 = path1.pathComponents let components2 = path2.pathComponents let count1 = components1.count diff --git a/Sources/SharedKit/Extensions/URL/URL+SymLink.swift b/Sources/SharedKit/Extensions/URL/URL+SymLink.swift index 2e489f9..868df4a 100644 --- a/Sources/SharedKit/Extensions/URL/URL+SymLink.swift +++ b/Sources/SharedKit/Extensions/URL/URL+SymLink.swift @@ -21,7 +21,6 @@ extension URL { guard let cPath = osPath.cString(using: .utf8) else { throw EncodingError.conversionFailed(.utf8) } - guard let result = realpath(cPath, nil) else { throw POSIXError(.init(rawValue: errno) ?? .ENOENT) } @@ -84,6 +83,7 @@ extension URL { guard let selectedPath else { return nil } + return (selectedPath, selectedPath != path) } @@ -93,6 +93,7 @@ extension URL { guard let contentType = try resourceValues(forKeys: [.contentTypeKey]).contentType else { return false } + return contentType == .aliasFile } diff --git a/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift b/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift index 023aed5..0ddd504 100644 --- a/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift +++ b/Sources/SharedKit/Extensions/View/TextView/NSTextView+Style.swift @@ -16,6 +16,7 @@ extension NSTextView { ?? NSParagraphStyle.default.mutableCopy() as? NSMutableParagraphStyle else { return } + let charWidth = (" " as NSString).size(withAttributes: [.font: font]).width paragraphStyle.defaultTabInterval = charWidth * Double(tabSpaces) @@ -27,7 +28,7 @@ extension NSTextView { typingAttributes[NSAttributedString.Key.font] = font self.typingAttributes = typingAttributes - let rangeOfChange = NSRange(location: 0, length: string.count) + let rangeOfChange = NSRange(location: 0, length: string.utf16.count) shouldChangeText(in: rangeOfChange, replacementString: nil) textStorage?.setAttributes( typingAttributes, @@ -41,6 +42,7 @@ extension NSTextView { guard let textContainer else { return } + let largeNumberForText = 1.0e7 textContainer.containerSize = NSSize(width: largeNumberForText, height: largeNumberForText) @@ -56,11 +58,9 @@ extension NSTextView { guard let textStorage else { return } - let string = textStorage.string - let length = string.count // remove the old colors - let area = NSRange(location: 0, length: length) + let area = NSRange(location: 0, length: textStorage.string.utf16.count) textStorage.removeAttribute(NSAttributedString.Key.foregroundColor, range: area) textStorage.removeAttribute(NSAttributedString.Key.backgroundColor, range: area) @@ -85,6 +85,6 @@ extension NSTextView { if let textStorage { textStorage.append(attrString) } - scrollRangeToVisible(NSRange(location: string.count, length: 0)) + scrollRangeToVisible(NSRange(location: string.utf16.count, length: 0)) } } diff --git a/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift b/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift index 6cc8ffd..9c84644 100644 --- a/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift +++ b/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift @@ -38,9 +38,9 @@ extension NSWorkspace { var secureURLs = [URL]() for p in paths { - let pUrl = URL(filePath: p) - urls.append(pUrl) - if let secureURL = SecureBookmark.shared.secure(fromBookmark: pUrl, startSecured: true) { + let pURL = URL(filePath: p) + urls.append(pURL) + if let secureURL = SecureBookmark.shared.secure(fromBookmark: pURL, startSecured: true) { secureURLs.append(secureURL) } } diff --git a/Sources/SharedKit/Utilities/Document/SecureBookmark.swift b/Sources/SharedKit/Utilities/Document/SecureBookmark.swift index 40095ee..de19c1f 100644 --- a/Sources/SharedKit/Utilities/Document/SecureBookmark.swift +++ b/Sources/SharedKit/Utilities/Document/SecureBookmark.swift @@ -17,8 +17,8 @@ class SecureBookmark: @unchecked Sendable { private init() {} /** - * Add a new url to the secure bookmarks - * @return true if the url is bookmarked with success, false otherwise + * Add a new URL to the secure bookmarks + * @return true if the URL is bookmarked successfully, false otherwise */ @discardableResult func add(_ path: URL, searchClosestPath: Bool = true) -> Bool { @@ -40,13 +40,8 @@ class SecureBookmark: @unchecked Sendable { includingResourceValuesForKeys: nil, relativeTo: nil ) - var dict = securedPaths - - if dict == nil { - dict = [String: Data]() - } - // swiftlint:disable:next force_unwrapping - dict![path.osPath] = bookmark + var dict = securedPaths ?? [String: Data]() + dict[path.osPath] = bookmark UserDefaults.standard.set(dict, forKey: sandboxedPaths) } catch { Logger.general.error("Secure bookmark failed \(error)") @@ -65,6 +60,7 @@ class SecureBookmark: @unchecked Sendable { let data = dict[bookmarkPath] else { return nil } + var isStale = false do { let url = try URL( @@ -91,6 +87,7 @@ class SecureBookmark: @unchecked Sendable { guard var dict = securedPaths else { return } + for path in paths { dict.removeValue(forKey: path) } @@ -102,10 +99,10 @@ class SecureBookmark: @unchecked Sendable { } func findClosestPath(to path: URL, searchPaths: [String]) -> String? { - // don't matter if path is a file or a directory, add the separator in any case - // so hasPrefix works fine with last path component - // eg "/Users/app 2 3" has prefix "/Users/app 2" but - // "/Users/app 2 3/" hasn't prefix "/Users/app 2/" and this is the correct result + // it does not matter whether path is a file or a directory, add the separator in any case + // so hasPrefix works correctly with the last path component + // for example "/Users/app 2 3" has prefix "/Users/app 2" but + // "/Users/app 2 3/" does not have prefix "/Users/app 2/" and that is the correct result let pathWithSep = path.osPath + "/" let sorted = searchPaths.sorted { $0.caseInsensitiveCompare($1) == .orderedDescending diff --git a/Sources/SharedKit/Utilities/Event/NSEvent+VirtualKeys.swift b/Sources/SharedKit/Utilities/Event/NSEvent+VirtualKeys.swift index 926833e..c9f600d 100644 --- a/Sources/SharedKit/Utilities/Event/NSEvent+VirtualKeys.swift +++ b/Sources/SharedKit/Utilities/Event/NSEvent+VirtualKeys.swift @@ -8,7 +8,6 @@ // swiftformat:disable wrapPropertyBodies // swiftlint:disable identifier_name -@objc enum KeyCode: UInt16 { case ansi_A = 0x00 case ansi_S = 0x01 @@ -270,7 +269,6 @@ extension KeyCode { // swiftlint:enable identifier_name extension NSEvent { - @objc func isDeleteShortcutKey(_ checkCommandDeleteKey: Bool) -> Bool { if checkCommandDeleteKey, modifierFlags.contains(.command), keyCode == KeyCode.deleteCharacter { return true diff --git a/Sources/SharedKit/Utilities/Formatters/FileSizeFormatter.swift b/Sources/SharedKit/Utilities/Formatters/FileSizeFormatter.swift index 11cbf8f..8f94a46 100644 --- a/Sources/SharedKit/Utilities/Formatters/FileSizeFormatter.swift +++ b/Sources/SharedKit/Utilities/Formatters/FileSizeFormatter.swift @@ -6,15 +6,13 @@ // Copyright (c) 2013 visualdiffer.com // -@objc class FileSizeFormatter: NumberFormatter, @unchecked Sendable { private(set) var showInBytes = false private(set) var showUnitForBytes = true private(set) var useGibiBytes = false - @objc static let `default` = FileSizeFormatter() + static let `default` = FileSizeFormatter() - @objc override init() { super.init() @@ -22,7 +20,6 @@ class FileSizeFormatter: NumberFormatter, @unchecked Sendable { maximumFractionDigits = 2 } - @objc convenience init( showInBytes: Bool, showUnitForBytes: Bool, diff --git a/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift b/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift index 24b0631..4adaa4b 100644 --- a/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift +++ b/Sources/SharedKit/Utilities/IO/BufferedInputStream.swift @@ -10,7 +10,6 @@ import Foundation import AppKit /// A line-by-line `InputStream` supporting `\n`, `\r`, and `\r\n` terminators. -// swiftlint:disable force_unwrapping public class BufferedInputStream: InputStream { public enum Constants { public static let invalidated = -2 @@ -162,13 +161,16 @@ public class BufferedInputStream: InputStream { startChar = nextChar nextChar = i if eol { + if data == nil { + data = Data() + } dataBuffer.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in - let ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: startChar) - if data == nil { - data = Data(bytes: ptr, count: i - startChar) - } else { - data!.append(ptr, count: i - startChar) + guard let baseAddress = bytes.baseAddress else { + return } + + let ptr = baseAddress.assumingMemoryBound(to: UInt8.self).advanced(by: startChar) + data?.append(ptr, count: i - startChar) } nextChar += 1 @@ -181,8 +183,12 @@ public class BufferedInputStream: InputStream { data = Data(capacity: Constants.defaultExpectedLineLength) } dataBuffer.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in - let ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: startChar) - data!.append(ptr, count: i - startChar) + guard let baseAddress = bytes.baseAddress else { + return + } + + let ptr = baseAddress.assumingMemoryBound(to: UInt8.self).advanced(by: startChar) + data?.append(ptr, count: i - startChar) } } } @@ -221,7 +227,11 @@ public class BufferedInputStream: InputStream { } let bytesRead = dataBuffer.withUnsafeMutableBytes { (bytes: UnsafeMutableRawBufferPointer) -> Int in - let ptr = bytes.baseAddress!.assumingMemoryBound(to: UInt8.self).advanced(by: dst) + guard let baseAddress = bytes.baseAddress else { + return 0 + } + + let ptr = baseAddress.assumingMemoryBound(to: UInt8.self).advanced(by: dst) return stream.read(ptr, maxLength: dataBufferLen - dst) } @@ -265,5 +275,3 @@ public class BufferedInputStream: InputStream { } } } - -// swiftlint:enable force_unwrapping diff --git a/Sources/SharedKit/Utilities/IO/FileError.swift b/Sources/SharedKit/Utilities/IO/FileError.swift index fa23cd4..51c23ba 100644 --- a/Sources/SharedKit/Utilities/IO/FileError.swift +++ b/Sources/SharedKit/Utilities/IO/FileError.swift @@ -12,6 +12,7 @@ public enum FileError: Error, Equatable { case openFile(path: String) case fileNotExists(path: URL, side: DisplaySide) case unknownVolumeType + case encodingFailed(encoding: String.Encoding) } extension FileError: LocalizedError { @@ -36,6 +37,8 @@ extension FileError: LocalizedError { return String.localizedStringWithFormat(message, path.osPath) case .unknownVolumeType: return NSLocalizedString("Unable to determine the disk volume type", comment: "") + case .encodingFailed: + return NSLocalizedString("Some characters cannot be represented in the current encoding", comment: "") } } diff --git a/Sources/SharedKit/Utilities/IO/PowerAssertion.swift b/Sources/SharedKit/Utilities/IO/PowerAssertion.swift index 605a546..4e75f46 100644 --- a/Sources/SharedKit/Utilities/IO/PowerAssertion.swift +++ b/Sources/SharedKit/Utilities/IO/PowerAssertion.swift @@ -31,6 +31,7 @@ private class PowerItem { guard pmAssertion == kIOPMNullAssertionID else { return kIOReturnError } + return IOPMAssertionCreateWithName( type as CFString, IOPMAssertionLevel(kIOPMAssertionLevelOn), @@ -71,14 +72,12 @@ private class PowerItem { * Minimize the number of IOPMAssertions creating different instances only when the name differs * */ -@objc class PowerAssertion: NSObject, @unchecked Sendable { - @objc static let shared = PowerAssertion() + static let shared = PowerAssertion() private var pmAssertions: [String: PowerItem] = [:] override private init() {} - @objc func setDisableSystemSleep(_ disableSleep: Bool, with name: String) { let item: PowerItem diff --git a/Sources/SharedKit/Utilities/Image/NoodleCustomImageRep.swift b/Sources/SharedKit/Utilities/Image/NoodleCustomImageRep.swift index ff2a05c..a37bc51 100644 --- a/Sources/SharedKit/Utilities/Image/NoodleCustomImageRep.swift +++ b/Sources/SharedKit/Utilities/Image/NoodleCustomImageRep.swift @@ -64,8 +64,9 @@ class NoodleCustomImageRep: NSImageRep { } override func copy(with zone: NSZone? = nil) -> Any { - // swiftlint:disable:next force_cast - let copy = super.copy(with: zone) as! NoodleCustomImageRep + guard let copy = super.copy(with: zone) as? NoodleCustomImageRep else { + preconditionFailure("NSImageRep copy should preserve NoodleCustomImageRep") + } // NSImageRep uses NSCopyObject so we have to force a copy here copy.drawBlock = drawBlock @@ -77,6 +78,7 @@ class NoodleCustomImageRep: NSImageRep { guard let drawBlock else { return false } + drawBlock(self) return true } diff --git a/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift b/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift index 1def531..e54c1f7 100644 --- a/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift +++ b/Sources/SharedKit/Utilities/OpenEditor/OpenEditor.swift @@ -44,6 +44,7 @@ extension OpenEditor { let secureURL = SecureBookmark.shared.secure(fromBookmark: item.path, startSecured: true) else { return } + defer { SecureBookmark.shared.stopAccessing(url: secureURL) } @@ -56,6 +57,7 @@ extension OpenEditor { guard let application else { throw OpenEditorError.applicationNotFound(item.path) } + if let scriptURL = fullScriptURL(application) { try runUnixScript(scriptURL) } else { diff --git a/Sources/SharedKit/Utilities/PopupButton/PreferredEditorPopupCell.swift b/Sources/SharedKit/Utilities/PopupButton/PreferredEditorPopupCell.swift index 22d6888..67b8860 100644 --- a/Sources/SharedKit/Utilities/PopupButton/PreferredEditorPopupCell.swift +++ b/Sources/SharedKit/Utilities/PopupButton/PreferredEditorPopupCell.swift @@ -54,6 +54,7 @@ class PreferredEditorPopupCell: NSPopUpButtonCell { guard let defaultAppPath = NSWorkspace.shared.urlForApplication(toOpen: path) else { return } + // get the localized display name for the app if let values = try? defaultAppPath.resourceValues(forKeys: [URLResourceKey.localizedNameKey]), let defaultAppName = values.localizedName { diff --git a/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift b/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift index 8607090..983d20f 100644 --- a/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift +++ b/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift @@ -27,6 +27,10 @@ class VisibleWhitespaces: NSObject { } static func tabs2space(_ line: String, tabWidth: Int) -> String { + guard tabWidth > 0 else { + return line + } + var dest = "" for ch in line { @@ -41,6 +45,10 @@ class VisibleWhitespaces: NSObject { } func showWhitespaces(_ line: String) -> String { + guard tabWidth > 0 else { + return line + } + let whitespaces = CharacterSet.whitespaces var dest = "" @@ -50,6 +58,7 @@ class VisibleWhitespaces: NSObject { dest.append(ch) continue } + if ch == "\t" { // subtract from spaces the character representing TAB let spaces = (tabWidth - (dest.count % tabWidth)) - 1 diff --git a/Sources/SharedKit/Utilities/TableView/DescriptionOutlineNode.swift b/Sources/SharedKit/Utilities/TableView/DescriptionOutlineNode.swift index cba25ad..28bfcba 100644 --- a/Sources/SharedKit/Utilities/TableView/DescriptionOutlineNode.swift +++ b/Sources/SharedKit/Utilities/TableView/DescriptionOutlineNode.swift @@ -32,6 +32,7 @@ class DescriptionOutlineNode: NSObject { guard let path = item.path else { continue } + let start = path.index(path.startIndex, offsetBy: rootPathLen) let relativePath = String(path[start ..< path.endIndex]) let node = DescriptionOutlineNode(text: relativePath) diff --git a/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift b/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift index 85ca7e3..37da868 100644 --- a/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift +++ b/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift @@ -47,6 +47,7 @@ class DualPaneSplitView: NSSplitView { guard let num = notification.userInfo?["NSSplitViewUserResizeKey"] as? NSNumber else { return } + let isUserResize = num.boolValue if isUserResize { let view = subviews[0] diff --git a/Sources/SharedKit/Utilities/View/Encoding/EncodingManager.swift b/Sources/SharedKit/Utilities/View/Encoding/EncodingManager.swift index 7ac704b..31791de 100644 --- a/Sources/SharedKit/Utilities/View/Encoding/EncodingManager.swift +++ b/Sources/SharedKit/Utilities/View/Encoding/EncodingManager.swift @@ -93,6 +93,7 @@ class EncodingManager: NSObject, @unchecked Sendable { guard let encodingPanel else { return } + if encodingPanel.isVisible { encodingPanel.makeKeyAndOrderFront(sender) return @@ -124,6 +125,7 @@ class EncodingManager: NSObject, @unchecked Sendable { guard let cfEncodings = CFStringGetListOfAvailableEncodings() else { return [] } + var index = 0 while true { @@ -245,6 +247,7 @@ class EncodingManager: NSObject, @unchecked Sendable { guard let encs = UserDefaults.standard.array(forKey: encodingsPrefName) as? [NSNumber] else { return nil } + return encs.map { String.Encoding(rawValue: $0.uintValue) } diff --git a/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift b/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift index 32d0df7..c61ae55 100644 --- a/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift +++ b/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift @@ -129,6 +129,7 @@ class SelectEncodingsPanel: NSWindow, NSTableViewDataSource, NSTableViewDelegate guard let contentView else { return } + let heightConstraint = scrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200) heightConstraint.priority = .defaultHigh @@ -188,6 +189,7 @@ class SelectEncodingsPanel: NSWindow, NSTableViewDataSource, NSTableViewDelegate guard let identifier = tableColumn?.identifier else { return nil } + let cell = tableView.makeView( withIdentifier: identifier, owner: self diff --git a/Sources/SharedKit/Utilities/View/SynchroScroll/SynchroScrollView.swift b/Sources/SharedKit/Utilities/View/SynchroScroll/SynchroScrollView.swift index 0732bc7..22cba05 100644 --- a/Sources/SharedKit/Utilities/View/SynchroScroll/SynchroScrollView.swift +++ b/Sources/SharedKit/Utilities/View/SynchroScroll/SynchroScrollView.swift @@ -73,6 +73,7 @@ class SynchroScrollView: NSScrollView { guard let synchronizedScrollView else { return } + let synchronizedContentView = synchronizedScrollView.contentView // remove any existing notification registration diff --git a/Sources/SharedUI/Cells/FilePathTableCellView.swift b/Sources/SharedUI/Cells/FilePathTableCellView.swift index 52d090b..a6115c8 100644 --- a/Sources/SharedUI/Cells/FilePathTableCellView.swift +++ b/Sources/SharedUI/Cells/FilePathTableCellView.swift @@ -7,7 +7,6 @@ // class FilePathTableCellView: NSTableCellView { - @objc convenience init(identifier: NSUserInterfaceItemIdentifier) { self.init(frame: .zero) self.identifier = identifier @@ -111,7 +110,7 @@ class FilePathTableCellView: NSTableCellView { } /** - * This is called by the parent as discussed on + * This is called by the parent as discussed at * https://developer.apple.com/documentation/appkit/nstablecellview/1483206-backgroundstyle?language=objc * "The default implementation automatically forwards calls to all subviews that implement setBackgroundStyle" */ diff --git a/Sources/SharedUI/Components/DiffCounters/DiffCountersTextFieldCell.swift b/Sources/SharedUI/Components/DiffCounters/DiffCountersTextFieldCell.swift index ae77698..fffcc66 100644 --- a/Sources/SharedUI/Components/DiffCounters/DiffCountersTextFieldCell.swift +++ b/Sources/SharedUI/Components/DiffCounters/DiffCountersTextFieldCell.swift @@ -13,7 +13,7 @@ let kStrokelineWidth: CGFloat = 1.0 let kDotBlendFraction: CGFloat = 0.5 class DiffCountersTextFieldCell: NSTextFieldCell { - @objc var counterItems = [DiffCountersItem]() + var counterItems = [DiffCountersItem]() override func draw(withFrame cellFrame: NSRect, in controlView: NSView) { if !stringValue.isEmpty { @@ -39,17 +39,17 @@ class DiffCountersTextFieldCell: NSTextFieldCell { var textOrigin = NSPoint(x: rect.origin.x + dotRect.size.width + kDotPaddingEnd, y: rect.origin.y) let offset = (textSize.height - dotRect.size.height) / 2 - // the dot height is smaller than the text so center it + // the dot height is smaller than the text, so center it if offset > 0 { dotRect.origin.y += offset } else { - // the dot height is taller than the text so center the text + // the dot height is taller than the text, so center the text textOrigin.y -= offset } drawDot(item.color, rect: dotRect, strokeLineWidth: kStrokelineWidth) item.text.draw(at: textOrigin, withAttributes: attrs) - // move to next dot position + // move to the next dot position rect.origin.x = textOrigin.x + textSize.width + kTextPaddingEnd } } diff --git a/Sources/SharedUI/Components/DiffCounters/DifferenceCounters.swift b/Sources/SharedUI/Components/DiffCounters/DifferenceCounters.swift index 011e9cd..5d9f0af 100644 --- a/Sources/SharedUI/Components/DiffCounters/DifferenceCounters.swift +++ b/Sources/SharedUI/Components/DiffCounters/DifferenceCounters.swift @@ -35,7 +35,6 @@ class DifferenceCounters: NSTextField { cell = counter } - @objc func update(counters: [DiffCountersItem]) { stringValue = "" counter.counterItems = counters diff --git a/Sources/SharedUI/Components/FindText.swift b/Sources/SharedUI/Components/FindText.swift index 160d1f1..84c881e 100644 --- a/Sources/SharedUI/Components/FindText.swift +++ b/Sources/SharedUI/Components/FindText.swift @@ -19,17 +19,14 @@ class FindText: NSView, NSSearchFieldDelegate { var delegate: FindTextDelegate? private lazy var rewindView: WindowOSD = .init( - // swiftlint:disable:next force_unwrapping - image: NSImage(named: VDImageNameRewind)!, + image: NSImage.required(named: VDImageNameRewind), parent: window ) private lazy var arrows: NSSegmentedControl = { let images = [ - // swiftlint:disable:next force_unwrapping - NSImage(named: NSImage.goLeftTemplateName)!, - // swiftlint:disable:next force_unwrapping - NSImage(named: NSImage.goRightTemplateName)!, + NSImage.required(named: NSImage.goLeftTemplateName), + NSImage.required(named: NSImage.goRightTemplateName), ] let view = NSSegmentedControl( images: images, @@ -129,6 +126,7 @@ class FindText: NSView, NSSearchFieldDelegate { guard let delegate else { return false } + return delegate.numberOfMatches(in: self) > 0 } @@ -136,6 +134,7 @@ class FindText: NSView, NSSearchFieldDelegate { guard let delegate else { return } + let count = delegate.numberOfMatches(in: self) if count > 0 { arrows.isEnabled = true @@ -174,6 +173,7 @@ class FindText: NSView, NSSearchFieldDelegate { guard let delegate else { return } + let foundCount = delegate.numberOfMatches(in: self) if foundCount == 0 { updateCount() diff --git a/Sources/SharedUI/Components/NSTextField+CenterVertically.swift b/Sources/SharedUI/Components/NSTextField+CenterVertically.swift index d07def7..5615207 100644 --- a/Sources/SharedUI/Components/NSTextField+CenterVertically.swift +++ b/Sources/SharedUI/Components/NSTextField+CenterVertically.swift @@ -6,7 +6,6 @@ // Copyright (c) 2025 visualdiffer.com // -@objc extension NSTextField { func centerVertically() { let centeredCell = RSVerticallyCenteredTextFieldCell(textCell: "") diff --git a/Sources/SharedUI/Components/PathControl.swift b/Sources/SharedUI/Components/PathControl.swift index 5d7962c..8febe83 100644 --- a/Sources/SharedUI/Components/PathControl.swift +++ b/Sources/SharedUI/Components/PathControl.swift @@ -13,7 +13,7 @@ protocol PathControlDelegate: NSPathControlDelegate { optional func pathControl(_ pathControl: PathControl, willContextMenu menu: NSMenu) @objc @MainActor - optional func pathControl(_ pathControl: PathControl, chosenUrl url: URL) + optional func pathControl(_ pathControl: PathControl, chosenURL url: URL) @objc @MainActor optional func pathControl(_ pathControl: PathControl, openWithApp app: URL) @@ -28,7 +28,7 @@ protocol PathControlDelegate: NSPathControlDelegate { public class PathControl: NSPathControl, NSMenuItemValidation { var safePathComponentItem: NSPathControlItem? { - // click is out any cell so clickedPathItem returned nil + // click is outside any cell so clickedPathItem returns nil guard let item = clickedPathItem else { return pathItems.last } @@ -36,7 +36,7 @@ public class PathControl: NSPathControl, NSMenuItemValidation { return item } - @objc var clickedPath: URL? { + var clickedPath: URL? { clickedPathItem?.url } @@ -114,7 +114,7 @@ public class PathControl: NSPathControl, NSMenuItemValidation { if action == #selector(choosePath) { return true } else if action == #selector(copyFileNames) { - // otherwise the item "Copy Path" is always visible + // otherwise the item "Copy File Name" is always visible menuItem.isAlternate = false } menuItem.isHidden = true @@ -138,7 +138,7 @@ public class PathControl: NSPathControl, NSMenuItemValidation { return } - // URL returns by pathCell can't be resolved but Finder so with get the file path + // the URL returned by pathCell cannot be resolved by Finder, so we pass the file path let paths = [url.osPath] NSWorkspace.shared.show(inFinder: paths) } @@ -193,7 +193,7 @@ public class PathControl: NSPathControl, NSMenuItemValidation { let URL = openPanel.urls[0] if let bindingsInfo = infoForBinding(.value) { - // Note that we set the value with an NSString not an URL + // note that we set the value with a path string, not a URL if let object = bindingsInfo[NSBindingInfoKey.observedObject] as? NSObject, let bindingsPath = bindingsInfo[NSBindingInfoKey.observedKeyPath] as? String { object.setValue( @@ -202,14 +202,14 @@ public class PathControl: NSPathControl, NSMenuItemValidation { ) } } - delegate.pathControl?(self, chosenUrl: URL) + delegate.pathControl?(self, chosenURL: URL) } } // MARK: - overridden override public var intrinsicContentSize: NSSize { - // Let it be flexible when using Auto Layout + // keep it flexible when using Auto Layout NSSize(width: NSView.noIntrinsicMetric, height: NSView.noIntrinsicMetric) } diff --git a/Sources/SharedUI/Components/PathView.swift b/Sources/SharedUI/Components/PathView.swift index c186bfc..7609829 100644 --- a/Sources/SharedUI/Components/PathView.swift +++ b/Sources/SharedUI/Components/PathView.swift @@ -61,9 +61,9 @@ class PathView: NSView { setupViews() } - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + @available(*, unavailable, message: "use init(frame:)") + required init?(coder _: NSCoder) { + nil } func setupViews() { @@ -72,7 +72,7 @@ class PathView: NSView { addSubview(stackView) setupConstraints() - // enable iterates the views so we must set it only after all views are added to self + // isEnabled iterates the views, so set it only after all views are added to self isEnabled = true } @@ -116,11 +116,8 @@ class PathView: NSView { } private func createBrowseButton() -> NSButton { - guard let image = NSImage(named: VDImageNameBrowse) else { - fatalError("Unable to create image for \(VDImageNameBrowse)") - } let view = NSButton( - image: image, + image: NSImage.required(named: VDImageNameBrowse), target: pathControl, action: #selector(PathControl.choosePath) ) @@ -133,11 +130,8 @@ class PathView: NSView { } private func createSaveButton() -> NSButton { - guard let image = NSImage(named: VDImageNameSave) else { - fatalError("Unable to create image for \(VDImageNameSave)") - } let view = NSButton( - image: image, + image: NSImage.required(named: VDImageNameSave), target: nil, action: nil ) @@ -186,8 +180,9 @@ class PathView: NSView { guard let delegate = pathControl.delegate else { return } - // We enable the views ourselves but when NSConditionallySetsEnabledBindingOption is true (the default) - // they are automagically enabled by the binding system so we turn off the NSConditionallySetsEnabledBindingOption flag + + // we enable the views ourselves, but when NSConditionallySetsEnabledBindingOption is true, which is the default, + // they are automatically enabled by the binding system so we turn off the NSConditionallySetsEnabledBindingOption flag let pathControlBindOptions = [ NSBindingOption.conditionallySetsEnabled: false, ] diff --git a/Sources/SharedUI/Components/PopUpButtonUrl.swift b/Sources/SharedUI/Components/PopUpButtonURL.swift similarity index 74% rename from Sources/SharedUI/Components/PopUpButtonUrl.swift rename to Sources/SharedUI/Components/PopUpButtonURL.swift index 136f5e0..af4769f 100644 --- a/Sources/SharedUI/Components/PopUpButtonUrl.swift +++ b/Sources/SharedUI/Components/PopUpButtonURL.swift @@ -1,12 +1,12 @@ // -// PopUpButtonUrl.swift +// PopUpButtonURL.swift // VisualDiffer // // Created by davide ficano on 25/03/17. // Copyright (c) 2017 visualdiffer.com // -class PopUpButtonUrl: NSPopUpButton { +class PopUpButtonURL: NSPopUpButton { init( title: String, target: AnyObject?, @@ -37,12 +37,11 @@ class PopUpButtonUrl: NSPopUpButton { fatalError("init(coder:) has not been implemented") } - @objc - func fill(_ documentUrls: [URL]) { - let dict = uniq(documentUrls: documentUrls) + func fill(_ documentURLs: [URL]) { + let dict = uniq(documentURLs: documentURLs) - // iterate documentURLs instead of dictionary because order is not preserved in dictionary - for url in documentUrls { + // iterate documentURLs instead of the dictionary because dictionary order is not preserved + for url in documentURLs { let key = url.lastPathComponent guard let arr = dict[key] else { continue @@ -65,21 +64,20 @@ class PopUpButtonUrl: NSPopUpButton { } } - @objc func clear() { - // leave the button title and remove all other menu items + // keep the button title and remove all other menu items for i in stride(from: numberOfItems - 1, through: 1, by: -1) { removeItem(at: i) } } - // menu label contains the last URL path component - // It would be present more times (same filename in different disk folders) - // so we group by last path component - private func uniq(documentUrls: [URL]) -> [String: [URL]] { + // menu labels contain the last URL path component + // it can appear multiple times for the same filename in different folders + // so group by the last path component + private func uniq(documentURLs: [URL]) -> [String: [URL]] { var dict = [String: [URL]]() - for url in documentUrls { + for url in documentURLs { let key = url.lastPathComponent var arr = dict[key] ?? [] arr.append(url) diff --git a/Tests/Sources/BaseTests/BaseTests+AssertCompareItem.swift b/Tests/Sources/BaseTests/BaseTests+AssertCompareItem.swift index 8898391..9c9da68 100644 --- a/Tests/Sources/BaseTests/BaseTests+AssertCompareItem.swift +++ b/Tests/Sources/BaseTests/BaseTests+AssertCompareItem.swift @@ -27,6 +27,7 @@ public extension BaseTests { Issue.record("CompareItem is nil", sourceLocation: sourceLocation) return } + let safeName = expectedFileName ?? "(nil filename)" if let expectedFileName { if let fsFileName = item.fileName { @@ -66,6 +67,7 @@ public extension BaseTests { Issue.record("Error is not a FileError: \(error)", sourceLocation: sourceLocation) return } + #expect( fileError == expected, "Error doesn't match: expected '\(expected) found '\(error)'", @@ -93,6 +95,7 @@ public extension BaseTests { guard let content else { return } + let foundContent = try String(contentsOfFile: path, encoding: .utf8) #expect(foundContent == content, "File \(path) content does not match, expected \(content) found \(foundContent)", sourceLocation: sourceLocation) } diff --git a/Tests/Sources/BaseTests/DateBuilder.swift b/Tests/Sources/BaseTests/DateBuilder.swift index e90af61..d8ec8d9 100644 --- a/Tests/Sources/BaseTests/DateBuilder.swift +++ b/Tests/Sources/BaseTests/DateBuilder.swift @@ -23,6 +23,7 @@ public struct DateBuilder { guard let date = isoDateFormatter.date(from: strDate) else { throw NSError(domain: "Unable to parse date", code: 0, userInfo: nil) } + return date } } diff --git a/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift b/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift index 4671862..cb713a0 100644 --- a/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift +++ b/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift @@ -140,6 +140,7 @@ final class DiffResultTests: DiffResultBaseTests { Issue.record("No indexes found") return } + #expect(indexes == IndexSet(integersIn: 4 ..< 8)) } diff --git a/Tests/Sources/FileCompare/DiffResult/Tests/DiffSideTests.swift b/Tests/Sources/FileCompare/DiffResult/Tests/DiffSideTests.swift new file mode 100644 index 0000000..9f97776 --- /dev/null +++ b/Tests/Sources/FileCompare/DiffResult/Tests/DiffSideTests.swift @@ -0,0 +1,77 @@ +// +// DiffSideTests.swift +// VisualDiffer +// +// Created by davide ficano on 19/05/26. +// Copyright (c) 2026 visualdiffer.com +// + +import Foundation +import Testing +@testable import VisualDiffer + +final class DiffSideTests: DiffResultBaseTests { + @Test + func writeLeavesOriginalFileUntouchedWhenEncodingFails() throws { + try createFolder("") + try removeItem("file.txt") + try createFile("file.txt", "original") + + let diffSide = DiffSide() + diffSide.add( + line: DiffLine( + with: .matching, + number: 1, + component: DiffLineComponent(text: "Euro €", eol: .missing) + ) + ) + + do { + try diffSide.write( + path: appendFolder("file.txt", false), + encoding: .ascii + ) + Issue.record("Expected encoding failure") + } catch let error as FileError { + #expect(error == .encodingFailed(encoding: .ascii)) + } + + let text = try String( + contentsOf: appendFolder("file.txt", false), + encoding: .utf8 + ) + #expect(text == "original") + } + + @Test + func writeUpdatesSymlinkDestinationWithoutReplacingTheLink() throws { + try createFolder("") + try removeItem("target.txt") + try removeItem("link.txt") + try createFile("target.txt", "original") + try createSymlink("link.txt", "target.txt") + + let diffSide = DiffSide() + diffSide.add( + line: DiffLine( + with: .matching, + number: 1, + component: DiffLineComponent(text: "updated", eol: .missing) + ) + ) + + let linkUrl = appendFolder("link.txt", false) + let targetUrl = appendFolder("target.txt", false) + + try diffSide.write( + path: linkUrl, + encoding: .utf8 + ) + + let targetText = try String(contentsOf: targetUrl, encoding: .utf8) + #expect(targetText == "updated") + + let destination = try fm.destinationOfSymbolicLink(atPath: linkUrl.osPath) + #expect(destination == targetUrl.osPath) + } +} diff --git a/Tests/Sources/FileSystem/BaseTests+AssertFileSystem.swift b/Tests/Sources/FileSystem/BaseTests+AssertFileSystem.swift index c5ebf21..567c161 100644 --- a/Tests/Sources/FileSystem/BaseTests+AssertFileSystem.swift +++ b/Tests/Sources/FileSystem/BaseTests+AssertFileSystem.swift @@ -18,15 +18,16 @@ public extension BaseTests { functionName: String = #function, sourceLocation: SourceLocation = #_sourceLocation ) throws { - guard let url = item.toUrl() else { + guard let url = item.toURL() else { try #require(item.path != nil, "Unable to find path for \(item)", sourceLocation: sourceLocation) return } + do { let resolved = try FileManager.default.destinationOfSymbolicLink(atPath: url.osPath) let real = URL(filePath: resolved, directoryHint: item.isFolder ? .isDirectory : .notDirectory) - let destUrl = appendFolder(destPath, functionName: functionName) - #expect(destUrl == real, "symlink dest doesn't match: expected \(destUrl) found \(real)", sourceLocation: sourceLocation) + let destURL = appendFolder(destPath, functionName: functionName) + #expect(destURL == real, "symlink dest doesn't match: expected \(destURL) found \(real)", sourceLocation: sourceLocation) #expect(item.isSymbolicLink, "\(url) must be a symlink", sourceLocation: sourceLocation) if isFolder { #expect(item.isFolder, "\(url) must be a folder", sourceLocation: sourceLocation) @@ -52,6 +53,7 @@ public extension BaseTests { try #require(item.path != nil, "Unable to find path for \(item)", sourceLocation: sourceLocation) return } + do { let attrs = try FileManager.default.attributesOfItem(atPath: fsPath) @@ -82,6 +84,7 @@ public extension BaseTests { Issue.record("CompareItem is nil", sourceLocation: sourceLocation) return } + #expect(item.mismatchingTags == oldValue, "Tags for '\(fileName)' expected \(oldValue) found \(item.mismatchingTags)", sourceLocation: sourceLocation) } @@ -90,6 +93,7 @@ public extension BaseTests { Issue.record("CompareItem is nil", sourceLocation: sourceLocation) return } + #expect(item.summary.hasMetadataTags == value, "Folder '\(fileName)' tags must be \(value)", sourceLocation: sourceLocation) } @@ -98,6 +102,7 @@ public extension BaseTests { Issue.record("CompareItem is nil", sourceLocation: sourceLocation) return } + #expect(item.mismatchingLabels == oldValue, "Labels for '\(fileName!)' expected \(oldValue) found \(item.mismatchingLabels)", sourceLocation: sourceLocation) } @@ -106,6 +111,7 @@ public extension BaseTests { Issue.record("CompareItem is nil", sourceLocation: sourceLocation) return } + #expect(item.summary.hasMetadataLabels == value, "Folder '\(fileName!)' labels must be \(value)", sourceLocation: sourceLocation) } @@ -114,8 +120,9 @@ public extension BaseTests { Issue.record("CompareItem is nil", sourceLocation: sourceLocation) return } + do { - let foundValue = try getLabelNumber(item.toUrl()!) + let foundValue = try getLabelNumber(item.toURL()!) #expect(foundValue == expectedValue, "Label for '\(path)' expected \(expectedValue) found \(foundValue)", sourceLocation: sourceLocation) } catch { Issue.record("Found error \(error)", sourceLocation: sourceLocation) diff --git a/Tests/Sources/FileSystem/BaseTests+FileSystemOperations.swift b/Tests/Sources/FileSystem/BaseTests+FileSystemOperations.swift index 6b8699c..a6a5dc7 100644 --- a/Tests/Sources/FileSystem/BaseTests+FileSystemOperations.swift +++ b/Tests/Sources/FileSystem/BaseTests+FileSystemOperations.swift @@ -11,8 +11,7 @@ import Foundation public extension BaseTests { func appendFolder(_ path: String, _ isFolder: Bool = true, functionName: String = #function) -> URL { rootDir - // swiftlint:disable:next line_length - .appending(path: functionName.trimmingCharacters(in: CharacterSet(charactersIn: "()")), directoryHint: .isDirectory) + .appending(path: pathComponent(functionName: functionName), directoryHint: .isDirectory) .appending(path: path, directoryHint: isFolder ? .isDirectory : .notDirectory) } @@ -100,4 +99,8 @@ public extension BaseTests { } return 0 } + + func pathComponent(functionName: String) -> String { + functionName.trimmingCharacters(in: CharacterSet(charactersIn: "()")) + } } diff --git a/Tests/Sources/FileSystem/Tests/AlignmentTests.swift b/Tests/Sources/FileSystem/Tests/AlignmentTests.swift index 700cb2d..57e7bb4 100644 --- a/Tests/Sources/FileSystem/Tests/AlignmentTests.swift +++ b/Tests/Sources/FileSystem/Tests/AlignmentTests.swift @@ -1612,6 +1612,139 @@ final class AlignmentTests: CaseSensitiveBaseTest { } } } + + @Test + func alignFilenameWithUnicode() throws { + let fileNameAlignments: [AlignRule] = [ + AlignRule( + regExp: AlignRegExp(pattern: "(.*)\\.txt", options: []), + template: AlignTemplate(pattern: "$1.doc", options: []) + ), + ] + let comparatorDelegate = MockItemComparatorDelegate() + let comparator = ItemComparator( + options: [.timestamp, .size, .content, .alignMatchCase], + delegate: comparatorDelegate, + bufferSize: 8192, + isLeftCaseSensitive: false, + isRightCaseSensitive: false, + fileNameAlignments: fileNameAlignments + ) + let filterConfig = FilterConfig( + showFilteredFiles: false, + hideEmptyFolders: true, + followSymLinks: false, + skipPackages: true, + traverseFilteredFolders: true, + predicate: defaultPredicate, + fileExtraOptions: [], + displayOptions: .onlyMismatches + ) + let folderReaderDelegate = MockFolderReaderDelegate(isRunning: true) + let folderReader = FolderReader( + with: folderReaderDelegate, + comparator: comparator, + filterConfig: filterConfig, + refreshInfo: RefreshInfo(initState: true) + ) + + try removeItem("l") + try removeItem("r") + + // create folders + try createFolder("l") + try createFolder("r") + + // create files + try createFile("l/a.txt", "l-a") + try createFile("r/a.doc", "r-a") + try createFile("l/b.txt", "l-b") + try createFile("r/b.doc", "r-b") + try createFile("l/c.txt", "l-c") + try createFile("r/c.doc", "r-c") + try createFile("l/photo𝄞_thumb.txt", "l-astral-plane-chr") + try createFile("r/photo𝄞_thumb.doc", "r-astral-plane-chr") + try createFile("l/photo𝄞.txt", "l-astral-plane-chr") + try createFile("r/photo𝄞.doc", "r-astral-plane-chr") + + folderReader.start( + withLeftRoot: nil, + rightRoot: nil, + leftPath: appendFolder("l"), + rightPath: appendFolder("r") + ) + + let rootL = try #require(folderReader.leftRoot) + let vi = try #require(rootL.visibleItem) + + do { + let child1 = rootL // l <-> r + assertItem(child1, 0, 5, 0, 0, 5, "l", .orphan, 45) + #expect(child1.orphanFolders == 0, "OrphanFolder: Expected count 0 found \(child1.orphanFolders)") + assertItem(child1.linkedItem, 0, 5, 0, 0, 5, "r", .orphan, 45) + #expect(child1.linkedItem?.orphanFolders == 0, "OrphanFolder: Expected count 0 found \(child1.linkedItem?.orphanFolders)") + + let child2 = child1.children[0] // l <-> r + assertItem(child2, 0, 1, 0, 0, 0, "a.txt", .changed, 3) + assertItem(child2.linkedItem, 0, 1, 0, 0, 0, "a.doc", .changed, 3) + + let child3 = child1.children[1] // l <-> r + assertItem(child3, 0, 1, 0, 0, 0, "b.txt", .changed, 3) + assertItem(child3.linkedItem, 0, 1, 0, 0, 0, "b.doc", .changed, 3) + + let child4 = child1.children[2] // l <-> r + assertItem(child4, 0, 1, 0, 0, 0, "c.txt", .changed, 3) + assertItem(child4.linkedItem, 0, 1, 0, 0, 0, "c.doc", .changed, 3) + + let child5 = child1.children[3] // l <-> r + assertItem(child5, 0, 1, 0, 0, 0, "photo𝄞_thumb.txt", .changed, 18) + assertItem(child5.linkedItem, 0, 1, 0, 0, 0, "photo𝄞_thumb.doc", .changed, 18) + + let child6 = child1.children[4] // l <-> r + assertItem(child6, 0, 1, 0, 0, 0, "photo𝄞.txt", .changed, 18) + assertItem(child6.linkedItem, 0, 1, 0, 0, 0, "photo𝄞.doc", .changed, 18) + } + do { + // VisibleItems + let childVI1 = vi // l <--> r + assertArrayCount(childVI1.children, 5) + let child1 = childVI1.item // nil <-> nil + assertItem(child1, 0, 5, 0, 0, 5, "l", .orphan, 45) + #expect(child1.orphanFolders == 0, "OrphanFolder: Expected count 0 found \(child1.orphanFolders)") + assertItem(child1.linkedItem, 0, 5, 0, 0, 5, "r", .orphan, 45) + #expect(child1.linkedItem?.orphanFolders == 0, "OrphanFolder: Expected count 0 found \(child1.linkedItem?.orphanFolders)") + + let childVI2 = childVI1.children[0] // l <--> r + assertArrayCount(childVI2.children, 0) + let child2 = childVI2.item // l <-> r + assertItem(child2, 0, 1, 0, 0, 0, "a.txt", .changed, 3) + assertItem(child2.linkedItem, 0, 1, 0, 0, 0, "a.doc", .changed, 3) + + let childVI3 = childVI1.children[1] // l <--> r + assertArrayCount(childVI3.children, 0) + let child3 = childVI3.item // l <-> r + assertItem(child3, 0, 1, 0, 0, 0, "b.txt", .changed, 3) + assertItem(child3.linkedItem, 0, 1, 0, 0, 0, "b.doc", .changed, 3) + + let childVI4 = childVI1.children[2] // l <--> r + assertArrayCount(childVI4.children, 0) + let child4 = childVI4.item // l <-> r + assertItem(child4, 0, 1, 0, 0, 0, "c.txt", .changed, 3) + assertItem(child4.linkedItem, 0, 1, 0, 0, 0, "c.doc", .changed, 3) + + let childVI5 = childVI1.children[3] // l <--> r + assertArrayCount(childVI5.children, 0) + let child5 = childVI5.item // l <-> r + assertItem(child5, 0, 1, 0, 0, 0, "photo𝄞_thumb.txt", .changed, 18) + assertItem(child5.linkedItem, 0, 1, 0, 0, 0, "photo𝄞_thumb.doc", .changed, 18) + + let childVI6 = childVI1.children[4] // l <--> r + assertArrayCount(childVI6.children, 0) + let child6 = childVI6.item // l <-> r + assertItem(child6, 0, 1, 0, 0, 0, "photo𝄞.txt", .changed, 18) + assertItem(child6.linkedItem, 0, 1, 0, 0, 0, "photo𝄞.doc", .changed, 18) + } + } } // swiftlint:enable file_length function_body_length diff --git a/Tests/Sources/FileSystem/Tests/BufferedInputStreamTests.swift b/Tests/Sources/FileSystem/Tests/BufferedInputStreamTests.swift index 0874230..ae0a773 100644 --- a/Tests/Sources/FileSystem/Tests/BufferedInputStreamTests.swift +++ b/Tests/Sources/FileSystem/Tests/BufferedInputStreamTests.swift @@ -164,6 +164,7 @@ final class BufferedInputStreamTests { let bisOldLine = bisOld.readLine() else { break } + lineCount += 1 #expect(bisNewLine == bisOldLine) } diff --git a/Tests/Sources/Utils/StringUtils.swift b/Tests/Sources/Utils/StringUtils.swift index f5eae2e..ae2a369 100644 --- a/Tests/Sources/Utils/StringUtils.swift +++ b/Tests/Sources/Utils/StringUtils.swift @@ -18,6 +18,7 @@ public func invertCase(_ str: inout String, index: Int) { guard len > 0 else { return } + let index = str.index(str.startIndex, offsetBy: index) let ch: Character = str[index] let inverted = ch.isUppercase ? str[index].lowercased() : str[index].uppercased() diff --git a/VisualDiffer.xcodeproj/project.pbxproj b/VisualDiffer.xcodeproj/project.pbxproj index 3d5e045..12ae204 100644 --- a/VisualDiffer.xcodeproj/project.pbxproj +++ b/VisualDiffer.xcodeproj/project.pbxproj @@ -174,7 +174,6 @@ 553741F12E9372DF00AB56D0 /* TextPreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537409E2E9372DF00AB56D0 /* TextPreferencesPanel.swift */; }; 553741F22E9372DF00AB56D0 /* AlignPopupButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741642E9372DF00AB56D0 /* AlignPopupButtonCell.swift */; }; 553741F32E9372DF00AB56D0 /* FolderSelectionInfo+ViewerActionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537406D2E9372DF00AB56D0 /* FolderSelectionInfo+ViewerActionValidator.swift */; }; - 553741F42E9372DF00AB56D0 /* PopUpButtonUrl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741722E9372DF00AB56D0 /* PopUpButtonUrl.swift */; }; 553741F52E9372DF00AB56D0 /* OpenEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741392E9372DF00AB56D0 /* OpenEditor.swift */; }; 553741F62E9372DF00AB56D0 /* NSMenu+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740F72E9372DF00AB56D0 /* NSMenu+File.swift */; }; 553741F72E9372DF00AB56D0 /* FilesWindowController+FileInfoBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC92E9372DF00AB56D0 /* FilesWindowController+FileInfoBarDelegate.swift */; }; @@ -423,6 +422,7 @@ 557315B12551307800CF4372 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 557315B02551307800CF4372 /* Images.xcassets */; }; 55740BF512D710EC004BF6CC /* IOKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55740BF412D710EC004BF6CC /* IOKit.framework */; }; 55740BF912D71103004BF6CC /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55740BF812D71103004BF6CC /* Security.framework */; }; + 5577FFE82FAF2FC30084FB06 /* PopUpButtonURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5577FFE72FAF2FC30084FB06 /* PopUpButtonURL.swift */; }; 557BBB0E2ED1BBB00016CBD4 /* DiffLineComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557BBB0D2ED1BBB00016CBD4 /* DiffLineComponent.swift */; }; 557C18182E7E6E0100381A3A /* big_file_5000_lines.txt in Resources */ = {isa = PBXBuildFile; fileRef = 557C18162E7E6E0100381A3A /* big_file_5000_lines.txt */; }; 557C18232E7E8C0D00381A3A /* Signing.local.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 557C18212E7E8C0D00381A3A /* Signing.local.xcconfig */; }; @@ -451,6 +451,7 @@ 559E927E1514A63100B76349 /* dropzone.png in Resources */ = {isa = PBXBuildFile; fileRef = 559E927D1514A63100B76349 /* dropzone.png */; }; 55A027D4158242310004583F /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55A027D3158242310004583F /* Quartz.framework */; }; 55A4DF322E9A49B000ED11BB /* EndOfLineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A4DF312E9A49A700ED11BB /* EndOfLineTests.swift */; }; + 55A4DF342FFD200100ED11BB /* DiffSideTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A4DF332FFD1FFF00ED11BB /* DiffSideTests.swift */; }; 55A585772F52BD8A006DD544 /* FileOperationDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55A585762F52BD88006DD544 /* FileOperationDestination.swift */; }; 55B1CE2B2E9934E900426FEC /* DateBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B1CE2A2E9934E900426FEC /* DateBuilder.swift */; }; 55B1CE312E9934F800426FEC /* DiffResultBaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B1CE2E2E9934F800426FEC /* DiffResultBaseTests.swift */; }; @@ -972,7 +973,6 @@ 5537416F2E9372DF00AB56D0 /* NSTextField+CenterVertically.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+CenterVertically.swift"; sourceTree = ""; }; 553741702E9372DF00AB56D0 /* PathControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathControl.swift; sourceTree = ""; }; 553741712E9372DF00AB56D0 /* PathView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathView.swift; sourceTree = ""; }; - 553741722E9372DF00AB56D0 /* PopUpButtonUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpButtonUrl.swift; sourceTree = ""; }; 553741732E9372DF00AB56D0 /* TablePanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TablePanelView.swift; sourceTree = ""; }; 553741742E9372DF00AB56D0 /* TimeToleranceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeToleranceView.swift; sourceTree = ""; }; 5537434A2E937B7F00AB56D0 /* colors.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = colors.json; path = AppDefaults/colors.json; sourceTree = ""; }; @@ -1010,6 +1010,7 @@ 557315B02551307800CF4372 /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = assets/Images.xcassets; sourceTree = SOURCE_ROOT; }; 55740BF412D710EC004BF6CC /* IOKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOKit.framework; path = System/Library/Frameworks/IOKit.framework; sourceTree = SDKROOT; }; 55740BF812D71103004BF6CC /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; + 5577FFE72FAF2FC30084FB06 /* PopUpButtonURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopUpButtonURL.swift; sourceTree = ""; }; 557BBB0D2ED1BBB00016CBD4 /* DiffLineComponent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffLineComponent.swift; sourceTree = ""; }; 557C18162E7E6E0100381A3A /* big_file_5000_lines.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = big_file_5000_lines.txt; sourceTree = ""; }; 557C181B2E7E8BDD00381A3A /* Signing.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.local.xcconfig; sourceTree = ""; }; @@ -1045,6 +1046,7 @@ 559E927D1514A63100B76349 /* dropzone.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dropzone.png; sourceTree = ""; }; 55A027D3158242310004583F /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; }; 55A4DF312E9A49A700ED11BB /* EndOfLineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndOfLineTests.swift; sourceTree = ""; }; + 55A4DF332FFD1FFF00ED11BB /* DiffSideTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSideTests.swift; sourceTree = ""; }; 55A585762F52BD88006DD544 /* FileOperationDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperationDestination.swift; sourceTree = ""; }; 55A67B5113FF95B5006DD5BB /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 55B1CE2A2E9934E900426FEC /* DateBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateBuilder.swift; sourceTree = ""; }; @@ -2712,7 +2714,7 @@ 5537416F2E9372DF00AB56D0 /* NSTextField+CenterVertically.swift */, 553741702E9372DF00AB56D0 /* PathControl.swift */, 553741712E9372DF00AB56D0 /* PathView.swift */, - 553741722E9372DF00AB56D0 /* PopUpButtonUrl.swift */, + 5577FFE72FAF2FC30084FB06 /* PopUpButtonURL.swift */, 553741732E9372DF00AB56D0 /* TablePanelView.swift */, 553741742E9372DF00AB56D0 /* TimeToleranceView.swift */, ); @@ -2822,9 +2824,10 @@ 55B1CE2D2E9934F800426FEC /* Tests */ = { isa = PBXGroup; children = ( + 55B1CE2C2E9934F800426FEC /* DiffResultTests.swift */, + 55A4DF332FFD1FFF00ED11BB /* DiffSideTests.swift */, 55A4DF312E9A49A700ED11BB /* EndOfLineTests.swift */, 55F9E79B2EDD6221001218C7 /* WhitespacesTests.swift */, - 55B1CE2C2E9934F800426FEC /* DiffResultTests.swift */, ); path = Tests; sourceTree = ""; @@ -3213,6 +3216,7 @@ 55D8812E2F3DA332008DE497 /* CopyFinderMetadata.swift in Sources */, 55DF3DDD2E924BB10044CC0C /* CaseSensitiveBaseTest.swift in Sources */, 55DF3DDE2E924BB10044CC0C /* BaseTests+AssertCompareItem.swift in Sources */, + 55A4DF342FFD200100ED11BB /* DiffSideTests.swift in Sources */, 55A4DF322E9A49B000ED11BB /* EndOfLineTests.swift in Sources */, 55DF3DDF2E924BB10044CC0C /* AlignmentTests.swift in Sources */, 554750C62F5C0A1100C1747A /* CommonAncestorPathTests.swift in Sources */, @@ -3404,7 +3408,6 @@ 553741F12E9372DF00AB56D0 /* TextPreferencesPanel.swift in Sources */, 553741F22E9372DF00AB56D0 /* AlignPopupButtonCell.swift in Sources */, 553741F32E9372DF00AB56D0 /* FolderSelectionInfo+ViewerActionValidator.swift in Sources */, - 553741F42E9372DF00AB56D0 /* PopUpButtonUrl.swift in Sources */, 553741F52E9372DF00AB56D0 /* OpenEditor.swift in Sources */, 553741F62E9372DF00AB56D0 /* NSMenu+File.swift in Sources */, 553741F72E9372DF00AB56D0 /* FilesWindowController+FileInfoBarDelegate.swift in Sources */, @@ -3603,6 +3606,7 @@ 553742AF2E9372DF00AB56D0 /* TouchController.swift in Sources */, 553742B02E9372DF00AB56D0 /* CompletionIndicator.swift in Sources */, 553742B12E9372DF00AB56D0 /* FoldersWindowController+UISetup.swift in Sources */, + 5577FFE82FAF2FC30084FB06 /* PopUpButtonURL.swift in Sources */, 553742B22E9372DF00AB56D0 /* NSTableView+Font.swift in Sources */, 553742B32E9372DF00AB56D0 /* ActionBarView.swift in Sources */, 553742B42E9372DF00AB56D0 /* TouchCompareItem.swift in Sources */,