diff --git a/Gas Mask.xcodeproj/project.pbxproj b/Gas Mask.xcodeproj/project.pbxproj index 94c13da..6203fdd 100644 --- a/Gas Mask.xcodeproj/project.pbxproj +++ b/Gas Mask.xcodeproj/project.pbxproj @@ -19,7 +19,6 @@ 3513A600113908A900AD789D /* Read Only.png in Resources */ = {isa = PBXBuildFile; fileRef = 3513A5FF113908A900AD789D /* Read Only.png */; }; 3513A61C11390B7900AD789D /* Error.m in Sources */ = {isa = PBXBuildFile; fileRef = 3513A61B11390B7900AD789D /* Error.m */; }; 351416CF28C3A7B80093A452 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 351416CE28C3A7B80093A452 /* Sparkle */; }; - AA1B2C3D4E5F000100000001 /* MASShortcut in Frameworks */ = {isa = PBXBuildFile; productRef = AA1B2C3D4E5F000200000001 /* MASShortcut */; }; 351D904510E76F7100CA6B5E /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 351D904110E76F7100CA6B5E /* Help */; }; 3522BEE0153316AA00035B90 /* ExtendedNSArray.m in Sources */ = {isa = PBXBuildFile; fileRef = 3522BEDF153316AA00035B90 /* ExtendedNSArray.m */; }; 3522BEE31533552200035B90 /* ExtendedNSPredicate.m in Sources */ = {isa = PBXBuildFile; fileRef = 3522BEE21533552200035B90 /* ExtendedNSPredicate.m */; }; @@ -86,7 +85,6 @@ 35B0499D1A462BC500EB89CA /* Blue Dot@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 35B0499C1A462BC500EB89CA /* Blue Dot@2x.png */; }; 35B36A641263B25A005F6A66 /* DebugUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 35B36A631263B25A005F6A66 /* DebugUtil.m */; }; 35C8D5A911144F7000B4242D /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 35C8D5A811144F7000B4242D /* Carbon.framework */; }; - BB1A2B3C4D5E000200000001 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB1A2B3C4D5E000100000001 /* ServiceManagement.framework */; }; 35D2C253113507A6007C8037 /* RemoteHostsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D2C252113507A6007C8037 /* RemoteHostsManager.m */; }; 35D3328E152F5A87001DA824 /* CombinedHosts.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D3328D152F5A87001DA824 /* CombinedHosts.m */; }; 35D33291152F5B7B001DA824 /* CombinedHostsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 35D33290152F5B7B001DA824 /* CombinedHostsController.m */; }; @@ -98,17 +96,12 @@ 35F414F4152E1B7800B99583 /* VDKQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 35F414F3152E1B7800B99583 /* VDKQueue.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 35FBCA3B1223172300860FDA /* RegexKitLite.m in Sources */ = {isa = PBXBuildFile; fileRef = 35FBCA3A1223172300860FDA /* RegexKitLite.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 35FBCA511223181000860FDA /* libicucore.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 35FBCA501223181000860FDA /* libicucore.dylib */; }; + 3CC80FFF3B4950D6CB6B71D1 /* EditorWindowPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E8D22B3228ABF7EBFDC08 /* EditorWindowPresenter.swift */; }; + 4367D8248BF62656D4AC86D1 /* EditorToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2C7D6281D5A0139260D12A /* EditorToolbar.swift */; }; + 7189711AD9756F2CBD4BCF22 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C64FE428AF00EB0DEEAAEBD /* EditorView.swift */; }; 8D11072B0486CEB800E47090 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 089C165CFE840E0CC02AAC07 /* InfoPlist.strings */; }; 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; - CC2B3C4D5E6F000100000040 /* NodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000010 /* NodeTests.m */; }; - CC2B3C4D5E6F000100000041 /* HostsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000011 /* HostsTests.m */; }; - CC2B3C4D5E6F000100000042 /* HostsGroupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000012 /* HostsGroupTests.m */; }; - CC2B3C4D5E6F000100000043 /* AbstractHostsControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000013 /* AbstractHostsControllerTests.m */; }; - CC2B3C4D5E6F000100000045 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; - CC2B3C4D5E6F000100000044 /* ApplicationControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000014 /* ApplicationControllerTests.m */; }; -/* End PBXBuildFile section */ - AA000004000000000000AAAA /* URLValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000003000000000000AAAA /* URLValidator.swift */; }; AA000006000000000000AAAA /* NetworkStatusObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000005000000000000AAAA /* NetworkStatusObserver.swift */; }; AA000008000000000000AAAA /* URLSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000007000000000000AAAA /* URLSheetView.swift */; }; @@ -117,33 +110,42 @@ AA00000E000000000000AAAA /* URLSheetPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */; }; AA000010000000000000AAAA /* URLSheetViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00000F000000000000AAAA /* URLSheetViewTests.swift */; }; AA000012000000000000AAAA /* HostsDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000011000000000000AAAA /* HostsDataStore.swift */; }; - 4367D8248BF62656D4AC86D1 /* EditorToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD2C7D6281D5A0139260D12A /* EditorToolbar.swift */; }; - DD2A255B82569B83ED993DC2 /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D979CFF69FCB07AB746051E /* StatusBarView.swift */; }; - 7189711AD9756F2CBD4BCF22 /* EditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C64FE428AF00EB0DEEAAEBD /* EditorView.swift */; }; - 3CC80FFF3B4950D6CB6B71D1 /* EditorWindowPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 682E8D22B3228ABF7EBFDC08 /* EditorWindowPresenter.swift */; }; AA000014000000000000AAAA /* HostsRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000013000000000000AAAA /* HostsRowView.swift */; }; AA000016000000000000AAAA /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000015000000000000AAAA /* SidebarView.swift */; }; AA00001A000000000000AAAA /* HostsDataStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000019000000000000AAAA /* HostsDataStoreTests.swift */; }; - AA000022000000000000AAAA /* HostsRowViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000021000000000000AAAA /* HostsRowViewTests.swift */; }; - EE000002000000000000EE01 /* EditorWindowPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE000001000000000000EE01 /* EditorWindowPresenterTests.swift */; }; AA00001C000000000000AAAA /* HostsTextViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */; }; AA00001E000000000000AAAA /* CombinedHostsPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */; }; AA000020000000000000AAAA /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA00001F000000000000AAAA /* ContentView.swift */; }; + AA000022000000000000AAAA /* HostsRowViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000021000000000000AAAA /* HostsRowViewTests.swift */; }; + AA000024000000000000AAAA /* HostsTextViewPerformanceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA000023000000000000AAAA /* HostsTextViewPerformanceTests.swift */; }; + AA1B2C3D4E5F000100000001 /* MASShortcut in Frameworks */ = {isa = PBXBuildFile; productRef = AA1B2C3D4E5F000200000001 /* MASShortcut */; }; + B09499173A8244AF5AE97A23 /* HostsDataStoreNotificationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313D8BA99EC0C9F97C402B58 /* HostsDataStoreNotificationTests.swift */; }; BB000002000000000000BBBB /* RemoteIntervalMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000001000000000000BBBB /* RemoteIntervalMapper.swift */; }; BB000004000000000000BBBB /* ShortcutRecorderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000003000000000000BBBB /* ShortcutRecorderView.swift */; }; - CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000CC01 /* GlobalShortcuts.swift */; }; BB000006000000000000BBBB /* SparkleObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000005000000000000BBBB /* SparkleObserver.swift */; }; BB000008000000000000BBBB /* LoginItemObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000007000000000000BBBB /* LoginItemObserver.swift */; }; BB00000A000000000000BBBB /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000009000000000000BBBB /* PreferencesView.swift */; }; BB00000C000000000000BBBB /* PreferencesPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB00000B000000000000BBBB /* PreferencesPresenter.swift */; }; - FF000002000000000000FF01 /* AboutBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF000001000000000000FF01 /* AboutBoxView.swift */; }; - FF000004000000000000FF01 /* AboutBoxPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF000003000000000000FF01 /* AboutBoxPresenter.swift */; }; BB00000E000000000000BBBB /* RemoteIntervalMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */; }; BB000010000000000000BBBB /* SparkleObserverTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB00000F000000000000BBBB /* SparkleObserverTests.swift */; }; BB000012000000000000BBBB /* PreferencesPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB000011000000000000BBBB /* PreferencesPresenterTests.swift */; }; + BB1A2B3C4D5E000200000001 /* ServiceManagement.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BB1A2B3C4D5E000100000001 /* ServiceManagement.framework */; }; + CC000002000000000000CC01 /* GlobalShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC000001000000000000CC01 /* GlobalShortcuts.swift */; }; + CC2B3C4D5E6F000100000040 /* NodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000010 /* NodeTests.m */; }; + CC2B3C4D5E6F000100000041 /* HostsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000011 /* HostsTests.m */; }; + CC2B3C4D5E6F000100000042 /* HostsGroupTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000012 /* HostsGroupTests.m */; }; + CC2B3C4D5E6F000100000043 /* AbstractHostsControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000013 /* AbstractHostsControllerTests.m */; }; + CC2B3C4D5E6F000100000044 /* ApplicationControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = CC2B3C4D5E6F000100000014 /* ApplicationControllerTests.m */; }; + CC2B3C4D5E6F000100000045 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; DD000002000000000000DD01 /* GlobalShortcutsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000001000000000000DD01 /* GlobalShortcutsTests.swift */; }; DD000004000000000000DD01 /* ShortcutRecorderViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */; }; + DD2A255B82569B83ED993DC2 /* StatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D979CFF69FCB07AB746051E /* StatusBarView.swift */; }; + EE000002000000000000EE01 /* EditorWindowPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE000001000000000000EE01 /* EditorWindowPresenterTests.swift */; }; + FF000002000000000000FF01 /* AboutBoxView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF000001000000000000FF01 /* AboutBoxView.swift */; }; + FF000004000000000000FF01 /* AboutBoxPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF000003000000000000FF01 /* AboutBoxPresenter.swift */; }; FF000006000000000000FF01 /* AboutBoxPresenterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF000005000000000000FF01 /* AboutBoxPresenterTests.swift */; }; +/* End PBXBuildFile section */ + /* Begin PBXContainerItemProxy section */ 353D18A01114C067005C4E54 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; @@ -182,6 +184,7 @@ 29B97316FDCFA39411CA2CEA /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Source/main.m; sourceTree = ""; }; 29B97324FDCFA39411CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; 29B97325FDCFA39411CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; + 313D8BA99EC0C9F97C402B58 /* HostsDataStoreNotificationTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HostsDataStoreNotificationTests.swift; sourceTree = ""; }; 350920B31226EB9F00ACA0F4 /* HostsDownloader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = HostsDownloader.h; path = Source/HostsDownloader.h; sourceTree = ""; }; 350920B41226EB9F00ACA0F4 /* HostsDownloader.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HostsDownloader.m; path = Source/HostsDownloader.m; sourceTree = ""; }; 350920BF1226ED5100ACA0F4 /* LocalHostsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = LocalHostsManager.h; path = Source/LocalHostsManager.h; sourceTree = ""; }; @@ -284,50 +287,12 @@ 3597135C110DED0F00C7ECAF /* HostsMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = HostsMenu.m; path = Source/HostsMenu.m; sourceTree = ""; }; 359967521656B52500BCF16D /* NotificationHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = NotificationHelper.h; path = Source/NotificationHelper.h; sourceTree = ""; }; 359967531656B52500BCF16D /* NotificationHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = NotificationHelper.m; path = Source/NotificationHelper.m; sourceTree = ""; }; - AA000002000000000000AAAA /* GasMask-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "GasMask-Bridging-Header.h"; path = "Source/Swift/GasMask-Bridging-Header.h"; sourceTree = ""; }; - AA000003000000000000AAAA /* URLValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLValidator.swift; path = "Source/Swift/URLValidator.swift"; sourceTree = ""; }; - AA000005000000000000AAAA /* NetworkStatusObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NetworkStatusObserver.swift; path = "Source/Swift/NetworkStatusObserver.swift"; sourceTree = ""; }; - AA000007000000000000AAAA /* URLSheetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLSheetView.swift; path = "Source/Swift/URLSheetView.swift"; sourceTree = ""; }; - AA000009000000000000AAAA /* URLSheetPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLSheetPresenter.swift; path = "Source/Swift/URLSheetPresenter.swift"; sourceTree = ""; }; - AA00000B000000000000AAAA /* URLValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLValidatorTests.swift; sourceTree = ""; }; - AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetPresenterTests.swift; sourceTree = ""; }; - AA00000F000000000000AAAA /* URLSheetViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetViewTests.swift; sourceTree = ""; }; - AA000011000000000000AAAA /* HostsDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsDataStore.swift; path = "Source/Swift/HostsDataStore.swift"; sourceTree = ""; }; - CD2C7D6281D5A0139260D12A /* EditorToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EditorToolbar.swift; path = "Source/Swift/EditorToolbar.swift"; sourceTree = ""; }; - 3D979CFF69FCB07AB746051E /* StatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StatusBarView.swift; path = "Source/Swift/StatusBarView.swift"; sourceTree = ""; }; - 7C64FE428AF00EB0DEEAAEBD /* EditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EditorView.swift; path = "Source/Swift/EditorView.swift"; sourceTree = ""; }; - 682E8D22B3228ABF7EBFDC08 /* EditorWindowPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EditorWindowPresenter.swift; path = "Source/Swift/EditorWindowPresenter.swift"; sourceTree = ""; }; - AA000013000000000000AAAA /* HostsRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsRowView.swift; path = "Source/Swift/HostsRowView.swift"; sourceTree = ""; }; - AA000015000000000000AAAA /* SidebarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SidebarView.swift; path = "Source/Swift/SidebarView.swift"; sourceTree = ""; }; - AA000019000000000000AAAA /* HostsDataStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsDataStoreTests.swift; sourceTree = ""; }; - AA000021000000000000AAAA /* HostsRowViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsRowViewTests.swift; sourceTree = ""; }; - EE000001000000000000EE01 /* EditorWindowPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorWindowPresenterTests.swift; sourceTree = ""; }; - AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsTextViewRepresentable.swift; path = "Source/Swift/HostsTextViewRepresentable.swift"; sourceTree = ""; }; - AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombinedHostsPickerView.swift; path = "Source/Swift/CombinedHostsPickerView.swift"; sourceTree = ""; }; - AA00001F000000000000AAAA /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContentView.swift; path = "Source/Swift/ContentView.swift"; sourceTree = ""; }; - 35A183A71A0ACF37002D6289 /* menuIcon@2x.tiff */ = {isa = PBXFileReference; lastKnownFileType = image.tiff; name = "menuIcon@2x.tiff"; path = "Resources/Images/menuIcon@2x.tiff"; sourceTree = ""; }; - BB000001000000000000BBBB /* RemoteIntervalMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RemoteIntervalMapper.swift; path = "Source/Swift/RemoteIntervalMapper.swift"; sourceTree = ""; }; - BB000003000000000000BBBB /* ShortcutRecorderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShortcutRecorderView.swift; path = "Source/Swift/ShortcutRecorderView.swift"; sourceTree = ""; }; - CC000001000000000000CC01 /* GlobalShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GlobalShortcuts.swift; path = "Source/Swift/GlobalShortcuts.swift"; sourceTree = ""; }; - BB000005000000000000BBBB /* SparkleObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SparkleObserver.swift; path = "Source/Swift/SparkleObserver.swift"; sourceTree = ""; }; - BB000007000000000000BBBB /* LoginItemObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginItemObserver.swift; path = "Source/Swift/LoginItemObserver.swift"; sourceTree = ""; }; - BB000009000000000000BBBB /* PreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesView.swift; path = "Source/Swift/PreferencesView.swift"; sourceTree = ""; }; - BB00000B000000000000BBBB /* PreferencesPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesPresenter.swift; path = "Source/Swift/PreferencesPresenter.swift"; sourceTree = ""; }; - FF000001000000000000FF01 /* AboutBoxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AboutBoxView.swift; path = "Source/Swift/AboutBoxView.swift"; sourceTree = ""; }; - FF000003000000000000FF01 /* AboutBoxPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AboutBoxPresenter.swift; path = "Source/Swift/AboutBoxPresenter.swift"; sourceTree = ""; }; - BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteIntervalMapperTests.swift; sourceTree = ""; }; - BB00000F000000000000BBBB /* SparkleObserverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SparkleObserverTests.swift; sourceTree = ""; }; - BB000011000000000000BBBB /* PreferencesPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPresenterTests.swift; sourceTree = ""; }; - DD000001000000000000DD01 /* GlobalShortcutsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalShortcutsTests.swift; sourceTree = ""; }; - DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutRecorderViewTests.swift; sourceTree = ""; }; - FF000005000000000000FF01 /* AboutBoxPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutBoxPresenterTests.swift; sourceTree = ""; }; 35A4CD2F1534927F005176BD /* Combined Hosts Hint.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Combined Hosts Hint.png"; path = "Resources/Images/Combined Hosts Hint.png"; sourceTree = ""; }; 35B0499A1A462AE900EB89CA /* Activated@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Activated@2x.png"; path = "Resources/Images/Activated@2x.png"; sourceTree = ""; }; 35B0499C1A462BC500EB89CA /* Blue Dot@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "Blue Dot@2x.png"; path = "Resources/Images/Blue Dot@2x.png"; sourceTree = ""; }; 35B36A621263B25A005F6A66 /* DebugUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = DebugUtil.h; path = Source/DebugUtil.h; sourceTree = ""; }; 35B36A631263B25A005F6A66 /* DebugUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = DebugUtil.m; path = Source/DebugUtil.m; sourceTree = ""; }; 35C8D5A811144F7000B4242D /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = /System/Library/Frameworks/Carbon.framework; sourceTree = ""; }; - BB1A2B3C4D5E000100000001 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; 35D2C251113507A6007C8037 /* RemoteHostsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RemoteHostsManager.h; path = Source/RemoteHostsManager.h; sourceTree = ""; }; 35D2C252113507A6007C8037 /* RemoteHostsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = RemoteHostsManager.m; path = Source/RemoteHostsManager.m; sourceTree = ""; }; 35D3328C152F5A87001DA824 /* CombinedHosts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CombinedHosts.h; path = Source/CombinedHosts.h; sourceTree = ""; }; @@ -349,14 +314,52 @@ 35FBCA501223181000860FDA /* libicucore.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libicucore.dylib; path = usr/lib/libicucore.dylib; sourceTree = SDKROOT; }; 35FC65991114B12600BD18C3 /* launcher.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = launcher.m; path = Source/launcher.m; sourceTree = ""; }; 35FC65A71114B29A00BD18C3 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; + 3D979CFF69FCB07AB746051E /* StatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = StatusBarView.swift; path = Source/Swift/StatusBarView.swift; sourceTree = ""; }; + 682E8D22B3228ABF7EBFDC08 /* EditorWindowPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EditorWindowPresenter.swift; path = Source/Swift/EditorWindowPresenter.swift; sourceTree = ""; }; + 7C64FE428AF00EB0DEEAAEBD /* EditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EditorView.swift; path = Source/Swift/EditorView.swift; sourceTree = ""; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; explicitFileType = text.plist.info; fileEncoding = 4; path = Info.plist; sourceTree = ""; }; 8D1107320486CEB800E47090 /* Gas Mask.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Gas Mask.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + AA000002000000000000AAAA /* GasMask-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "GasMask-Bridging-Header.h"; path = "Source/Swift/GasMask-Bridging-Header.h"; sourceTree = ""; }; + AA000003000000000000AAAA /* URLValidator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLValidator.swift; path = Source/Swift/URLValidator.swift; sourceTree = ""; }; + AA000005000000000000AAAA /* NetworkStatusObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NetworkStatusObserver.swift; path = Source/Swift/NetworkStatusObserver.swift; sourceTree = ""; }; + AA000007000000000000AAAA /* URLSheetView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLSheetView.swift; path = Source/Swift/URLSheetView.swift; sourceTree = ""; }; + AA000009000000000000AAAA /* URLSheetPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = URLSheetPresenter.swift; path = Source/Swift/URLSheetPresenter.swift; sourceTree = ""; }; + AA00000B000000000000AAAA /* URLValidatorTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLValidatorTests.swift; sourceTree = ""; }; + AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetPresenterTests.swift; sourceTree = ""; }; + AA00000F000000000000AAAA /* URLSheetViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheetViewTests.swift; sourceTree = ""; }; + AA000011000000000000AAAA /* HostsDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsDataStore.swift; path = Source/Swift/HostsDataStore.swift; sourceTree = ""; }; + AA000013000000000000AAAA /* HostsRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsRowView.swift; path = Source/Swift/HostsRowView.swift; sourceTree = ""; }; + AA000015000000000000AAAA /* SidebarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SidebarView.swift; path = Source/Swift/SidebarView.swift; sourceTree = ""; }; + AA000019000000000000AAAA /* HostsDataStoreTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsDataStoreTests.swift; sourceTree = ""; }; + AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HostsTextViewRepresentable.swift; path = Source/Swift/HostsTextViewRepresentable.swift; sourceTree = ""; }; + AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CombinedHostsPickerView.swift; path = Source/Swift/CombinedHostsPickerView.swift; sourceTree = ""; }; + AA00001F000000000000AAAA /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ContentView.swift; path = Source/Swift/ContentView.swift; sourceTree = ""; }; + AA000021000000000000AAAA /* HostsRowViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsRowViewTests.swift; sourceTree = ""; }; + AA000023000000000000AAAA /* HostsTextViewPerformanceTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HostsTextViewPerformanceTests.swift; sourceTree = ""; }; + BB000001000000000000BBBB /* RemoteIntervalMapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RemoteIntervalMapper.swift; path = Source/Swift/RemoteIntervalMapper.swift; sourceTree = ""; }; + BB000003000000000000BBBB /* ShortcutRecorderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ShortcutRecorderView.swift; path = Source/Swift/ShortcutRecorderView.swift; sourceTree = ""; }; + BB000005000000000000BBBB /* SparkleObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SparkleObserver.swift; path = Source/Swift/SparkleObserver.swift; sourceTree = ""; }; + BB000007000000000000BBBB /* LoginItemObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LoginItemObserver.swift; path = Source/Swift/LoginItemObserver.swift; sourceTree = ""; }; + BB000009000000000000BBBB /* PreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesView.swift; path = Source/Swift/PreferencesView.swift; sourceTree = ""; }; + BB00000B000000000000BBBB /* PreferencesPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PreferencesPresenter.swift; path = Source/Swift/PreferencesPresenter.swift; sourceTree = ""; }; + BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RemoteIntervalMapperTests.swift; sourceTree = ""; }; + BB00000F000000000000BBBB /* SparkleObserverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SparkleObserverTests.swift; sourceTree = ""; }; + BB000011000000000000BBBB /* PreferencesPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreferencesPresenterTests.swift; sourceTree = ""; }; + BB1A2B3C4D5E000100000001 /* ServiceManagement.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ServiceManagement.framework; path = System/Library/Frameworks/ServiceManagement.framework; sourceTree = SDKROOT; }; + CC000001000000000000CC01 /* GlobalShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = GlobalShortcuts.swift; path = Source/Swift/GlobalShortcuts.swift; sourceTree = ""; }; CC2B3C4D5E6F000100000010 /* NodeTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NodeTests.m; sourceTree = ""; }; CC2B3C4D5E6F000100000011 /* HostsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostsTests.m; sourceTree = ""; }; CC2B3C4D5E6F000100000012 /* HostsGroupTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HostsGroupTests.m; sourceTree = ""; }; CC2B3C4D5E6F000100000013 /* AbstractHostsControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbstractHostsControllerTests.m; sourceTree = ""; }; CC2B3C4D5E6F000100000014 /* ApplicationControllerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ApplicationControllerTests.m; sourceTree = ""; }; CC2B3C4D5E6F000100000020 /* Gas Mask Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Gas Mask Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + CD2C7D6281D5A0139260D12A /* EditorToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = EditorToolbar.swift; path = Source/Swift/EditorToolbar.swift; sourceTree = ""; }; + DD000001000000000000DD01 /* GlobalShortcutsTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalShortcutsTests.swift; sourceTree = ""; }; + DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutRecorderViewTests.swift; sourceTree = ""; }; + EE000001000000000000EE01 /* EditorWindowPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EditorWindowPresenterTests.swift; sourceTree = ""; }; + FF000001000000000000FF01 /* AboutBoxView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AboutBoxView.swift; path = Source/Swift/AboutBoxView.swift; sourceTree = ""; }; + FF000003000000000000FF01 /* AboutBoxPresenter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AboutBoxPresenter.swift; path = Source/Swift/AboutBoxPresenter.swift; sourceTree = ""; }; + FF000005000000000000FF01 /* AboutBoxPresenterTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutBoxPresenterTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -438,7 +441,7 @@ name = Products; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA /* Gas Mask */ = { + 29B97314FDCFA39411CA2CEA = { isa = PBXGroup; children = ( 35D4C98210E64D8800B9F63A /* Extended Next Step */, @@ -696,45 +699,6 @@ name = "Remote Hosts"; sourceTree = ""; }; - AA000001000000000000AAAA /* Swift */ = { - isa = PBXGroup; - children = ( - AA000002000000000000AAAA /* GasMask-Bridging-Header.h */, - CC000001000000000000CC01 /* GlobalShortcuts.swift */, - AA000003000000000000AAAA /* URLValidator.swift */, - AA000005000000000000AAAA /* NetworkStatusObserver.swift */, - AA000007000000000000AAAA /* URLSheetView.swift */, - AA000009000000000000AAAA /* URLSheetPresenter.swift */, - FF000001000000000000FF01 /* AboutBoxView.swift */, - FF000003000000000000FF01 /* AboutBoxPresenter.swift */, - AA000011000000000000AAAA /* HostsDataStore.swift */, - CD2C7D6281D5A0139260D12A /* EditorToolbar.swift */, - 3D979CFF69FCB07AB746051E /* StatusBarView.swift */, - 7C64FE428AF00EB0DEEAAEBD /* EditorView.swift */, - 682E8D22B3228ABF7EBFDC08 /* EditorWindowPresenter.swift */, - AA000013000000000000AAAA /* HostsRowView.swift */, - AA000015000000000000AAAA /* SidebarView.swift */, - AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */, - AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */, - AA00001F000000000000AAAA /* ContentView.swift */, - BB000020000000000000BBBB /* Preferences */, - ); - name = Swift; - sourceTree = ""; - }; - BB000020000000000000BBBB /* Preferences */ = { - isa = PBXGroup; - children = ( - BB000009000000000000BBBB /* PreferencesView.swift */, - BB00000B000000000000BBBB /* PreferencesPresenter.swift */, - BB000001000000000000BBBB /* RemoteIntervalMapper.swift */, - BB000003000000000000BBBB /* ShortcutRecorderView.swift */, - BB000005000000000000BBBB /* SparkleObserver.swift */, - BB000007000000000000BBBB /* LoginItemObserver.swift */, - ); - name = Preferences; - sourceTree = ""; - }; 35D3328B152F5A45001DA824 /* Combined Hosts */ = { isa = PBXGroup; children = ( @@ -783,40 +747,81 @@ name = "3rd Party"; sourceTree = ""; }; + AA000001000000000000AAAA /* Swift */ = { + isa = PBXGroup; + children = ( + AA000002000000000000AAAA /* GasMask-Bridging-Header.h */, + CC000001000000000000CC01 /* GlobalShortcuts.swift */, + AA000003000000000000AAAA /* URLValidator.swift */, + AA000005000000000000AAAA /* NetworkStatusObserver.swift */, + AA000007000000000000AAAA /* URLSheetView.swift */, + AA000009000000000000AAAA /* URLSheetPresenter.swift */, + FF000001000000000000FF01 /* AboutBoxView.swift */, + FF000003000000000000FF01 /* AboutBoxPresenter.swift */, + AA000011000000000000AAAA /* HostsDataStore.swift */, + CD2C7D6281D5A0139260D12A /* EditorToolbar.swift */, + 3D979CFF69FCB07AB746051E /* StatusBarView.swift */, + 7C64FE428AF00EB0DEEAAEBD /* EditorView.swift */, + 682E8D22B3228ABF7EBFDC08 /* EditorWindowPresenter.swift */, + AA000013000000000000AAAA /* HostsRowView.swift */, + AA000015000000000000AAAA /* SidebarView.swift */, + AA00001B000000000000AAAA /* HostsTextViewRepresentable.swift */, + AA00001D000000000000AAAA /* CombinedHostsPickerView.swift */, + AA00001F000000000000AAAA /* ContentView.swift */, + BB000020000000000000BBBB /* Preferences */, + ); + name = Swift; + sourceTree = ""; + }; + BB000020000000000000BBBB /* Preferences */ = { + isa = PBXGroup; + children = ( + BB000009000000000000BBBB /* PreferencesView.swift */, + BB00000B000000000000BBBB /* PreferencesPresenter.swift */, + BB000001000000000000BBBB /* RemoteIntervalMapper.swift */, + BB000003000000000000BBBB /* ShortcutRecorderView.swift */, + BB000005000000000000BBBB /* SparkleObserver.swift */, + BB000007000000000000BBBB /* LoginItemObserver.swift */, + ); + name = Preferences; + sourceTree = ""; + }; + CC2B3C4D5E6F000100000030 /* Tests */ = { + isa = PBXGroup; + children = ( + CC2B3C4D5E6F000100000031 /* GasMaskTests */, + ); + name = Tests; + path = Tests; + sourceTree = ""; + }; CC2B3C4D5E6F000100000031 /* GasMaskTests */ = { isa = PBXGroup; children = ( - AA00000B000000000000AAAA /* URLValidatorTests.swift */, + AA00000B000000000000AAAA /* URLValidatorTests.swift */, AA00000D000000000000AAAA /* URLSheetPresenterTests.swift */, AA00000F000000000000AAAA /* URLSheetViewTests.swift */, AA000019000000000000AAAA /* HostsDataStoreTests.swift */, + AA000023000000000000AAAA /* HostsTextViewPerformanceTests.swift */, AA000021000000000000AAAA /* HostsRowViewTests.swift */, EE000001000000000000EE01 /* EditorWindowPresenterTests.swift */, - BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */, - BB00000F000000000000BBBB /* SparkleObserverTests.swift */, - BB000011000000000000BBBB /* PreferencesPresenterTests.swift */, - DD000001000000000000DD01 /* GlobalShortcutsTests.swift */, - DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */, - FF000005000000000000FF01 /* AboutBoxPresenterTests.swift */, + BB00000D000000000000BBBB /* RemoteIntervalMapperTests.swift */, + BB00000F000000000000BBBB /* SparkleObserverTests.swift */, + BB000011000000000000BBBB /* PreferencesPresenterTests.swift */, + DD000001000000000000DD01 /* GlobalShortcutsTests.swift */, + DD000003000000000000DD01 /* ShortcutRecorderViewTests.swift */, + FF000005000000000000FF01 /* AboutBoxPresenterTests.swift */, CC2B3C4D5E6F000100000010 /* NodeTests.m */, CC2B3C4D5E6F000100000011 /* HostsTests.m */, CC2B3C4D5E6F000100000012 /* HostsGroupTests.m */, CC2B3C4D5E6F000100000013 /* AbstractHostsControllerTests.m */, CC2B3C4D5E6F000100000014 /* ApplicationControllerTests.m */, + 313D8BA99EC0C9F97C402B58 /* HostsDataStoreNotificationTests.swift */, ); name = GasMaskTests; path = GasMaskTests; sourceTree = ""; }; - CC2B3C4D5E6F000100000030 /* Tests */ = { - isa = PBXGroup; - children = ( - CC2B3C4D5E6F000100000031 /* GasMaskTests */, - ); - name = Tests; - path = Tests; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -898,7 +903,7 @@ English, en, ); - mainGroup = 29B97314FDCFA39411CA2CEA /* Gas Mask */; + mainGroup = 29B97314FDCFA39411CA2CEA; packageReferences = ( 351416CD28C3A7B80093A452 /* XCRemoteSwiftPackageReference "Sparkle" */, AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "MASShortcut" */, @@ -1074,6 +1079,7 @@ AA00000E000000000000AAAA /* URLSheetPresenterTests.swift in Sources */, AA000010000000000000AAAA /* URLSheetViewTests.swift in Sources */, AA00001A000000000000AAAA /* HostsDataStoreTests.swift in Sources */, + AA000024000000000000AAAA /* HostsTextViewPerformanceTests.swift in Sources */, AA000022000000000000AAAA /* HostsRowViewTests.swift in Sources */, EE000002000000000000EE01 /* EditorWindowPresenterTests.swift in Sources */, BB00000E000000000000BBBB /* RemoteIntervalMapperTests.swift in Sources */, @@ -1082,6 +1088,7 @@ DD000002000000000000DD01 /* GlobalShortcutsTests.swift in Sources */, DD000004000000000000DD01 /* ShortcutRecorderViewTests.swift in Sources */, FF000006000000000000FF01 /* AboutBoxPresenterTests.swift in Sources */, + B09499173A8244AF5AE97A23 /* HostsDataStoreNotificationTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1126,9 +1133,9 @@ ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_IDENTITY = ""; CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; GCC_DYNAMIC_NO_PIC = NO; @@ -1162,9 +1169,9 @@ ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_IDENTITY = ""; CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -1197,11 +1204,11 @@ buildSettings = { ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_IDENTITY = ""; CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_REQUIRED = NO; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -1233,11 +1240,11 @@ buildSettings = { ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES; ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_ARC = YES; - CODE_SIGN_IDENTITY = ""; CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_REQUIRED = NO; - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = ""; COMBINE_HIDPI_IMAGES = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; FRAMEWORK_SEARCH_PATHS = ( @@ -1333,9 +1340,9 @@ isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = ""; CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", @@ -1343,7 +1350,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = Gas_Mask_Prefix.pch; MACOSX_DEPLOYMENT_TARGET = 13.0; - PRODUCT_BUNDLE_IDENTIFIER = "ee.clockwise.gmask.tests"; + PRODUCT_BUNDLE_IDENTIFIER = ee.clockwise.gmask.tests; PRODUCT_NAME = "Gas Mask Tests"; SDKROOT = macosx; SWIFT_VERSION = 5.9; @@ -1355,9 +1362,9 @@ isa = XCBuildConfiguration; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; - CODE_SIGN_IDENTITY = ""; CODE_SIGNING_ALLOWED = NO; CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_IDENTITY = ""; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PLATFORM_DIR)/Developer/Library/Frameworks", @@ -1365,7 +1372,7 @@ GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = Gas_Mask_Prefix.pch; MACOSX_DEPLOYMENT_TARGET = 13.0; - PRODUCT_BUNDLE_IDENTIFIER = "ee.clockwise.gmask.tests"; + PRODUCT_BUNDLE_IDENTIFIER = ee.clockwise.gmask.tests; PRODUCT_NAME = "Gas Mask Tests"; SDKROOT = macosx; SWIFT_VERSION = 5.9; diff --git a/Source/HostsDownloader.m b/Source/HostsDownloader.m index 2023519..b52c4c7 100644 --- a/Source/HostsDownloader.m +++ b/Source/HostsDownloader.m @@ -133,40 +133,48 @@ @implementation HostsDownloader (Private) - (void)notifyDelegateHostsUpToDate { - SEL selector = @selector(hostsUpToDate:); - if (delegate && [delegate respondsToSelector:selector]) { - SuppressPerformSelectorLeakWarning( - [delegate performSelector:selector withObject:self]); - } + dispatch_async(dispatch_get_main_queue(), ^{ + SEL selector = @selector(hostsUpToDate:); + if (delegate && [delegate respondsToSelector:selector]) { + SuppressPerformSelectorLeakWarning( + [delegate performSelector:selector withObject:self]); + } + }); } - (void)notifyDelegateDownloadingStarted { - SEL selector = @selector(hostsDownloadingStarted:); - if (delegate && [delegate respondsToSelector:selector]) { - SuppressPerformSelectorLeakWarning( - [delegate performSelector:selector withObject:self]); - } + dispatch_async(dispatch_get_main_queue(), ^{ + SEL selector = @selector(hostsDownloadingStarted:); + if (delegate && [delegate respondsToSelector:selector]) { + SuppressPerformSelectorLeakWarning( + [delegate performSelector:selector withObject:self]); + } + }); } - (void)notifyDelegateDownloaded { - logDebug(@"Downloading complete: %@", url); - - SEL selector = @selector(hostsDownloaded:); - if (delegate && [delegate respondsToSelector:selector]) { - SuppressPerformSelectorLeakWarning( - [delegate performSelector:selector withObject:self]); - } + dispatch_async(dispatch_get_main_queue(), ^{ + logDebug(@"Downloading complete: %@", url); + + SEL selector = @selector(hostsDownloaded:); + if (delegate && [delegate respondsToSelector:selector]) { + SuppressPerformSelectorLeakWarning( + [delegate performSelector:selector withObject:self]); + } + }); } - (void)notifyDelegateDownloadFailed { - SEL selector = @selector(hostsDownloadFailed:); - if (delegate && [delegate respondsToSelector:selector]) { - SuppressPerformSelectorLeakWarning( - [delegate performSelector:selector withObject:self]); - } + dispatch_async(dispatch_get_main_queue(), ^{ + SEL selector = @selector(hostsDownloadFailed:); + if (delegate && [delegate respondsToSelector:selector]) { + SuppressPerformSelectorLeakWarning( + [delegate performSelector:selector withObject:self]); + } + }); } #pragma mark - diff --git a/Source/HostsTextView.h b/Source/HostsTextView.h index 0ba80eb..91a90ac 100644 --- a/Source/HostsTextView.h +++ b/Source/HostsTextView.h @@ -19,6 +19,8 @@ ***************************************************************************/ +NS_ASSUME_NONNULL_BEGIN + @interface HostsTextView : NSTextView { @private BOOL syntaxHighlighting; @@ -27,11 +29,17 @@ NSColor *textColor; NSColor *commentColor; NSCharacterSet *nameCharacterSet; + NSUInteger _highlightGeneration; + BOOL _replacingContent; } - (void)setSyntaxHighlighting:(BOOL)value; - (BOOL)syntaxHighlighting; +- (void)cancelPendingHighlighting; +- (void)replaceContentWith:(NSString *)newContent; -+ (instancetype)createForProgrammaticUse; ++ (nullable instancetype)createForProgrammaticUse; @end + +NS_ASSUME_NONNULL_END diff --git a/Source/HostsTextView.m b/Source/HostsTextView.m index 1f455f1..9c8d6e1 100644 --- a/Source/HostsTextView.m +++ b/Source/HostsTextView.m @@ -22,8 +22,12 @@ #import "ExtendedNSString.h" #import "IP.h" +#define kAsyncHighlightThreshold 50000 +#define kHighlightChunkSize 100000 + @interface HostsTextView (HighLight) --(void)colorText: (NSTextStorage*)textStorage; +-(void)colorTextInRange:(NSRange)range; +-(void)highlightAsyncFrom:(NSUInteger)start generation:(NSUInteger)generation; -(void)removeColors; -(void)removeMarks: (NSTextStorage*)textStorage range: (NSRange)range; -(void)markComment: (NSTextStorage*)textStorage range:(NSRange)range; @@ -118,9 +122,19 @@ -(void)setSyntaxHighlighting:(BOOL)value { syntaxHighlighting = value; if (syntaxHighlighting) { - [self colorText:[self textStorage]]; + NSUInteger length = [[[self textStorage] string] length]; + if (length > kAsyncHighlightThreshold) { + _highlightGeneration++; + [self highlightAsyncFrom:0 generation:_highlightGeneration]; + } else if (length > 0) { + NSTextStorage *ts = [self textStorage]; + [ts beginEditing]; + [self colorTextInRange:NSMakeRange(0, length)]; + [ts endEditing]; + } } else { + _highlightGeneration++; [self removeColors]; } } @@ -129,45 +143,74 @@ -(BOOL)syntaxHighlighting return syntaxHighlighting; } +-(void)cancelPendingHighlighting +{ + _highlightGeneration++; +} + +-(void)replaceContentWith:(NSString *)newContent +{ + // Cancel any pending async highlighting from a previous call + _highlightGeneration++; + _replacingContent = YES; + [self setString:newContent]; + _replacingContent = NO; + + if (syntaxHighlighting) { + NSUInteger length = [[self string] length]; + if (length > kAsyncHighlightThreshold) { + _highlightGeneration++; + NSUInteger generation = _highlightGeneration; + dispatch_async(dispatch_get_main_queue(), ^{ + [self highlightAsyncFrom:0 generation:generation]; + }); + } else if (length > 0) { + NSTextStorage *ts = [self textStorage]; + [ts beginEditing]; + [self colorTextInRange:NSMakeRange(0, length)]; + [ts endEditing]; + } + } +} + @end @implementation HostsTextView (HighLight) --(void)colorText: (NSTextStorage*)textStorage +-(void)colorTextInRange:(NSRange)range { - - NSRange range = [self changedLinesRange: textStorage]; + NSTextStorage *textStorage = [self textStorage]; NSString *contents = [[textStorage string] substringWithRange:range]; - + if ([contents length] == 0) { return; } - + [self removeMarks: textStorage range: range]; - - NSArray *array = [contents componentsSeparatedByString: @"\n"]; // 37ms - + + NSArray *array = [contents componentsSeparatedByString: @"\n"]; + int pos = 0; IP *ip = [[IP alloc] initWithString:contents]; - + for (NSString *line in array) { NSRange ipRange = NSMakeRange(NSNotFound, NSNotFound); NSRange namesRange = NSMakeRange(NSNotFound, NSNotFound); - + int i; for (i=0; i<[line length]; i++) { unichar character = [line characterAtIndex:i]; // Start of comment if (character == '#') { - + // End of names if (namesRange.location != NSNotFound) { namesRange.length = pos-namesRange.location; } - + NSRange range2 = NSMakeRange(range.location+pos, [line length]-i); [self markComment:textStorage range:range2]; - + pos += [line length]-i; break; } @@ -185,16 +228,16 @@ -(void)colorText: (NSTextStorage*)textStorage } pos++; } - + if (ipRange.location != NSNotFound && ipRange.length == NSNotFound) { ipRange.length = pos-ipRange.location; } - + if (ipRange.location != NSNotFound && ipRange.length != NSNotFound) { [ip setRange:ipRange]; - + ipRange.location += range.location; - + if ([ip isValid]) { if ([ip isVersion4]) { [self markIPv4:textStorage range:ipRange]; @@ -206,27 +249,27 @@ -(void)colorText: (NSTextStorage*)textStorage else { NSRange badIPRange = [ip invalidRange]; badIPRange.location += ipRange.location; - + [self markInvalid: textStorage range:badIPRange]; } } - + if (namesRange.length == NSNotFound) { namesRange.length = pos-namesRange.location; - + if ([line hasSuffix:@"\r"]) { namesRange.length--; } } // Color names if (namesRange.location != NSNotFound) { - + NSRange nameRange = NSMakeRange(namesRange.location, NSNotFound); - + int end = NSMaxRange(namesRange); for (int i=namesRange.location; i 0) { if (![self validName:contents range:nameRange]) { [self markInvalid:textStorage range:NSMakeRange(range.location+nameRange.location, nameRange.length)]; @@ -248,12 +291,45 @@ -(void)colorText: (NSTextStorage*)textStorage } } } - + // Move over newline pos++; } } +-(void)highlightAsyncFrom:(NSUInteger)start generation:(NSUInteger)generation +{ + if (generation != _highlightGeneration) return; + if (!syntaxHighlighting) return; + + NSTextStorage *textStorage = [self textStorage]; + NSString *string = [textStorage string]; + NSUInteger totalLength = [string length]; + + if (start >= totalLength) return; + + NSUInteger end = MIN(start + kHighlightChunkSize, totalLength); + + // Extend to line boundary + if (end < totalLength) { + NSRange lineRange = [string lineRangeForRange:NSMakeRange(end, 0)]; + end = NSMaxRange(lineRange); + } + + NSRange range = NSMakeRange(start, end - start); + + [textStorage beginEditing]; + [self colorTextInRange:range]; + [textStorage endEditing]; + + if (end < totalLength && generation == _highlightGeneration) { + NSUInteger nextStart = end; + dispatch_async(dispatch_get_main_queue(), ^{ + [self highlightAsyncFrom:nextStart generation:generation]; + }); + } +} + -(void)removeColors { NSTextStorage *textStorage = [self textStorage]; @@ -340,9 +416,26 @@ -(BOOL)validName:(NSString*)contents range:(NSRange)nameRange - (void)textStorageDidProcessEditing:(NSNotification *)notification { - if (syntaxHighlighting && [[self textStorage] editedMask] != NSTextStorageEditedAttributes) { - [self colorText:[notification object]]; + if (_replacingContent) return; + if (!syntaxHighlighting) return; + if ([[self textStorage] editedMask] == NSTextStorageEditedAttributes) return; + + // Cancel any pending async highlighting (text has changed) + _highlightGeneration++; + + NSTextStorage *textStorage = [notification object]; + NSRange range = [self changedLinesRange:textStorage]; + + // For large edits (bulk string replacement), use async chunked highlighting + if (range.length > kAsyncHighlightThreshold) { + NSUInteger generation = _highlightGeneration; + dispatch_async(dispatch_get_main_queue(), ^{ + [self highlightAsyncFrom:0 generation:generation]; + }); + return; } + + [self colorTextInRange:range]; } @end diff --git a/Source/RemoteHostsManager.m b/Source/RemoteHostsManager.m index 01e46c5..5840ad5 100644 --- a/Source/RemoteHostsManager.m +++ b/Source/RemoteHostsManager.m @@ -117,38 +117,36 @@ - (void)hostsDownloaded:(HostsDownloader*)downloader { logDebug(@"Remote hosts downloaded"); [self decreaseActiveDownloadsCount]; - + RemoteHosts *hosts = (RemoteHosts*)[downloader hosts]; [hosts setEnabled:YES]; [hosts setContents:[downloader response]]; [hosts setUpdated:[NSDate date]]; - + if ([downloader lastModified]) { [hosts setLastModified:[downloader lastModified]]; } - + if ([downloader error]) { if ([[downloader error] type] == FileNotFound && (![hosts error] || [[hosts error] type] != FileNotFound)) { [NotificationHelper notify:@"Failed to Download" message:[NSString stringWithFormat:@"Remote hosts file \"%@\" not found on the remote server.", [hosts name]]]; } - + [hosts setError:[downloader error]]; } else { [hosts setError:nil]; } - + [hostsController saveHosts:hosts]; - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - [nc postNotificationName:HostsFileSavedNotification object:hosts]; - + if (![downloader initialLoad] && ![downloader error]) { [NotificationHelper notify:@"Hosts File Updated" message:[hosts name]]; } - + [self removeDownloader:downloader]; - + [self saveRemoteHostsProperties]; } diff --git a/Source/Swift/ContentView.swift b/Source/Swift/ContentView.swift index 83ecfb2..61d98e3 100644 --- a/Source/Swift/ContentView.swift +++ b/Source/Swift/ContentView.swift @@ -9,7 +9,13 @@ struct ContentView: View { CombinedHostsPickerView(store: store) Divider() } - HostsTextViewRepresentable(store: store) + HostsTextViewRepresentable( + selectedHosts: store.selectedHosts, + contentToken: store.rowRefreshToken, + onTextChange: { [weak store] newText in + store?.selectedHosts?.setContents(newText) + } + ) } } } diff --git a/Source/Swift/HostsDataStore.swift b/Source/Swift/HostsDataStore.swift index ec298a5..8bea586 100644 --- a/Source/Swift/HostsDataStore.swift +++ b/Source/Swift/HostsDataStore.swift @@ -45,6 +45,7 @@ final class HostsDataStore: ObservableObject { private var notificationObservers: [NSObjectProtocol] = [] private var isSyncingSelection = false private var busyCount = 0 + private var pendingRowRefresh = false // MARK: Init @@ -86,6 +87,24 @@ final class HostsDataStore: ObservableObject { isSyncingSelection = false } + // MARK: Row Refresh Coalescing + + /// Coalesces multiple rapid row-refresh notifications (e.g. from a download + /// lifecycle that posts 9-12 notifications) into a single `objectWillChange` + /// signal, so SwiftUI performs one re-render instead of many. + /// Thread safety: Both the observer callbacks (queue: .main) and the + /// DispatchQueue.main.async block execute on the main thread, so + /// `pendingRowRefresh` access is serialized without explicit locking. + private func scheduleRowRefresh() { + guard !pendingRowRefresh else { return } + pendingRowRefresh = true + DispatchQueue.main.async { [weak self] in + guard let self else { return } + self.pendingRowRefresh = false + self.rowRefreshToken &+= 1 + } + } + // MARK: Notification Observers private func observeNotifications() { @@ -126,8 +145,7 @@ final class HostsDataStore: ObservableObject { for name in rowRefreshNames { let observer = nc.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in - guard let self else { return } - self.rowRefreshToken &+= 1 + self?.scheduleRowRefresh() } notificationObservers.append(observer) } diff --git a/Source/Swift/HostsTextViewRepresentable.swift b/Source/Swift/HostsTextViewRepresentable.swift index 06d6282..bfdd58d 100644 --- a/Source/Swift/HostsTextViewRepresentable.swift +++ b/Source/Swift/HostsTextViewRepresentable.swift @@ -1,7 +1,9 @@ import SwiftUI struct HostsTextViewRepresentable: NSViewRepresentable { - @ObservedObject var store: HostsDataStore + let selectedHosts: Hosts? + let contentToken: UInt64 + let onTextChange: (String) -> Void @AppStorage("syntaxHighlighting") private var syntaxHighlighting = true func makeNSView(context: Context) -> NSScrollView { @@ -24,14 +26,36 @@ struct HostsTextViewRepresentable: NSViewRepresentable { func updateNSView(_ scrollView: NSScrollView, context: Context) { guard let textView = context.coordinator.textView else { return } - let contents = store.selectedHosts?.contents() ?? "" - if textView.string != contents { + let contents = selectedHosts?.contents() ?? "" + + if selectedHosts !== context.coordinator.lastUpdatedHosts { + // Selection changed — always replace (O(1) check + O(n) replacement) context.coordinator.isUpdatingFromModel = true - textView.string = contents - context.coordinator.isUpdatingFromModel = false + defer { context.coordinator.isUpdatingFromModel = false } + textView.replaceContent(with: contents) + context.coordinator.lastUpdatedHosts = selectedHosts + context.coordinator.lastContentToken = contentToken + } else if contentToken != context.coordinator.lastContentToken { + // Token changed — external content update (download, save, sync) or user edit. + // User edits also trigger this path (textDidChange → setContents → setSaved:NO + // → HostsNodeNeedsUpdate → rowRefreshToken++), but the text view already contains + // the correct content, so the comparison below will find equality and skip. + // For editable files (local/combined, typically small), this O(n) check is cheap. + context.coordinator.lastContentToken = contentToken + let currentLength = (textView.string as NSString).length + let newLength = (contents as NSString).length + if currentLength != newLength || textView.string != contents { + context.coordinator.isUpdatingFromModel = true + defer { context.coordinator.isUpdatingFromModel = false } + textView.replaceContent(with: contents) + } } + // If neither selection nor token changed, skip entirely — O(1) - textView.isEditable = store.selectedHosts?.editable ?? false + let isEditable = selectedHosts?.editable ?? false + if textView.isEditable != isEditable { + textView.isEditable = isEditable + } if textView.syntaxHighlighting() != syntaxHighlighting { textView.setSyntaxHighlighting(syntaxHighlighting) @@ -39,24 +63,27 @@ struct HostsTextViewRepresentable: NSViewRepresentable { } func makeCoordinator() -> Coordinator { - Coordinator(store: store) + Coordinator(onTextChange: onTextChange) } // MARK: - Coordinator @MainActor final class Coordinator: NSObject, NSTextViewDelegate { - let store: HostsDataStore + let onTextChange: (String) -> Void weak var textView: HostsTextView? + // Weak to avoid retaining a deleted Hosts object; nilling forces a content refresh + weak var lastUpdatedHosts: Hosts? + var lastContentToken: UInt64 = 0 var isUpdatingFromModel = false - init(store: HostsDataStore) { - self.store = store + init(onTextChange: @escaping (String) -> Void) { + self.onTextChange = onTextChange } func textDidChange(_ notification: Notification) { guard !isUpdatingFromModel, let textView = notification.object as? HostsTextView else { return } - store.selectedHosts?.setContents(textView.string) + onTextChange(textView.string) } } } diff --git a/Source/Swift/SidebarView.swift b/Source/Swift/SidebarView.swift index 0da9a6a..cddddc5 100644 --- a/Source/Swift/SidebarView.swift +++ b/Source/Swift/SidebarView.swift @@ -60,9 +60,6 @@ struct SidebarView: View { renameField(for: hosts) } else { HostsRowView(hosts: hosts, isGroup: false, refreshToken: store.rowRefreshToken) - .draggable(hosts.contents() ?? "") { - Text(hosts.name() ?? "") - } .contextMenu { contextMenuItems(for: hosts) } } } diff --git a/Source/Util.h b/Source/Util.h index 3f6cb37..3e2e2ad 100644 --- a/Source/Util.h +++ b/Source/Util.h @@ -20,7 +20,7 @@ @interface Util : NSObject -+ (BOOL) flushDirectoryServiceCache; ++ (void) flushDirectoryServiceCache; /** * OS X 10.10 and later support the NSStatusItemBar button which is what the * "Show Host File Name in Status Bar" feature is built upon. This method diff --git a/Source/Util.m b/Source/Util.m index 462ba0d..8ed8ef2 100644 --- a/Source/Util.m +++ b/Source/Util.m @@ -23,13 +23,18 @@ @implementation Util -+ (BOOL) flushDirectoryServiceCache ++ (void) flushDirectoryServiceCache { logDebug(@"Flushing Directory Service Cache"); - NSArray *arguments = [NSArray arrayWithObject:@"-flushcache"]; - NSTask * task = [NSTask launchedTaskWithLaunchPath:@"/usr/bin/dscacheutil" arguments:arguments]; - [task waitUntilExit]; - return [task terminationStatus] == 0; + NSTask *task = [[NSTask alloc] init]; + task.launchPath = @"/usr/bin/dscacheutil"; + task.arguments = @[@"-flushcache"]; + task.terminationHandler = ^(NSTask *t) { + if (t.terminationStatus != 0) { + logDebug(@"dscacheutil failed with status %d", t.terminationStatus); + } + }; + [task launch]; } + (BOOL) isPre10_10 diff --git a/Tests/GasMaskTests/HostsDataStoreNotificationTests.swift b/Tests/GasMaskTests/HostsDataStoreNotificationTests.swift new file mode 100644 index 0000000..ef9b813 --- /dev/null +++ b/Tests/GasMaskTests/HostsDataStoreNotificationTests.swift @@ -0,0 +1,125 @@ +import XCTest +import Combine +@testable import Gas_Mask + +final class HostsDataStoreNotificationTests: XCTestCase { + + // MARK: - Notification Coalescing + + /// Verifies that multiple rapid row-refresh notifications are coalesced + /// into a single `rowRefreshToken` increment (one SwiftUI re-render). + func testCoalescing_rapidNotifications_incrementTokenOnce() { + let store = HostsDataStore() + let before = store.rowRefreshToken + + // Post 10 rapid notifications without draining the run loop + for _ in 0..<10 { + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + } + + // Drain: observer blocks fire, then the single coalesced async block + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.2)) + + let increment = store.rowRefreshToken &- before + XCTAssertEqual(increment, 1, + "Expected 1 coalesced increment, got \(increment) — " + + "each increment triggers a full SwiftUI re-render") + } + + /// Verifies coalescing across different notification types. + /// A download lifecycle posts hostsFileSaved, hostsNodeNeedsUpdate, + /// and synchronizingStatusChanged in rapid succession. + func testCoalescing_mixedNotificationTypes_incrementTokenOnce() { + let store = HostsDataStore() + let before = store.rowRefreshToken + + // Simulate notification cascade from hostsDownloaded: + NotificationCenter.default.post(name: .synchronizingStatusChanged, object: nil) + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + NotificationCenter.default.post(name: .hostsFileSaved, object: nil) + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + NotificationCenter.default.post(name: .synchronizingStatusChanged, object: nil) + + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.2)) + + let increment = store.rowRefreshToken &- before + XCTAssertEqual(increment, 1, + "Expected 1 coalesced increment from mixed notifications, got \(increment)") + } + + /// Verifies that a single notification still increments the token by 1 + /// (coalescing doesn't break the single-notification case). + func testCoalescing_singleNotification_incrementsTokenByOne() { + let store = HostsDataStore() + let before = store.rowRefreshToken + + NotificationCenter.default.post(name: .hostsFileSaved, object: nil) + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.2)) + + XCTAssertEqual(store.rowRefreshToken, before &+ 1) + } + + /// Verifies that notifications separated by a run loop drain each + /// produce their own increment (coalescing is per-cycle, not global). + func testCoalescing_separateCycles_incrementTokenSeparately() { + let store = HostsDataStore() + let before = store.rowRefreshToken + + // First cycle + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + + // Second cycle + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + NotificationCenter.default.post(name: .hostsNodeNeedsUpdate, object: nil) + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + + let increment = store.rowRefreshToken &- before + XCTAssertEqual(increment, 2, + "Expected 2 increments (one per cycle), got \(increment)") + } + + // MARK: - objectWillChange Coalescing + + /// Verifies that a download lifecycle's notification cascade produces + /// a bounded number of objectWillChange signals. + func testCoalescing_downloadLifecycle_boundedObjectWillChange() { + let store = HostsDataStore() + + let remote = Hosts(path: "/tmp/coalesceTest.hst")! + remote.setSaved(true) + remote.exists = true + remote.setEnabled(true) + + // Drain any pending events + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.1)) + + var changeCount = 0 + let cancellable = store.objectWillChange.sink { _ in + changeCount += 1 + } + defer { cancellable.cancel() } + + // Simulate full download lifecycle notification cascade + NotificationCenter.default.post(name: .synchronizingStatusChanged, object: remote) + remote.setEnabled(false) + NotificationCenter.default.post(name: .threadBusy, object: nil) + NotificationCenter.default.post(name: .synchronizingStatusChanged, object: remote) + remote.setEnabled(true) + remote.setContents("large content here") + remote.setSaved(true) + NotificationCenter.default.post(name: .hostsFileSaved, object: remote) + NotificationCenter.default.post(name: .threadNotBusy, object: nil) + + RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.3)) + + // With coalescing: row refresh notifications collapse to 1 objectWillChange, + // plus busy state changes (isBusy = true, isBusy = false) = ~3 total. + // Without coalescing: 8-12 objectWillChange signals. + XCTAssertLessThanOrEqual(changeCount, 5, + "Download lifecycle caused \(changeCount) objectWillChange signals — " + + "expected ≤5 with coalescing (was 8-12 without)") + } +} diff --git a/Tests/GasMaskTests/HostsTextViewPerformanceTests.swift b/Tests/GasMaskTests/HostsTextViewPerformanceTests.swift new file mode 100644 index 0000000..6a01662 --- /dev/null +++ b/Tests/GasMaskTests/HostsTextViewPerformanceTests.swift @@ -0,0 +1,899 @@ +import XCTest +import Combine +import SwiftUI +@testable import Gas_Mask + +final class HostsTextViewPerformanceTests: XCTestCase { + + private var textView: HostsTextView! + private var scrollView: NSScrollView! + private var window: NSWindow! + + override func setUp() { + super.setUp() + guard let tv = HostsTextView.createForProgrammaticUse() else { + XCTFail("Failed to create HostsTextView") + return + } + textView = tv + textView.setSyntaxHighlighting(true) + + scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 800, height: 600)) + scrollView.documentView = textView + scrollView.hasVerticalScroller = true + scrollView.hasHorizontalScroller = true + + // Put in a window so layout is triggered (mimics real usage) + window = NSWindow( + contentRect: NSRect(x: 0, y: 0, width: 800, height: 600), + styleMask: [.titled], + backing: .buffered, + defer: false + ) + window.contentView = scrollView + window.orderBack(nil) + } + + override func tearDown() { + window.orderOut(nil) + window = nil + scrollView = nil + textView = nil + super.tearDown() + } + + // MARK: - Small File Switching (User-reported lockup with local files) + + /// Reproduces the reported issue: switching between two SMALL local files + /// should not lock up the UI. This exercises the exact updateNSView flow. + func testRapidSwitching_twoSmallLocalFiles_completesQuickly() { + let fileA = "127.0.0.1 localhost\n::1 localhost\n# my local config\n" + let fileB = "127.0.0.1 myhost.local\n192.168.1.1 router.local\n" + + let switchCount = 20 + let start = CFAbsoluteTimeGetCurrent() + + for _ in 0.. 0.05 ? " ⚠️ SLOW" : "") + } + NSLog("Concurrent download test: avg=%.1fms, max=%.1fms, total=%.0fms", + avgSwitch * 1000, maxSwitch * 1000, totalActive * 1000) + + // The switch that coincides with download completion might be slower, + // but should still be under 200ms for a good user experience + XCTAssertLessThan(maxSwitch, 0.2, + "Slowest switch during concurrent download took " + + "\(String(format: "%.0f", maxSwitch * 1000))ms — user will perceive lockup") + } + + // MARK: - Helpers + + /// Simulates the text replacement portion of updateNSView (length-first guard + replace). + /// Does NOT test the pointer-based selection guard — that is tested separately in + /// testGuardCheck_pointerBased_skipsStringComparison. + private func simulateUpdateNSView(contents: String) { + let currentLength = (textView.string as NSString).length + let newLength = (contents as NSString).length + if currentLength != newLength || textView.string != contents { + textView.replaceContent(with: contents) + } + } + + private static func generateLargeHostsContent(lineCount: Int) -> String { + var lines: [String] = [] + lines.reserveCapacity(lineCount + 2) + lines.append("# Large hosts file for testing performance") + lines.append("# Generated with \(lineCount) entries") + for i in 0..