diff --git a/.gitignore b/.gitignore index 61539f8..0716547 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,4 @@ fastlane/test_output fastlane/report.xml fastlane/README.md -notes.txt +.bundle/ diff --git a/.swiftlint.yml b/.swiftlint.yml index d7be92d..0d510f9 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -1,5 +1,6 @@ excluded: - build + - .bundle opt_in_rules: - array_init diff --git a/AppConfig.xcconfig b/AppConfig.xcconfig index d20ce64..9fa2a86 100644 --- a/AppConfig.xcconfig +++ b/AppConfig.xcconfig @@ -20,6 +20,12 @@ REGISTER_APP_GROUPS = YES ENABLE_APP_SANDBOX = YES ENABLE_APP_SANDBOX[config=DebugNoSandbox] = NO +// the entitlements are a property of the target, not of the machine, so they live here and not +// in Signing.local.xcconfig, which every contributor generates from Signing-Template.xcconfig +// the Sparkle configuration overrides this in AppConfig-Sparkle.xcconfig +CODE_SIGN_ENTITLEMENTS = VisualDiffer.entitlements +CODE_SIGN_ENTITLEMENTS[config=DebugNoSandbox] = ./Tests/NoSandbox.entitlements + LD_RUNPATH_SEARCH_PATHS=@executable_path/../Frameworks INFOPLIST_FILE = ./VisualDiffer-Info.plist diff --git a/AppDefaults/colors.json b/AppDefaults/colors.json index 5ab5c47..a09c4c6 100644 --- a/AppDefaults/colors.json +++ b/AppDefaults/colors.json @@ -1,9 +1,4 @@ { - "menuColorsMap": { - "openWithDefaultAppContext": { - "textColor":"#0000ff" - } - }, "consoleLogColorsMap": { "info": { @@ -83,6 +78,11 @@ "backgroundColor":"#e4f1d5" }, + "changedInline": { + "textColor": "#5A1010", + "backgroundColor": "#FBE38A" + }, + "added": { "textColor": "#0000FF", "backgroundColor":"#DCE5F5" diff --git a/AppDefaults/colorsDark.json b/AppDefaults/colorsDark.json index 6f95e05..2de04fb 100644 --- a/AppDefaults/colorsDark.json +++ b/AppDefaults/colorsDark.json @@ -1,9 +1,4 @@ { - "menuColorsMap": { - "openWithDefaultAppContext": { - "textColor":"#FF9300" - } - }, "consoleLogColorsMap": { "info": { @@ -80,7 +75,12 @@ { "changed": { "textColor": "#C4EFB7", - "backgroundColor":"#5B8B6D" + "backgroundColor":"#2E4A38" + }, + + "changedInline": { + "textColor": "#2E4A38", + "backgroundColor":"#FFC24B" }, "added": { diff --git a/Base.lproj/MainMenu.xib b/Base.lproj/MainMenu.xib index a345101..e4fb1e3 100644 --- a/Base.lproj/MainMenu.xib +++ b/Base.lproj/MainMenu.xib @@ -344,12 +344,6 @@ - - - - - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22789e3..1bd9b0c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,39 +1,67 @@ -## 🛠️ Build +## 🛠️ Build -Clone the repository and build the app: +### Requirements + +- **Xcode 26 or later.** The project is stored in the Xcode 26 format (`objectVersion = 90`) and uses + file system synchronized groups, so earlier versions of Xcode cannot open it. +- [Homebrew](https://brew.sh), used to install the tools below. +- The app targets macOS 13.5 and later. + +Install the tools needed to configure and lint the project: + +```bash +brew install gnu-getopt \ + swiftformat \ + swiftlint +``` + +`gnu-getopt` is required by `./scripts/setup-local-env.sh`: the BSD `getopt` shipped with macOS does not +support long options. + +### Get the sources + +Clone the repository and generate the local configuration files: ```bash git clone https://github.com/visualdiffer/visualdiffer.git cd visualdiffer ./scripts/setup-local-env.sh - ``` -Open in Xcode +The script copies the `*-Template.xcconfig` files to the `*.local.xcconfig` ones that the build expects. +It never overwrites an existing file, so it is safe to run again. + +Open `VisualDiffer.xcworkspace` in Xcode, not `VisualDiffer.xcodeproj`. + +The workspace is the container that pins the Swift Package Manager dependencies, and it is the one +the release is built from. Opening the project instead resolves the package versions freely, so you +may end up building against a different Sparkle version than the one that ships. + +### Run the tests -## 👥 Contributing +Press ⌘U in Xcode, or from the shell: + +```bash +xcodebuild -workspace VisualDiffer.xcworkspace -scheme VisualDiffer test +``` -Contributions, issues, and feature requests are welcome! +The shared scheme already runs the tests under the `DebugNoSandbox` configuration, which grants the +file system permissions the file and folder comparison tests need. -### Prerequisites +## 👥 Contributing -To contribute: +Contributions, issues, and feature requests are welcome! -1. Install [swiftformat](https://github.com/nicklockwood/SwiftFormat), [swiftlint](https://github.com/realm/SwiftLint), and fzf - - ``` - brew install swiftformat \ - swiftlint \ - fzf - ``` +1. Fork the repository +2. Create a new branch (`feat/xyz` or `fix/abc`) +3. Apply your modifications +4. Run `./scripts/lint.sh` to apply `swiftformat` and `swiftlint` +5. Commit your changes with clear messages +6. Open a pull request describing your update -2. Fork the repository -3. Create a new branch (`feat/xyz` or `fix/abc`) -4. Apply your modifications -5. Run `./scripts/lint.sh` to apply `swiftformat` and `swiftlint` -6. Commit your changes with clear messages -7. Open a pull request describing your update +`./scripts/lint.sh` only formats the files you changed. Pass `a` to format the whole project, or `p` to +run [periphery](https://github.com/peripheryapp/periphery) instead and look for unused code. ⚠️ Please follow the existing code style and include tests or examples when possible. @@ -43,11 +71,13 @@ For internal use only — contributors can skip this section. ### 1. Prerequisites -Deployment relies on [fastlane](https://fastlane.tools/), which in turn uses [xcbeautify](https://github.com/cpisciotta/xcbeautify). +Deployment relies on [fastlane](https://fastlane.tools/), which in turn uses +[xcbeautify](https://github.com/cpisciotta/xcbeautify). `./scripts/build.sh` uses +[fzf](https://github.com/junegunn/fzf) to pick the build profile. ``` cd visualdiffer -brew install fastlane xcbeautify gnu-getopt +brew install fastlane xcbeautify fzf bundle install ``` @@ -65,13 +95,14 @@ npm install -g $PWD Run the admin script with the private config, passing its absolute path: - ./scripts/setup-local-env.sh -m admin /Users/dave/visualdiffer-admin-config/ + ./scripts/setup-local-env.sh -m admin /absolute/path/to/visualdiffer-private/ ### 3. Build #### Automatic -Run `./scripts/build.sh` and select the environment from the list. Unit tests run as part of the build. +Run `./scripts/build.sh` and select an entry from the list. Unit tests run as part of the build. +The list also offers *Create Changelog*, which only generates the release notes and exits. #### Manual @@ -83,4 +114,5 @@ Run `./scripts/build.sh` and select the environment from the list. Unit tests ru |--------------------|-----------------------------------------------------| | `local` | Release build deployed on GitHub | | `prerelease.local` | Pre-release testing build (also deployed on GitHub) | +| `sparkle.local` | Sparkle build, does not upload the appcast | | `test.local` | Build for [tart](https://tart.run/) or other VMs | diff --git a/Gemfile.lock b/Gemfile.lock index 2d636c3..613c866 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,8 +8,8 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.4.0) - aws-partitions (1.1268.0) - aws-sdk-core (3.254.0) + aws-partitions (1.1287.0) + aws-sdk-core (3.257.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -17,11 +17,12 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.130.0) - aws-sdk-core (~> 3, >= 3.254.0) + rexml (~> 3.4, >= 3.4.2) + aws-sdk-kms (1.132.0) + aws-sdk-core (~> 3, >= 3.256.0) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.227.0) - aws-sdk-core (~> 3, >= 3.254.0) + aws-sdk-s3 (1.232.1) + aws-sdk-core (~> 3, >= 3.256.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) @@ -29,51 +30,40 @@ GEM babosa (1.0.4) base64 (0.3.0) benchmark (0.5.0) - bigdecimal (4.1.2) + bigdecimal (4.1.3) + cgi (0.5.2) claide (1.1.0) colored (1.2) colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - csv (3.3.5) + csv (3.3.6) declarative (0.0.20) digest-crc (0.7.0) rake (>= 12.0.0, < 14.0.0) - domain_name (0.6.20240107) + domain_name (0.6.20260907) dotenv (2.8.1) emoji_regex (3.2.3) - excon (1.5.0) + erb (6.0.7) + excon (1.7.1) + logger + faraday (2.14.4) + faraday-net_http (>= 2.0, < 3.5) + json logger - faraday (1.10.6) - faraday-em_http (~> 1.0) - faraday-em_synchrony (~> 1.0) - faraday-excon (~> 1.1) - faraday-httpclient (~> 1.0) - faraday-multipart (~> 1.0) - faraday-net_http (~> 1.0) - faraday-net_http_persistent (~> 1.0) - faraday-patron (~> 1.0) - faraday-rack (~> 1.0) - faraday-retry (~> 1.0) - ruby2_keywords (>= 0.0.4) faraday-cookie_jar (0.0.8) faraday (>= 0.8.0) http-cookie (>= 1.0.0) - faraday-em_http (1.0.0) - faraday-em_synchrony (1.0.1) - faraday-excon (1.1.0) - faraday-httpclient (1.0.1) + faraday-follow_redirects (0.5.0) + faraday (>= 1, < 3) faraday-multipart (1.2.0) multipart-post (~> 2.0) - faraday-net_http (1.0.2) - faraday-net_http_persistent (1.2.0) - faraday-patron (1.0.0) - faraday-rack (1.0.0) - faraday-retry (1.0.4) - faraday_middleware (1.2.1) - faraday (~> 1.0) + faraday-net_http (3.4.4) + net-http (~> 0.5) + faraday-retry (2.4.0) + faraday (~> 2.0) fastimage (2.4.1) - fastlane (2.237.0) + fastlane (2.240.1) CFPropertyList (>= 2.3, < 5.0.0) abbrev (~> 0.1) addressable (>= 2.9.0, < 3.0.0) @@ -83,24 +73,27 @@ GEM base64 (~> 0.2) benchmark (>= 0.1.0) bundler (>= 2.4.0, < 5.0.0) + cgi (~> 0.4) colored (~> 1.2) commander (~> 4.6) csv (~> 3.3) dotenv (>= 2.1.1, < 3.0.0) emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 2.0.0) - faraday (~> 1.0) - faraday-cookie_jar (~> 0.0.6) - faraday_middleware (~> 1.0) + faraday (~> 2.7) + faraday-cookie_jar (~> 0.0.8) + faraday-follow_redirects (~> 0.3) + faraday-multipart (~> 1.0) + faraday-retry (~> 2.0) fastimage (>= 2.1.0, < 3.0.0) fastlane-sirp (>= 1.1.0) gh_inspector (>= 1.1.2, < 2.0.0) - google-apis-androidpublisher_v3 (~> 0.3) + google-apis-androidpublisher_v3 (~> 0.99) google-apis-playcustomapp_v1 (~> 0.1) - google-cloud-env (>= 1.6.0, < 2.3.0) + google-cloud-env (>= 1.6.0, < 2.4.0) google-cloud-storage (~> 1.31) highline (~> 2.0) - http-cookie (~> 1.0.5) + irb (>= 1.8) json (< 3.0.0) jwt (>= 2.10.3, < 4) logger (>= 1.6, < 2.0) @@ -113,11 +106,11 @@ GEM optparse (>= 0.1.1, < 1.0.0) ostruct (>= 0.1.0) plist (>= 3.1.0, < 4.0.0) - rubyzip (>= 2.0.0, < 3.0.0) - security (= 0.1.5) + rubyzip (>= 3.4.0, < 4.0.0) + security (~> 0.3) simctl (~> 1.6.3) terminal-notifier (>= 2.0.0, < 3.0.0) - terminal-table (~> 3) + terminal-table (~> 4) tty-screen (>= 0.6.3, < 1.0.0) tty-spinner (>= 0.8.0, < 1.0.0) word_wrap (~> 1.0.0) @@ -126,26 +119,27 @@ GEM xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) fastlane-sirp (1.1.0) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.104.0) + google-apis-androidpublisher_v3 (0.108.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-core (0.18.0) - addressable (~> 2.5, >= 2.5.1) - googleauth (~> 1.9) - httpclient (>= 2.8.3, < 3.a) - mini_mime (~> 1.0) - mutex_m + google-apis-core (1.2.5) + addressable (~> 2.9) + faraday (~> 2.13) + faraday-follow_redirects (~> 0.3) + googleauth (~> 1.14) + mini_mime (~> 1.1) + multi_json (~> 1.11) representable (~> 3.0) - retriable (>= 2.0, < 4.a) + retriable (>= 3.1, < 5.0) google-apis-iamcredentials_v1 (0.28.0) google-apis-core (>= 0.15.0, < 2.a) google-apis-playcustomapp_v1 (0.18.0) google-apis-core (>= 0.15.0, < 2.a) - google-apis-storage_v1 (0.64.0) + google-apis-storage_v1 (0.67.0) google-apis-core (>= 0.15.0, < 2.a) google-cloud-core (1.9.0) google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) - google-cloud-env (2.2.2) + google-cloud-env (2.3.1) base64 (~> 0.2) faraday (>= 1.0, < 3.a) google-cloud-errors (1.7.0) @@ -159,7 +153,7 @@ GEM googleauth (~> 1.9) mini_mime (~> 1.0) google-logging-utils (0.2.0) - googleauth (1.17.1) + googleauth (1.17.4) faraday (>= 1.0, < 3.a) google-cloud-env (~> 2.2) google-logging-utils (~> 0.1) @@ -168,42 +162,62 @@ GEM pstore (~> 0.1) signet (>= 0.16, < 2.a) highline (2.0.3) - http-cookie (1.0.8) + http-cookie (1.1.6) domain_name (~> 0.5) - httpclient (2.9.0) - mutex_m + io-console (0.9.4) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) jmespath (1.6.2) - json (2.20.0) - jwt (3.2.0) + json (2.21.2) + jwt (3.3.0) base64 kramdown (2.5.1) rexml (>= 3.3.9) logger (1.7.0) mini_magick (4.13.2) mini_mime (1.1.5) - multi_json (1.21.1) + multi_json (1.21.2) multipart-post (2.4.1) mutex_m (0.3.0) nanaimo (0.4.0) naturally (2.3.0) + net-http (0.9.1) + uri (>= 0.11.1) nkf (0.3.0) optparse (0.8.1) os (1.1.4) ostruct (0.6.3) plist (3.7.2) + pp (0.6.4) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) pstore (0.2.1) public_suffix (7.0.5) rake (13.4.2) + rbs (4.2.0) + logger + prism (>= 1.6.0) + tsort + rdoc (8.0.0) + erb + prism (>= 1.6.0) + rbs (>= 4.0.0) + tsort + reline (0.7.0) + io-console (~> 0.5) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) - retriable (3.8.0) + retriable (4.2.0) rexml (3.4.4) rouge (3.28.0) - ruby2_keywords (0.0.5) - rubyzip (2.4.1) - security (0.1.5) + rubyzip (3.6.0) + security (0.3.0) signet (0.22.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) @@ -212,15 +226,19 @@ GEM CFPropertyList naturally terminal-notifier (2.0.0) - terminal-table (3.0.2) - unicode-display_width (>= 1.1.1, < 3) + terminal-table (4.0.0) + unicode-display_width (>= 1.1.1, < 4) trailblazer-option (0.1.2) + tsort (0.2.0) tty-cursor (0.7.1) tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) - unicode-display_width (2.6.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) + uri (1.1.1) word_wrap (1.0.0) xcodeproj (1.28.1) CFPropertyList (>= 2.3.3, < 4.0) @@ -252,94 +270,100 @@ CHECKSUMS artifactory (3.0.17) sha256=3023d5c964c31674090d655a516f38ca75665c15084140c08b7f2841131af263 atomos (0.1.3) sha256=7d43b22f2454a36bace5532d30785b06de3711399cb1c6bf932573eda536789f aws-eventstream (1.4.0) sha256=116bf85c436200d1060811e6f5d2d40c88f65448f2125bc77ffce5121e6e183b - aws-partitions (1.1268.0) sha256=61c41c23445380425455e549dcb283f0df50aa2f3d71892f692572fed9ef6c0f - aws-sdk-core (3.254.0) sha256=ee3e3220b8468a3c9e59daba18e6ec897bf5c7ce8adcc0670cfa2f1f092112fe - aws-sdk-kms (1.130.0) sha256=a2e83662ca31b77a2a19c9aa2f40a98165a67270c18c718fe1c70d0cbd7cd749 - aws-sdk-s3 (1.227.0) sha256=552b23bf9a37c7db4957e9291543e61c8de886cf31d1d45ea3b429df0feea939 + aws-partitions (1.1287.0) sha256=76f4c73011654553566c5db5bbef0f24738bb016e923910e8b1b3f9cb7d989ca + aws-sdk-core (3.257.0) sha256=a92ad19c7ff668cec6688ee3200ee562da80647a3bbeed9fda02e8b0ca6492c6 + aws-sdk-kms (1.132.0) sha256=094b0097bb3be9d5c1ac87e971ca7f5aebb746801dfb5f0f576480bf98dca69c + aws-sdk-s3 (1.232.1) sha256=124440f3a44fbc28cc3a2dec6eaeeea2a236bfc33772005010a19baa62654870 aws-sigv4 (1.12.1) sha256=6973ff95cb0fd0dc58ba26e90e9510a2219525d07620c8babeb70ef831826c00 babosa (1.0.4) sha256=18dea450f595462ed7cb80595abd76b2e535db8c91b350f6c4b3d73986c5bc99 base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c - bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bigdecimal (4.1.3) sha256=61ebe1e5e559bdc3cc6f2c0ee7f427321fc838f59611c294356eb04d6e21cf66 + bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 + cgi (0.5.2) sha256=61ca30298171190fd4fa0d8018e57ada456eae9b7a2b78526debf7f0a0e6f8bb claide (1.1.0) sha256=6d3c5c089dde904d96aa30e73306d0d4bd444b1accb9b3125ce14a3c0183f82e colored (1.2) sha256=9d82b47ac589ce7f6cab64b1f194a2009e9fd00c326a5357321f44afab2c1d2c colored2 (3.1.2) sha256=b13c2bd7eeae2cf7356a62501d398e72fde78780bd26aec6a979578293c28b4a commander (4.6.0) sha256=7d1ddc3fccae60cc906b4131b916107e2ef0108858f485fdda30610c0f2913d9 - csv (3.3.5) sha256=6e5134ac3383ef728b7f02725d9872934f523cb40b961479f69cf3afa6c8e73f + csv (3.3.6) sha256=aba61e7e507a66f03d45cb1f3c4b6359861c3504038b422962875dce099e4456 declarative (0.0.20) sha256=8021dd6cb17ab2b61233c56903d3f5a259c5cf43c80ff332d447d395b17d9ff9 digest-crc (0.7.0) sha256=64adc23a26a241044cbe6732477ca1b3c281d79e2240bcff275a37a5a0d78c07 - domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 + domain_name (0.6.20260907) sha256=5517a7f82c4464a61fc17133c3f6a02833faa13533c16c07e8f73b11d872cb03 dotenv (2.8.1) sha256=c5944793349ae03c432e1780a2ca929d60b88c7d14d52d630db0508c3a8a17d8 emoji_regex (3.2.3) sha256=ecd8be856b7691406c6bf3bb3a5e55d6ed683ffab98b4aa531bb90e1ddcc564b - excon (1.5.0) sha256=c503ad1d0123bc8ab2a062ff3789dc891ec368cb9e13765ab88a9c58c8bb6d50 - faraday (1.10.6) sha256=7ff4802a6b312876a2241b3e641ce0d5045e168dd871b422c35b505e5261ad4d + erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92 + excon (1.7.1) sha256=dd2d870eece1b5ce4e4f9efd938e67a69ccd8c003c3825b638b937e1082eaac2 + faraday (2.14.4) sha256=9bb4408c44621b0dbaaaa39e5dbfc4dbe58cc6751b6eb94d16ae9e1f2f2fab6e faraday-cookie_jar (0.0.8) sha256=0140605823f8cc63c7028fccee486aaed8e54835c360cffc1f7c8c07c4299dbb - faraday-em_http (1.0.0) sha256=7a3d4c7079789121054f57e08cd4ef7e40ad1549b63101f38c7093a9d6c59689 - faraday-em_synchrony (1.0.1) sha256=bf3ce45dcf543088d319ab051f80985ea6d294930635b7a0b966563179f81750 - faraday-excon (1.1.0) sha256=b055c842376734d7f74350fe8611542ae2000c5387348d9ba9708109d6e40940 - faraday-httpclient (1.0.1) sha256=4c8ff1f0973ff835be8d043ef16aaf54f47f25b7578f6d916deee8399a04d33b + faraday-follow_redirects (0.5.0) sha256=5cde93c894b30943a5d2b93c2fe9284216a6b756f7af406a1e55f211d97d10ad faraday-multipart (1.2.0) sha256=7d89a949693714176f612323ca13746a2ded204031a6ba528adee788694ef757 - faraday-net_http (1.0.2) sha256=63992efea42c925a20818cf3c0830947948541fdcf345842755510d266e4c682 - faraday-net_http_persistent (1.2.0) sha256=0b0cbc8f03dab943c3e1cc58d8b7beb142d9df068b39c718cd83e39260348335 - faraday-patron (1.0.0) sha256=dc2cd7b340bb3cc8e36bcb9e6e7eff43d134b6d526d5f3429c7a7680ddd38fa7 - faraday-rack (1.0.0) sha256=ef60ec969a2bb95b8dbf24400155aee64a00fc8ba6c6a4d3968562bcc92328c0 - faraday-retry (1.0.4) sha256=dc659233777fabf96c69c2ffe56c0a5d2c102af90321a42cc6c90157bcd716aa - faraday_middleware (1.2.1) sha256=d45b78c8ee864c4783fbc276f845243d4a7918a67301c052647bacabec0529e9 + faraday-net_http (3.4.4) sha256=0e78af151747ed1b00f33e25973b4bc220d7f16c00c39676817c8b12331eb588 + faraday-retry (2.4.0) sha256=7b79c48fb7e56526faf247b12d94a680071ff40c9fda7cf1ec1549439ad11ebe fastimage (2.4.1) sha256=c64bebd46b6fd8943ab70c1e6e85ff728f970f2e48f92ecd249b6bc3a540ad20 - fastlane (2.237.0) sha256=bb1e867bc070fb328741b5e6e7606d5ba596941a9ecca0478f60ef22d1009db9 + fastlane (2.240.1) sha256=cf792bc99ec6af6574ea416cda6530f519c8b4b3a46b1a3b52488ebf1696aa5c fastlane-sirp (1.1.0) sha256=10bc94f9682efd8e1badfb31452a76dd8981f1f3a33717c765fde6d75b54d847 gh_inspector (1.1.3) sha256=04cca7171b87164e053aa43147971d3b7f500fcb58177698886b48a9fc4a1939 - google-apis-androidpublisher_v3 (0.104.0) sha256=3bf7f0dee23ae71e070e279848374834853204e304831cf6aa76fa435a4d6eff - google-apis-core (0.18.0) sha256=96b057816feeeab448139ed5b5c78eab7fc2a9d8958f0fbc8217dedffad054ee + google-apis-androidpublisher_v3 (0.108.0) sha256=11a8b1885dce62ef28d8b7f16225d67e60c77b5baf16310a31d33fb1dc3c2d4e + google-apis-core (1.2.5) sha256=e21562b5627a0fc6a0c3165e429c3afed0f6a628eaa514e83f7204dda1adff69 google-apis-iamcredentials_v1 (0.28.0) sha256=0a92ffe6cc39c569554af2a77a25dfc61519ed8bbb64ab04cffdd352dc5ef106 google-apis-playcustomapp_v1 (0.18.0) sha256=44b277b9dee4a59ac5e9d98be1485edc5e382d2f9d73c79ae8908a455786a254 - google-apis-storage_v1 (0.64.0) sha256=75b11afa2edcee859b84c7a6972ee4456314eeef5f762827fd6cf5c5ffaf93f2 + google-apis-storage_v1 (0.67.0) sha256=13befc89bf03512783f535d214590f15993b0c56411155fd8090ca607e1b8d20 google-cloud-core (1.9.0) sha256=ab55409f51488e8deefb6edcc1ce4771dfb5da2fe7b3bc075709a030c2b682a4 - google-cloud-env (2.2.2) sha256=94bed40e05a67e9468ce1cb38389fba9a90aa8fc62fc9e173204c1dca59e21e7 + google-cloud-env (2.3.1) sha256=0faac01eb27be78c2591d64433663b1a114f8f7af55a4f819755426cac9178e7 google-cloud-errors (1.7.0) sha256=6e682f42d89aae08689f36f28495de629d756da076bafff0003bac7f8042ebb3 google-cloud-storage (1.62.0) sha256=e2c3c08bf8fd40d50be92304084942203314d4fc0ee52028e99f9359c3ad1330 google-logging-utils (0.2.0) sha256=675462b4ea5affa825a3442694ca2d75d0069455a1d0956127207498fca3df7b - googleauth (1.17.1) sha256=0f7e6fc70e204cee1b2d71f1e1de2d3b349d432404197fe68ebf7fa23d0821b9 + googleauth (1.17.4) sha256=acc2cac2a6011048f149c922cbc6c9675719e165aa5000c7fa3876c480fe9ae3 highline (2.0.3) sha256=2ddd5c127d4692721486f91737307236fe005352d12a4202e26c48614f719479 - http-cookie (1.0.8) sha256=b14fe0445cf24bf9ae098633e9b8d42e4c07c3c1f700672b09fbfe32ffd41aa6 - httpclient (2.9.0) sha256=4b645958e494b2f86c2f8a2f304c959baa273a310e77a2931ddb986d83e498c8 + http-cookie (1.1.6) sha256=ba4b82be64de61dc281243dac70e3c382c45142f20268ed9276a3670c93feaa9 + io-console (0.9.4) sha256=a403304f32928ee85e56d457b4612ca43f2703b561a3258844992bd24085fbb1 + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 jmespath (1.6.2) sha256=238d774a58723d6c090494c8879b5e9918c19485f7e840f2c1c7532cf84ebcb1 - json (2.20.0) sha256=9362bc6e55a952b056abf9167cf053358181c904cb70cd6eee0808ea830fc32b - jwt (3.2.0) sha256=5419b1fe37b1da0982bd07051f573a8b8789ab724c2aa7e785e4784a3ed217d7 + json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a + jwt (3.3.0) sha256=44cc34fbc341c148233cf66641522b4bce0dfb1ce55e8073f05c56f44f4ee546 kramdown (2.5.1) sha256=87bbb6abd9d3cebe4fc1f33e367c392b4500e6f8fa19dd61c0972cf4afe7368c logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 mini_magick (4.13.2) sha256=71d6258e0e8a3d04a9a0a09784d5d857b403a198a51dd4f882510435eb95ddd9 mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef - multi_json (1.21.1) sha256=e6126a31808e3b4d19f483c775ceac34df190dffa62adfb63a165ee14ba68080 + multi_json (1.21.2) sha256=245531eaf54bdba57aba515ff08b4503505c5e09911daa89c5a561e2f85bea8e multipart-post (2.4.1) sha256=9872d03a8e552020ca096adadbf5e3cb1cd1cdd6acd3c161136b8a5737cdb4a8 mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 nanaimo (0.4.0) sha256=faf069551bab17f15169c1f74a1c73c220657e71b6e900919897a10d991d0723 naturally (2.3.0) sha256=459923cf76c2e6613048301742363200c3c7e4904c324097d54a67401e179e01 + net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996 nkf (0.3.0) sha256=357a8dbeba38b727b75930f665146546076a394a1c243faf634ff176e3588895 optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a os (1.1.4) sha256=57816d6a334e7bd6aed048f4b0308226c5fb027433b67d90a9ab435f35108d3f ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 plist (3.7.2) sha256=d37a4527cc1116064393df4b40e1dbbc94c65fa9ca2eec52edf9a13616718a42 + pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 pstore (0.2.1) sha256=03904d0f2c66579e96d1e6704cdabc0c88df7ea8ed8782d9f3569f6f6c702c1a public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 + rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 + reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d representable (3.2.0) sha256=cc29bf7eebc31653586849371a43ffe36c60b54b0a6365b5f7d95ec34d1ebace - retriable (3.8.0) sha256=9f2f1b0207594c7817f17f671587b8ec7587387ac6cebda6c941a802bb98a8e5 + retriable (4.2.0) sha256=90c3b257472a1c02f2a3dfa64dd35a420f7a624cef1a5981e20fdac5730f8cdc rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 rouge (3.28.0) sha256=0d6de482c7624000d92697772ab14e48dca35629f8ddf3f4b21c99183fd70e20 - ruby2_keywords (0.0.5) sha256=ffd13740c573b7301cf7a2e61fc857b2a8e3d3aff32545d6f8300d8bae10e3ef - rubyzip (2.4.1) sha256=8577c88edc1fde8935eb91064c5cb1aef9ad5494b940cf19c775ee833e075615 - security (0.1.5) sha256=3a977a0eca7706e804c96db0dd9619e0a94969fe3aac9680fcfc2bf9b8a833b7 + rubyzip (3.6.0) sha256=268994d44d62282d1cfd99bf10eae48d7267199158ad7ea3e1fee2da9458b695 + security (0.3.0) sha256=bc9d06718258a96c8552ce4044bfe69144491b10d3403d74c4753a03a720ecf0 signet (0.22.0) sha256=b76d495ccb07ad35dbc89f3e920665a9d8ed717141955034005d7843dcfe4780 simctl (1.6.10) sha256=b99077f4d13ad81eace9f86bf5ba4df1b0b893a4d1b368bd3ed59b5b27f9236b terminal-notifier (2.0.0) sha256=7a0d2b2212ab9835c07f4b2e22a94cff64149dba1eed203c04835f7991078cea - terminal-table (3.0.2) sha256=f951b6af5f3e00203fb290a669e0a85c5dd5b051b3b023392ccfd67ba5abae91 + terminal-table (4.0.0) sha256=f504793203f8251b2ea7c7068333053f0beeea26093ec9962e62ea79f94301d2 trailblazer-option (0.1.2) sha256=20e4f12ea4e1f718c8007e7944ca21a329eee4eed9e0fa5dde6e8ad8ac4344a3 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f tty-cursor (0.7.1) sha256=79534185e6a777888d88628b14b6a1fdf5154a603f285f80b1753e1908e0bf48 tty-screen (0.8.2) sha256=c090652115beae764336c28802d633f204fb84da93c6a968aa5d8e319e819b50 tty-spinner (0.9.3) sha256=0e036f047b4ffb61f2aa45f5a770ec00b4d04130531558a94bfc5b192b570542 uber (0.1.0) sha256=5beeb407ff807b5db994f82fa9ee07cfceaa561dad8af20be880bc67eba935dc - unicode-display_width (2.6.0) sha256=12279874bba6d5e4d2728cef814b19197dbb10d7a7837a869bab65da943b7f5a + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f + uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 word_wrap (1.0.0) sha256=f556d4224c812e371000f12a6ee8102e0daa724a314c3f246afaad76d82accc7 xcodeproj (1.28.1) sha256=6f12670f00739d9817ca27ac89d6ef01cc86050e22a0bc08a3131487e5b5cddc xcpretty (0.4.1) sha256=b14c50e721f6589ee3d6f5353e2c2cfcd8541fa1ea16d6c602807dd7327f3892 diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 24f2c74..56ca138 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -716,6 +716,12 @@ }, "Compare Files" : { + }, + "Compare Files on Left" : { + + }, + "Compare Files on Right" : { + }, "Compare Finder Label" : { @@ -725,9 +731,24 @@ }, "Compare Folders" : { + }, + "Compare Folders on Left" : { + + }, + "Compare Folders on Right" : { + }, "Compare Folders or Files" : { + }, + "Compare on Left" : { + + }, + "Compare on Right" : { + + }, + "Compare selected files or folders" : { + }, "Comparison" : { @@ -773,6 +794,10 @@ }, "Confirmations and Warnings" : { + }, + "Containing Text" : { + "comment" : "Title of the \"Containing Text\" option in the find options menu.", + "isCommentAutoGenerated" : true }, "contains" : { @@ -1262,6 +1287,9 @@ }, "Hide Log Console" : { + }, + "Hide Preview" : { + }, "Hide Whitespace" : { @@ -1414,12 +1442,28 @@ }, "Make read-only" : { + }, + "Match Case" : { + "comment" : "Title of a menu item that toggles whether the search should be case-sensitive.", + "isCommentAutoGenerated" : true }, "Match File Name Case" : { + }, + "Match Files" : { + "comment" : "Title for a menu option in the find text view that toggles whether to match file names during a search.", + "isCommentAutoGenerated" : true + }, + "Match Folders" : { + "comment" : "Text for matching folder names in a search.", + "isCommentAutoGenerated" : true }, "Matched Range $0" : { + }, + "Matching Word" : { + "comment" : "Title for the \"Matching Word\" find mode in the find options UI.", + "isCommentAutoGenerated" : true }, "Maybe it was located in a temporary directory and another process deleted it" : { @@ -1574,6 +1618,9 @@ }, "Prev Difference" : { + }, + "Preview" : { + }, "Previous File" : { @@ -1586,6 +1633,10 @@ }, "Quick Look" : { + }, + "Reading %@" : { + "comment" : "Text displayed in the progress bar while reading a file. The argument is the name of the file being read.", + "isCommentAutoGenerated" : true }, "Reading and comparing folders" : { @@ -1598,6 +1649,10 @@ }, "Regexp error: %@" : { + }, + "Regular Expression" : { + "comment" : "Description of a find mode when the pattern is a regular expression.", + "isCommentAutoGenerated" : true }, "Regular expression doesn't compile: %@" : { @@ -1688,6 +1743,9 @@ }, "Run" : { + }, + "Same Row" : { + }, "Save" : { @@ -1739,6 +1797,10 @@ }, "Select All Folders Both Sides" : { + }, + "Select All Found" : { + "comment" : "Text for a menu item that allows the user to select all lines that were found using the \"Find\" feature.", + "isCommentAutoGenerated" : true }, "Select Application" : { @@ -1807,6 +1869,9 @@ } } } + }, + "Selected Files" : { + }, "Selection already contains Filtered Files" : { @@ -1882,6 +1947,9 @@ }, "Show Log Console" : { + }, + "Show Preview" : { + }, "Show Toolbar" : { @@ -1925,9 +1993,6 @@ }, "Stopping comparison can take a while, please wait..." : { - }, - "Swap Sides" : { - }, "Sync" : { diff --git a/README.md b/README.md index 6947ad5..d9edc3b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ The app helps developers, designers, and anyone managing multiple versions of pr - 🧩 **File-level diff view** — inspect detailed content changes line-by-line (for supported file types). - 🧹 **Powerful filters** — exclude version control, backup, or temporary files (e.g., `.git`, `.svn`, `.zip`, `.DS_Store`). - 🖱️ **Drag & drop support** — compare folders by simply dragging them into the app window. -- 📦 **Export and automation** — integrate comparisons into scripts or workflows using CLI tools (if available). +- 📦 **Command line & automation** — compare from the shell with the bundled [`visdiff`](https://wiki.visualdiffer.com/unixshell.html) tool, or drive the app from your own scripts with [AppleScript](https://wiki.visualdiffer.com/externalApps.html#applescript). - ⚡ **Fast comparison engine** — optimized to handle large folder structures efficiently. For more information, visit the [VisualDiffer Wiki](https://wiki.visualdiffer.com/). @@ -67,6 +67,17 @@ brew install visualdiffer Download from [releases](https://github.com/visualdiffer/visualdiffer/releases/latest), unzip, and drag the app to Applications folder +### Command line tool + +The app bundles `visdiff`, a CLI tool to start a comparison from the shell. +It is not installed in the PATH automatically (sandbox restriction), create a symlink: + +```bash +sudo ln -s /Applications/VisualDiffer.app/Contents/Helpers/visdiff /usr/local/bin/visdiff +``` + +See the [visdiff documentation](https://wiki.visualdiffer.com/unixshell.html) for the available options. + ### Updates When the app launches, it automatically checks for updates and notifies the user when a new version is available. diff --git a/Signing-Template.xcconfig b/Signing-Template.xcconfig index 4cd6095..6c1d4a4 100644 --- a/Signing-Template.xcconfig +++ b/Signing-Template.xcconfig @@ -6,15 +6,11 @@ // Copyright (c) 2025 visualdiffer.com // -// Copy this file to Signing-local.xcconfig and edit the values -// The `*-local*` file will not be committed +// scripts/setup-local-env.sh copies this file to Signing.local.xcconfig, edit the values there +// the `*.local*` files are never committed DEVELOPMENT_TEAM = PROVISIONING_PROFILE_SPECIFIER = CODE_SIGN_STYLE = Automatic - -CODE_SIGN_ENTITLEMENTS[config=Debug] = VisualDiffer.entitlements -CODE_SIGN_ENTITLEMENTS[config=Release] = VisualDiffer.entitlements -CODE_SIGN_ENTITLEMENTS[config=DebugNoSandbox] = ./VisualDifferTests/NoSandbox.entitlements diff --git a/Sources/App/AppDelegate.swift b/Sources/App/AppDelegate.swift index 64c337e..9baacfc 100644 --- a/Sources/App/AppDelegate.swift +++ b/Sources/App/AppDelegate.swift @@ -75,4 +75,9 @@ class AppDelegate: NSObject, NSApplicationDelegate { UserDefaults.standard.register(defaults: defaultsDict) } } + + // explicitly enables secure restorable state to suppress the Xcode console warning + func applicationSupportsSecureRestorableState(_: NSApplication) -> Bool { + true + } } diff --git a/Sources/Core/Common/VDSymbol.swift b/Sources/Core/Common/VDSymbol.swift index 3dd105e..681a7cf 100644 --- a/Sources/Core/Common/VDSymbol.swift +++ b/Sources/Core/Common/VDSymbol.swift @@ -22,6 +22,7 @@ enum VDSymbol { case prevFile = "vd.file_prev" case top = "vd.top" case bottom = "vd.bottom" + case emptyPath = "square.dashed" } enum Toolbar: String, VDSymbolImage { @@ -48,6 +49,7 @@ enum VDSymbol { case comparisonMethod = "list.bullet.rectangle" case wordWrapOn = "vd.wordWrapOn" case wordWrapOff = "vd.wordWrapOff" + case compareItems = "vd.compareItems" } enum Button: String, VDSymbolImage { diff --git a/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift b/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift index b7c034e..0116f5f 100644 --- a/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift +++ b/Sources/Core/CommonPrefs/CommonPrefs+FileCompare.swift @@ -11,6 +11,7 @@ let defaultTabWidth = 4 enum FileColorAttribute: String { case added case changed + case changedInline case deleted case lineNumber case lineNumberSeparator diff --git a/Sources/Core/Document/DiffOpenerDelegate.swift b/Sources/Core/Document/DiffOpenerDelegate.swift index 90a9481..7edb4bc 100644 --- a/Sources/Core/Document/DiffOpenerDelegate.swift +++ b/Sources/Core/Document/DiffOpenerDelegate.swift @@ -27,9 +27,6 @@ public protocol DiffOpenerDelegate: AnyObject { func openPreviousDifference(from leftPath: String?, rightPath: String?, block: DiffOpenerDelegateBlock) - func hasNextDifference(from leftPath: String?, rightPath: String?) -> Bool - func hasPreviousDifference(from leftPath: String?, rightPath: String?) -> Bool - /// /// returns the parent paths of the two compared items /// if one of the passed paths is nil, the other is used to infer the missing parent. diff --git a/Sources/Core/Document/VDDocument.swift b/Sources/Core/Document/VDDocument.swift index a5bac17..3345c3c 100644 --- a/Sources/Core/Document/VDDocument.swift +++ b/Sources/Core/Document/VDDocument.swift @@ -22,6 +22,8 @@ public class VDDocument: NSPersistentDocument { // see http://lists.apple.com/archives/cocoa-dev/2012/Sep/msg00428.html private var isClosed = false + private var isMarkingEdited = true + let uuid = ProcessInfo.processInfo.globallyUniqueString var parentSession: DiffOpenerDelegate? @@ -56,8 +58,30 @@ public class VDDocument: NSPersistentDocument { } } + // preserves the current opt-out and suppresses the Xcode console warning + override public class var autosavesInPlace: Bool { + false + } + // MARK: - init + override public init() { + super.init() + + // the app has no undo/redo, the Core Data undo manager would only feed the document change + // tracking and its snapshots can roll back the model while the document is closing + if let moc = managedObjectContext { + moc.undoManager = nil + + NotificationCenter.default.addObserver( + self, + selector: #selector(sessionObjectsDidChange), + name: NSManagedObjectContext.didChangeObjectsNotification, + object: moc + ) + } + } + // —initWithType:error:—that is called only when a new document is created, not when it is subsequently reopened. @MainActor convenience init(type _: String) throws { @@ -67,9 +91,8 @@ public class VDDocument: NSPersistentDocument { return } - moc.rollback() - - moc.updateWithoutRecordingModifications { + updateWithoutMarkingEdited { + moc.rollback() _sessionDiff = SessionDiff.newObject(moc) if let sessionDiff { VDDocumentController.shared.fillSessionDiff(sessionDiff) @@ -81,7 +104,10 @@ public class VDDocument: NSPersistentDocument { try super.read(from: absoluteURL, ofType: typeName) try MainActor.assumeIsolated { - try isReadSessionDiffValid() + // the sessionDiff is fetched here and awakeFromFetch can rewrite migrated values + try updateWithoutMarkingEdited { + try isReadSessionDiffValid() + } } } @@ -212,6 +238,10 @@ public class VDDocument: NSPersistentDocument { } isClosed = true + // the change notification is posted only when the context processes its pending changes, + // so flush them or a change made during this same event is not counted yet + managedObjectContext?.processPendingChanges() + // TODO: update only if document is edited but do not call self.isDocumentEdited because is overridden if super.isDocumentEdited { HistorySessionManager.shared.update(document: self, closeDocument: true) @@ -249,6 +279,7 @@ public class VDDocument: NSPersistentDocument { if CommonPrefs.shared.bool(forKey: .dontAskSave) { return false } + return super.isDocumentEdited } @@ -321,6 +352,38 @@ public class VDDocument: NSPersistentDocument { storeOptions: options ) } + + // update the sessionDiff without considering it a change made by the user + func updateWithoutMarkingEdited(_ block: () throws -> Void) rethrows { + // changes made before the block are the user's, mark them before turning the flag off + managedObjectContext?.processPendingChanges() + + isMarkingEdited = false + defer { + // flush the changes so the notification is delivered while the flag is still off + managedObjectContext?.processPendingChanges() + isMarkingEdited = true + } + + try block() + } + + @objc + private func sessionObjectsDidChange(_ notification: Notification) { + guard isMarkingEdited else { + return + } + + // a refreshed or invalidated object is not a modification + let changeKeys = [NSInsertedObjectsKey, NSUpdatedObjectsKey, NSDeletedObjectsKey] + let isChanged = changeKeys.contains { + !((notification.userInfo?[$0] as? Set)?.isEmpty ?? true) + } + + if isChanged { + updateChangeCount(.changeDone) + } + } } public extension Notification.Name { diff --git a/Sources/Core/Document/VDDocumentController.swift b/Sources/Core/Document/VDDocumentController.swift index dacb97c..973392a 100644 --- a/Sources/Core/Document/VDDocumentController.swift +++ b/Sources/Core/Document/VDDocumentController.swift @@ -149,8 +149,8 @@ class VDDocumentController: NSDocumentController { return nil } - moc.rollback() - moc.updateWithoutRecordingModifications { + doc.updateWithoutMarkingEdited { + moc.rollback() doc.sessionDiff = SessionDiff.newObject(moc) block(doc) } diff --git a/Sources/Core/Document/Window/DocumentWindow.swift b/Sources/Core/Document/Window/DocumentWindow.swift index b2da537..d0c3a5e 100644 --- a/Sources/Core/Document/Window/DocumentWindow.swift +++ b/Sources/Core/Document/Window/DocumentWindow.swift @@ -108,6 +108,7 @@ class DocumentWindow: NSWindow, FileDropImageViewDelegate, HistoryControllerDele minSize = NSSize(width: 600, height: 330) setupViews() + setupKeyViewLoop() } private func setupViews() { @@ -279,7 +280,10 @@ class DocumentWindow: NSWindow, FileDropImageViewDelegate, HistoryControllerDele // MARK: - History Controller - func history(controller _: HistoryController, doubleClickedEntity _: HistoryEntity?) { + func history(controller: HistoryController, doubleClickedEntity entity: HistoryEntity?) { + if let entity { + history(controller: controller, selectedEntities: [entity]) + } showDiffs(nil) } @@ -358,4 +362,24 @@ class DocumentWindow: NSWindow, FileDropImageViewDelegate, HistoryControllerDele center() makeKeyAndOrderFront(self) } + + private func setupKeyViewLoop() { + autorecalculatesKeyViewLoop = false + + let keyViews: [NSView] = [ + leftPathChooser.comboBox, + rightPathChooser.comboBox, + leftPathChooser.chooseButton, + rightPathChooser.chooseButton, + sessionButton, + recentPopup, + compareButton, + searchHistory, + historyController.tableView, + ] + + zip(keyViews, keyViews.dropFirst()).forEach { $0.nextKeyView = $1 } + keyViews.last?.nextKeyView = keyViews.first + initialFirstResponder = keyViews.first + } } diff --git a/Sources/Core/Document/Window/PathChooser.swift b/Sources/Core/Document/Window/PathChooser.swift index 9bd97da..f5eb49c 100644 --- a/Sources/Core/Document/Window/PathChooser.swift +++ b/Sources/Core/Document/Window/PathChooser.swift @@ -63,7 +63,7 @@ class PathChooser: NSObject, NSComboBoxDelegate { return view }() - private lazy var comboBox: NSComboBox = { + private(set) lazy var comboBox: NSComboBox = { let view = NSComboBox(frame: .zero) view.completes = true @@ -78,7 +78,7 @@ class PathChooser: NSObject, NSComboBoxDelegate { return view }() - private var chooseButton: NSButton + private(set) var chooseButton: NSButton private var comboBoxPaths: NSArrayController init( diff --git a/Sources/Features/FilesCompare/Components/FileInfoBar.swift b/Sources/Features/FilesCompare/Components/FileInfoBar.swift index 6171ff8..c3ac22e 100644 --- a/Sources/Features/FilesCompare/Components/FileInfoBar.swift +++ b/Sources/Features/FilesCompare/Components/FileInfoBar.swift @@ -8,6 +8,7 @@ private let separatorWidth: CGFloat = 1.0 private let itemSpacing: CGFloat = 6.0 +private let eolTrailingMargin: CGFloat = 6.0 private let separatorColor = NSColor(calibratedWhite: 0.52, alpha: 1.0) protocol FileInfoBarDelegate: AnyObject { @@ -15,11 +16,11 @@ protocol FileInfoBarDelegate: AnyObject { } class FileInfoBar: NSView { - private lazy var labelText: NSTextField = createLabelText() - private lazy var encodingPopup: NSPopUpButton = createEncodingPopup() - private lazy var eolText: NSTextField = createEolText() + private lazy var labelText = createClippedText() + private lazy var encodingPopup = createEncodingPopup() + private lazy var eolText = createClippedText() - var delegate: FileInfoBarDelegate? + weak var delegate: FileInfoBarDelegate? var encoding: String.Encoding? { get { @@ -58,9 +59,9 @@ class FileInfoBar: 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 } private func setupViews() { @@ -81,27 +82,15 @@ class FileInfoBar: NSView { NSLayoutConstraint.activate([ stackView.topAnchor.constraint(equalTo: topAnchor), - stackView.trailingAnchor.constraint(equalTo: trailingAnchor), + stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -eolTrailingMargin), stackView.bottomAnchor.constraint(equalTo: bottomAnchor), ]) } - private func createLabelText() -> NSTextField { - let view = NSTextField(frame: .zero) + private func createClippedText() -> NSTextField { + let view = NSTextField.hintWithTitle("") - view.isEditable = false - view.isBordered = false - view.drawsBackground = false - view.alignment = .right - view.translatesAutoresizingMaskIntoConstraints = false - - let cell = TextFieldVerticalCentered() - cell.lineBreakMode = .byClipping - - view.cell = cell - - // set the font after the cell otherwise it is lost - view.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + view.lineBreakMode = .byClipping return view } @@ -120,25 +109,6 @@ class FileInfoBar: NSView { return view } - private func createEolText() -> NSTextField { - let view = NSTextField(frame: .zero) - - view.isEditable = false - view.isBordered = false - view.drawsBackground = false - view.translatesAutoresizingMaskIntoConstraints = false - - let cell = TextFieldVerticalCentered() - cell.lineBreakMode = .byClipping - - view.cell = cell - - // set the font after the cell otherwise it is lost - view.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) - - return view - } - private func createSeparator() -> NSView { let view = NSView() view.wantsLayer = true diff --git a/Sources/Features/FilesCompare/Components/FilePanelView+File.swift b/Sources/Features/FilesCompare/Components/FilePanelView+File.swift index 82a4582..094f0d1 100644 --- a/Sources/Features/FilesCompare/Components/FilePanelView+File.swift +++ b/Sources/Features/FilesCompare/Components/FilePanelView+File.swift @@ -8,9 +8,9 @@ extension FilePanelView { func readFile(_ path: URL) throws -> String { - treeView.isDirty = false fileInfoBar.encoding = CommonPrefs.shared.defaultEncoding fileInfoBar.fileAttrs = nil + fileInfoBar.eol = .missing let secureURL = SecureBookmark.shared.secure(fromBookmark: path, startSecured: true) defer { @@ -21,7 +21,13 @@ extension FilePanelView { fileInfoBar.fileAttrs = try FileManager.default.attributesOfItem(atPath: path.osPath) - return try readContent(path) + let content = try readContent(path) + + // the panel is showing this file only now, a read that threw earlier must leave + // the flag alone or unsaved edits are discarded without asking + treeView.isDirty = false + + return content } private func readContent(_ path: URL) throws -> String { diff --git a/Sources/Features/FilesCompare/Components/FilePanelView.swift b/Sources/Features/FilesCompare/Components/FilePanelView.swift index 76deb25..2a27870 100644 --- a/Sources/Features/FilesCompare/Components/FilePanelView.swift +++ b/Sources/Features/FilesCompare/Components/FilePanelView.swift @@ -70,15 +70,7 @@ class FilePanelView: TablePanelView { ]) } - func setSliderMaxValue(_ left: [DiffLine], right: [DiffLine]) { - var maxColumn = 0 - - for (index, line) in left.enumerated() { - let tempMax = max(line.text.count, right[index].text.count) - if tempMax > maxColumn { - maxColumn = tempMax - } - } + func setSliderMaxValue(_ maxColumn: Int) { columnSlider.maxValue = Double(maxColumn) } diff --git a/Sources/Features/FilesCompare/Components/FilesScopeBar.swift b/Sources/Features/FilesCompare/Components/FilesScopeBar.swift index a0d9cdd..af714bf 100644 --- a/Sources/Features/FilesCompare/Components/FilesScopeBar.swift +++ b/Sources/Features/FilesCompare/Components/FilesScopeBar.swift @@ -43,7 +43,7 @@ class FilesScopeBar: ScopeBarView { } } - var actionDelegate: FilesScopeBarDelegate? + weak var actionDelegate: FilesScopeBarDelegate? let findView = FindText(frame: .zero) diff --git a/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableView.swift b/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableView.swift index e802a07..f1a40fc 100644 --- a/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableView.swift +++ b/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableView.swift @@ -109,6 +109,10 @@ class FilesTableView: NSTableView, @preconcurrency DisplayPositionable, ViewLink filesTableDelegate?.filesTableView(self, scrollHorizontally: true) } else if keyCode == .rightArrow, !isArrowKeyDownWithModifiers { filesTableDelegate?.filesTableView(self, scrollHorizontally: false) + } else if keyCode == .tab, let linkedView { + // tab moves the focus between the two panels + // the key view loop is not used because it walks out of the panels + window?.makeFirstResponder(linkedView) } else { super.keyDown(with: event) } diff --git a/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift b/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift index 21a867e..9393c30 100644 --- a/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift +++ b/Sources/Features/FilesCompare/Components/FilesTableView/FilesTableViewFindTextDelegate.swift @@ -16,37 +16,20 @@ class FilesTableViewFindTextDelegate: NSObject, @preconcurrency FindTextDelegate self.view = view } - func find(findText _: FindText, searchPattern pattern: String) -> Bool { - let globPattern = pattern.convertGlobMetaCharsToRegexpMetaChars() - guard let re = try? NSRegularExpression( - pattern: globPattern, - options: .caseInsensitive - ), - let left = view.diffSide?.lines, - let right = view.linkedView?.diffSide?.lines else { + func find(findText: FindText, searchPattern pattern: String) -> Bool { + guard let matcher = findText.findOptions.matcher(for: pattern), + let left = view.diffSide?.lines, + let right = view.linkedView?.diffSide?.lines else { return false } + // a line matches when the text is found on any of the two sides for i in 0 ..< left.count { - var line = left[i].text - var range = re.rangeOfFirstMatch( - in: line, - options: [], - 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.utf16.count) - ) - } - - if range.location != NSNotFound { + if matcher(left[i].text) || matcher(right[i].text) { lines.append(i) } } + return true } @@ -59,10 +42,7 @@ class FilesTableViewFindTextDelegate: NSObject, @preconcurrency FindTextDelegate } if row < count { - view.scrollRowToVisible(row) - let indexes = IndexSet(integer: row) - view.selectRowIndexes(indexes, byExtendingSelection: false) - view.linkedView?.selectRowIndexes(indexes, byExtendingSelection: false) + select(rows: IndexSet(integer: row)) return true } @@ -78,4 +58,29 @@ class FilesTableViewFindTextDelegate: NSObject, @preconcurrency FindTextDelegate func clearMatches(in _: FindText) { lines.removeAll() } + + func selectAllMatches(in _: FindText, side: DisplaySide) { + guard let dataSource = view.dataSource, + let count = dataSource.numberOfRows?(in: view) else { + return + } + + select(rows: IndexSet(lines.filter { $0 < count }), side: side) + } + + private func select(rows: IndexSet) { + select(rows: rows, side: view.side) + select(rows: rows, side: view.side.opposite) + } + + private func select(rows: IndexSet, side: DisplaySide) { + // the rows of the two sides are aligned + guard let firstRow = rows.first, + let target = side == view.side ? view : view.linkedView else { + return + } + + target.scrollRowToVisible(firstRow) + target.selectRowIndexes(rows, byExtendingSelection: false) + } } diff --git a/Sources/Features/FilesCompare/Components/LineDetailTextView.swift b/Sources/Features/FilesCompare/Components/LineDetailTextView.swift index 9d67c3c..5776407 100644 --- a/Sources/Features/FilesCompare/Components/LineDetailTextView.swift +++ b/Sources/Features/FilesCompare/Components/LineDetailTextView.swift @@ -38,6 +38,13 @@ final class LineDetailTextView: NSTextView { return NSRange(location: selectableLength, length: clampedSuffixLength) } + // the TextKit 2 layout engine sizes the view by laying out the entire line, and this + // view can render lines of any length: laying out a single-line minified file takes + // about a second, so the view uses the TextKit 1 stack, which measures text lazily + convenience init() { + self.init(usingTextLayoutManager: false) + } + override func selectionRange( forProposedRange proposedCharRange: NSRange, granularity: NSSelectionGranularity @@ -87,12 +94,16 @@ final class LineDetailTextView: NSTextView { } func updateContent( + _ diffLine: DiffLine, text: String, lineEnding: String, - textColor: NSColor?, - backgroundColor: NSColor? + highlightRanges: [Range] ) { - string = text + lineEnding + let textColor = diffLine.colors?.text + let backgroundColor = diffLine.colors?.background + let content = text + lineEnding + + string = content // use the NSString length because NSTextView selection ranges are NSRange-based protectedSuffixLength = (lineEnding as NSString).length @@ -102,6 +113,7 @@ final class LineDetailTextView: NSTextView { setTextColor(.controlTextColor, backgroundColor: .clear) } + applyInlineHighlight(in: content, ranges: highlightRanges, colors: diffLine.inlineColors) applyLineEndingStyle(textColor: textColor, backgroundColor: backgroundColor) } @@ -183,6 +195,21 @@ final class LineDetailTextView: NSTextView { return (lighterColor + Self.contrastRatioBias) / (darkerColor + Self.contrastRatioBias) } + + private func applyInlineHighlight(in content: String, ranges: [Range], colors: ColorSet?) { + guard !ranges.isEmpty, + let textStorage, + let inlineColors = colors else { + return + } + + for range in content.characterRanges(from: ranges) { + let highlightRange = NSRange(range, in: content) + + textStorage.addAttribute(.foregroundColor, value: inlineColors.text, range: highlightRange) + textStorage.addAttribute(.backgroundColor, value: inlineColors.background, range: highlightRange) + } + } } private extension NSColor { diff --git a/Sources/Features/FilesCompare/Components/LineNumberTableCellView.swift b/Sources/Features/FilesCompare/Components/LineNumberTableCellView.swift index 8b200b3..ecdce0a 100644 --- a/Sources/Features/FilesCompare/Components/LineNumberTableCellView.swift +++ b/Sources/Features/FilesCompare/Components/LineNumberTableCellView.swift @@ -87,31 +87,48 @@ class LineNumberTableCellView: NSTableCellView { } } + // character offsets relative to formattedText + var inlineRanges = [Range]() { + didSet { + if inlineRanges != oldValue { + updateContent() + } + } + } + var font: NSFont? { didSet { lineNumberTextField.font = font contentTextField.font = font - updateContent() + + // a recycled cell keeps the font of the panel, redrawing it again would + // rebuild the whole content for nothing + if font != oldValue { + updateContent() + } } } var isWordWrapEnabled: Bool = false { didSet { updateWordWrapSettings() - updateContent() + + if isWordWrapEnabled != oldValue { + updateContent() + } } } var isHighlighted = false { didSet { - updateColors() + updateColorsAndText() } } dynamic var isSelected: Bool = false { didSet { if oldValue != isSelected { - updateColors() + updateColorsAndText() } } } @@ -123,9 +140,9 @@ class LineNumberTableCellView: NSTableCellView { setupConstraints() } - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + @available(*, unavailable, message: "use init(frame:)") + required init?(coder _: NSCoder) { + nil } // MARK: - Setup @@ -204,14 +221,62 @@ class LineNumberTableCellView: NSTableCellView { if diffLine.number > 0 { showLineNumberAndText() lineNumberTextField.stringValue = String(format: "%ld", diffLine.number) - contentTextField.stringValue = formattedText ?? diffLine.text updateColors() + updateText(diffLine) updateSectionSeparator() } else { showMissingLineImage() } } + /// the highlighted text has to be rebuilt together with the colors, because textColor + /// does not override the foreground attributes of an attributed string, a plain text + /// follows textColor and can be left alone + private func updateColorsAndText() { + updateColors() + + if !inlineRanges.isEmpty, let diffLine, diffLine.number > 0 { + updateText(diffLine) + } + } + + private func updateText(_ diffLine: DiffLine) { + guard let text = formattedText, + !inlineRanges.isEmpty, + let font, + let inlineColors = diffLine.inlineColors else { + contentTextField.stringValue = formattedText ?? diffLine.text + return + } + + let paragraphStyle = NSMutableParagraphStyle() + + paragraphStyle.lineBreakMode = contentTextField.lineBreakMode + + // the highlight changes colors only, a bolder font would change the glyph + // advances and break the alignment between the two panels + let normalStyle: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: diffLine.color(for: .text, isSelected: isSelected), + .paragraphStyle: paragraphStyle, + ] + // the background is painted over the selection too, whitespace has no ink to + // colour, so without the patch a difference made of spaces or tabs would show + // nothing on a selected row + let highlightStyle: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: inlineColors.text, + .paragraphStyle: paragraphStyle, + .backgroundColor: inlineColors.background, + ] + + contentTextField.attributedStringValue = text.highlights( + text.characterRanges(from: inlineRanges), + normalStyle: normalStyle, + highlightStyle: highlightStyle + ) + } + private func showLineNumberAndText() { lineNumberTextField.isHidden = false contentTextField.isHidden = false diff --git a/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift b/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift index 57f52a0..01c633a 100644 --- a/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift +++ b/Sources/Features/FilesCompare/Components/RowHeightCalculator.swift @@ -28,6 +28,9 @@ private let verticalPadding: CGFloat = 4.0 class RowHeightCalculator { private var heightCache: [Int: CGFloat] = [:] + // the column widths the cached heights were computed for + private var cachedWidths = (left: CGFloat.zero, right: CGFloat.zero) + weak var dataSource: RowHeightDataSource? var isWordWrapEnabled: Bool = false { @@ -38,6 +41,7 @@ class RowHeightCalculator { func clearCache() { heightCache.removeAll(keepingCapacity: true) + cachedWidths = (0, 0) } func height(for row: Int) -> CGFloat { @@ -104,4 +108,23 @@ class RowHeightCalculator { clearCache() dataSource?.reloadTableData() } + + // the caller doesn't know what the heights depend on: without word wrap they are the + // font line height, so any resize leaves them alone, and with word wrap only a change + // of the column width makes them stale + func reloadDataIfWidthChanged() { + guard isWordWrapEnabled, + let dataSource else { + return + } + + let widths = (left: dataSource.columnWidth(at: .left), right: dataSource.columnWidth(at: .right)) + + if widths == cachedWidths { + return + } + + reloadData() + cachedWidths = widths + } } diff --git a/Sources/Features/FilesCompare/Components/TextFieldVerticalCentered.swift b/Sources/Features/FilesCompare/Components/TextFieldVerticalCentered.swift deleted file mode 100644 index 7306507..0000000 --- a/Sources/Features/FilesCompare/Components/TextFieldVerticalCentered.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// TextFieldVerticalCentered.swift -// VisualDiffer -// -// Created by davide ficano on 24/11/12. -// Copyright (c) 2012 visualdiffer.com -// - -class TextFieldVerticalCentered: NSTextFieldCell { - override func titleRect(forBounds rect: NSRect) -> NSRect { - let titleSize = attributedStringValue.size() - var titleFrame = super.titleRect(forBounds: rect) - titleFrame.origin.y = rect.origin.y + (rect.size.height - titleSize.height) / 2.0 - - return titleFrame - } - - override func drawInterior(withFrame cellFrame: NSRect, in _: NSView) { - let titleRect = titleRect(forBounds: cellFrame) - attributedStringValue.draw(in: titleRect) - } -} diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Common.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Common.swift index 1f99c12..9c5aabf 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Common.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Common.swift @@ -6,13 +6,6 @@ // Copyright (c) 2025 visualdiffer.com // -func swap(_ lhs: inout T, _ rhs: inout T) { - let tmp = lhs - - lhs = rhs - rhs = tmp -} - extension FilesWindowController { // MARK: - Refresh after edit @@ -24,7 +17,7 @@ extension FilesWindowController { // refresh sections otherwise moving between differences can position to wrong line diffResult.refreshSections() differenceCounters.update(counters: DiffCountersItem.diffCounter(withResult: diffResult)) - cachedLineTextMap.removeAllObjects() + cachedLineTextMap.removeAll() let selectedRow = min( selectedRow < 0 ? lastUsedView.selectedRow : selectedRow, @@ -87,28 +80,64 @@ extension FilesWindowController { let lineEnding = scopeBar.showWhitespaces ? "" : diffLine.component.eol.visibleSymbol lineView.updateContent( + diffLine, text: getLine(diffLine), lineEnding: lineEnding, - textColor: diffLine.colors?.text, - backgroundColor: diffLine.colors?.background + highlightRanges: inlineDisplayRanges(diffLine) ) } // MARK: - Cache lines - func getLine(_ diffLine: DiffLine) -> String { - if let line = cachedLineTextMap.object(forKey: diffLine) as? String { - return line as String - } - let line = visibleWhitespaces.getString( + // only the text is cached, the offset mapping costs an array per line and is needed + // by the few changed lines carrying inline differences, so it is rebuilt on demand + func displayLine(_ diffLine: DiffLine) -> DisplayLine { + visibleWhitespaces.getString( diffLine.component, isWhitespacesVisible: scopeBar.showWhitespaces ) - cachedLineTextMap.setObject(line as NSString, forKey: diffLine) + } + + func getLine(_ diffLine: DiffLine) -> String { + if let line = cachedLineTextMap[diffLine] { + return line + } + let line = displayLine(diffLine).text + + cachedLineTextMap[diffLine] = line return line } + // the offsets are relative to the text drawn from the given column, so the highlight + // follows the horizontal scrolling of the panels + func inlineDisplayRanges(_ diffLine: DiffLine, startingColumn: Int = 0) -> [Range] { + guard diffLine.type == .changed, !diffLine.inlineRanges.isEmpty else { + return [] + } + + let display = displayLine(diffLine) + let visibleCount = display.text.count - startingColumn + + if visibleCount < 0 { + return [] + } + var ranges = [Range]() + + ranges.reserveCapacity(diffLine.inlineRanges.count) + + for range in diffLine.inlineRanges { + let lower = max(0, display.displayOffset(for: range.lowerBound) - startingColumn) + let upper = min(visibleCount, display.displayOffset(for: range.upperBound) - startingColumn) + + if lower < upper { + ranges.append(lower ..< upper) + } + } + + return ranges + } + // MARK: - Actions @objc @@ -117,16 +146,18 @@ extension FilesWindowController { return } + reloadRestoringPosition() + } + + func reloadRestoringPosition(statusMessage: String? = nil) { let index = leftView.selectedRowIndexes let row = leftView.firstVisibleRow - reloadAllMove(toFirstDifference: false) - - if row <= leftView.numberOfRows { - leftView.scrollTo(row: row, center: false) - leftView.selectRowIndexes(index, byExtendingSelection: false) - } else { - moveToDifference(true, showAnim: true, moveToFile: false) + // the restore travels with the comparison, a refused or queued request leaves the + // views untouched and restoring a position over them would fight the run that + // eventually happens + startReload(toFirstDifference: false, statusMessage: statusMessage) { [self] in + restoreSelectionAndScrollPosition(index, firstVisibleRow: row) } } @@ -136,28 +167,21 @@ extension FilesWindowController { return } - let row = lastUsedView.selectedRow + // the focused panel can change while the comparison runs, the restore + // belongs to the one the row was taken from + let view = lastUsedView + let row = view.selectedRow compare( leftLines: diffResult.leftSide.nonMissingLineComponents(), rightLines: diffResult.rightSide.nonMissingLineComponents(), moveToFirstDifference: false - ) - - lastUsedView.scrollTo(row: row, center: true) - lastUsedView.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: true) - } - - @objc - func swapSides(_: AnyObject) { - swap(&sessionDiff.leftPath, &sessionDiff.rightPath) - swap(&leftView.diffSide, &rightView.diffSide) - swap(&leftPanelView.fileInfoBar.fileAttrs, &rightPanelView.fileInfoBar.fileAttrs) - swap(&leftPanelView.fileInfoBar.encoding, &rightPanelView.fileInfoBar.encoding) - swap(&leftPanelView.fileInfoBar.eol, &rightPanelView.fileInfoBar.eol) - - reloadRowHeights() - fileThumbnail.needsDisplay = true + ) { + if (0 ..< view.numberOfRows).contains(row) { + view.scrollTo(row: row, center: true) + view.selectRowIndexes(IndexSet(integer: row), byExtendingSelection: true) + } + } } @objc @@ -182,6 +206,23 @@ extension FilesWindowController { scopeBar.findView.moveToMatch(true) } + @objc + func selectAllFound(_ sender: AnyObject) { + guard let sender = sender as? NSMenuItem else { + return + } + + let side = SelectionSide(menuItem: sender) + let findView = scopeBar.findView + + if side.contains(.left) { + findView.delegate?.selectAllMatches(in: findView, side: .left) + } + if side.contains(.right) { + findView.delegate?.selectAllMatches(in: findView, side: .right) + } + } + @objc func setLeftReadOnly(_: AnyObject) { sessionDiff.leftReadOnly.toggle() @@ -191,4 +232,22 @@ extension FilesWindowController { func setRightReadOnly(_: AnyObject) { sessionDiff.rightReadOnly.toggle() } + + private func restoreSelectionAndScrollPosition( + _ selectedRows: IndexSet, + firstVisibleRow: Int + ) { + // the comparison can return fewer rows than the restored ones + let rowCount = leftView.numberOfRows + + if (0 ..< rowCount).contains(firstVisibleRow) { + leftView.scrollTo(row: firstVisibleRow, center: false) + leftView.selectRowIndexes( + selectedRows.filteredIndexSet { $0 < rowCount }, + byExtendingSelection: false + ) + } else { + moveToDifference(true, showAnim: true, moveToFile: false) + } + } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+ConsoleViewDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+ConsoleViewDelegate.swift new file mode 100644 index 0000000..6226b09 --- /dev/null +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+ConsoleViewDelegate.swift @@ -0,0 +1,19 @@ +// +// FilesWindowController+ConsoleViewDelegate.swift +// VisualDiffer +// +// Created by davide ficano on 11/09/26 +// Copyright (c) 2026 visualdiffer.com +// + +extension FilesWindowController: ConsoleDisplayable { + var consoleFocusView: NSView { + lastUsedView + } + + // a menu item needs a real selector, a protocol extension cannot provide one + @objc + func toggleLogConsole(_: AnyObject?) { + hide(console: consoleView) + } +} diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Document.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Document.swift index 1307675..8090f69 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Document.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Document.swift @@ -33,6 +33,11 @@ extension FilesWindowController: @preconcurrency DocumentWindowControllerDelegat leftView.isDirty = false rightView.isDirty = false + + // the change notification is posted only when the context processes its + // pending changes, the answer below must not miss an edit of this event + document.managedObjectContext?.processPendingChanges() + if document.isDocumentEdited { leftView.isDirty = tmpLeftDirty rightView.isDirty = tmpRightDirty diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift index 1d54306..c638a60 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+FilesTableViewContextMenu.swift @@ -26,17 +26,17 @@ extension FilesWindowController: @preconcurrency FilesTableViewContextMenu { @objc func showWhitespaces(_ sender: AnyObject?) { - cachedLineTextMap.removeAllObjects() + cachedLineTextMap.removeAll() // the scopeBar button toggles automatically but when this method is called // from the menu we set it manually if sender !== scopeBar { scopeBar.showWhitespaces(!scopeBar.showWhitespaces, informDelegate: false) } - updateDetailLines(leftView.selectedRow) - leftView.reloadData(restoreSelection: true) rightView.reloadData(restoreSelection: true) + + updateDetailLines(lastUsedView.selectedRow) } @objc @@ -99,8 +99,14 @@ extension FilesWindowController: @preconcurrency FilesTableViewContextMenu { @objc func deleteLines(_: AnyObject?) { let selectedRows = lastUsedView.selectedRowIndexes - - guard !selectedRows.isEmpty, + let isReadOnly = lastUsedView.side == .left ? sessionDiff.leftReadOnly : sessionDiff.rightReadOnly + + // the delete key reaches this action without going through + // any validation, so the checks the menu makes have to be repeated here + guard !isComparing, + !isReadOnly, + lastUsedView.isEditAllowed, + !selectedRows.isEmpty, let diffResult, let currentDiffResult else { return @@ -146,7 +152,7 @@ extension FilesWindowController: @preconcurrency FilesTableViewContextMenu { } else { lastUsedView } - saveViewAndRefreshUI(view) + saveViewShowingErrors(view) } // MARK: - Open with external applications diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Menu.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Menu.swift index 854daf1..a0636aa 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Menu.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Menu.swift @@ -77,6 +77,31 @@ extension FilesWindowController { action: #selector(findPrevious), keyEquivalent: "G" ) + + let selectAllFoundItem = NSMenuItem( + title: NSLocalizedString("Select All Found", comment: ""), + action: nil, + keyEquivalent: "" + ) + let selectAllFoundSub = NSMenu(title: NSLocalizedString("Select All Found", comment: "")) + selectAllFoundSub.addItem( + withTitle: NSLocalizedString("Left Side", comment: ""), + action: #selector(selectAllFound), + keyEquivalent: "" + ).tag = SelectionSide.left.rawValue + selectAllFoundSub.addItem( + withTitle: NSLocalizedString("Right Side", comment: ""), + action: #selector(selectAllFound), + keyEquivalent: "" + ).tag = SelectionSide.right.rawValue + selectAllFoundSub.addItem( + withTitle: NSLocalizedString("Both Sides", comment: ""), + action: #selector(selectAllFound), + keyEquivalent: "" + ).tag = SelectionSide.both.rawValue + findSubmenu.setSubmenu(selectAllFoundSub, for: selectAllFoundItem) + findSubmenu.addItem(selectAllFoundItem) + editMenu.setSubmenu(findSubmenu, for: findItem) editMenu.addItem(findItem) @@ -147,11 +172,6 @@ extension FilesWindowController { ) viewMenu.addItem(NSMenuItem.separator()) - viewMenu.addItem( - withTitle: NSLocalizedString("Swap Sides", comment: ""), - action: #selector(swapSides), - keyEquivalent: "" - ) viewMenu.addItem( withTitle: NSLocalizedString("Word Wrap", comment: ""), action: #selector(toggleWordWrap), @@ -200,6 +220,14 @@ extension FilesWindowController { keyEquivalent: "" ) + let logConsole = NSMenuItem( + title: NSLocalizedString("Show Log Console", comment: ""), + action: #selector(toggleLogConsole), + keyEquivalent: "l" + ) + logConsole.keyEquivalentModifierMask = [.option, .command] + viewMenu.addItem(logConsole) + let toolbarItem = NSMenuItem( title: NSLocalizedString("Show Toolbar", comment: ""), action: #selector(NSWindow.toggleToolbarShown), diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+MenuDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+MenuDelegate.swift index 5203dbf..dfdc491 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+MenuDelegate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+MenuDelegate.swift @@ -29,6 +29,14 @@ extension FilesWindowController: NSMenuDelegate, NSMenuItemValidation { // called for menu bar items public func validateMenuItem(_ item: NSMenuItem) -> Bool { + // the titles, the hidden flags and the submenus are refreshed even while a + // comparison runs, only the action is refused + let isValid = validateMenuItemState(item) + + return isComparing ? false : isValid + } + + private func validateMenuItemState(_ item: NSMenuItem) -> Bool { let action = item.action let isLeftView = lastUsedView.side == .left @@ -47,22 +55,16 @@ extension FilesWindowController: NSMenuDelegate, NSMenuItemValidation { for: item ) return true + } else if action == #selector(selectAllFound) { + return scopeBar.findView.hasMatches } else if action == #selector(saveFile) { return lastUsedView.isDirty } else if action == #selector(saveLeftFile) { return leftView.isDirty } else if action == #selector(saveRightFile) { return rightView.isDirty - } else if action == #selector(previousDifference) { - return canMoveToDifference( - gotoNext: false, - moveToFile: CommonPrefs.shared.fileAutoAdvanceWhenNoMoreDifferences - ) - } else if action == #selector(nextDifference) { - return canMoveToDifference( - gotoNext: true, - moveToFile: CommonPrefs.shared.fileAutoAdvanceWhenNoMoreDifferences - ) + } else if action == #selector(previousDifference) || action == #selector(nextDifference) { + return canMoveToDifference() } else if action == #selector(copyLinesToLeft) { if lastUsedView.side == .right { item.isHidden = false @@ -87,7 +89,11 @@ extension FilesWindowController: NSMenuDelegate, NSMenuItemValidation { item.title = NSLocalizedString("Delete Lines from Right", comment: "") return !sessionDiff.rightReadOnly && rightView.isEditAllowed } - } else if action == #selector(pasteLinesToClipboard) || action == #selector(cutToClipboard) { + } else if action == #selector(pasteLinesToClipboard) + || action == #selector(cutToClipboard) + // the Edit menu binds the public actions, they must not skip these checks + || action == #selector(paste(_:)) + || action == #selector(cut(_:)) { if scopeBar.showLinesFilter == .all { if isLeftView { return !sessionDiff.leftReadOnly && leftView.isEditAllowed @@ -107,9 +113,14 @@ extension FilesWindowController: NSMenuDelegate, NSMenuItemValidation { ? NSLocalizedString("Show Details", comment: "") : NSLocalizedString("Hide Details", comment: "") return true + } else if action == #selector(toggleLogConsole) { + item.title = consoleSplitter.hasSubviewCollapsed + ? NSLocalizedString("Show Log Console", comment: "") + : NSLocalizedString("Hide Log Console", comment: "") + return true } else if action == #selector(previousDifferenceFiles) || action == #selector(nextDifferenceFiles) { - return (document as? VDDocument)?.parentSession != nil + return parentSession != nil } else if action == #selector(toggleWordWrap) { item.state = rowHeightCalculator.isWordWrapEnabled ? .on : .off return true diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift index f2d0cd6..e5e5a7d 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSToolbarDelegate.swift @@ -171,22 +171,18 @@ extension FilesWindowController: NSToolbarDelegate, NSToolbarItemValidation { } open func validateToolbarItem(_ item: NSToolbarItem) -> Bool { + if isComparing { + return false + } var enabled = true let side = lastUsedView.side - if item.itemIdentifier == .Files.prevDifference { - return canMoveToDifference( - gotoNext: false, - moveToFile: CommonPrefs.shared.fileAutoAdvanceWhenNoMoreDifferences - ) - } else if item.itemIdentifier == .Files.nextDifference { - return canMoveToDifference( - gotoNext: true, moveToFile: - CommonPrefs.shared.fileAutoAdvanceWhenNoMoreDifferences - ) + if item.itemIdentifier == .Files.prevDifference + || item.itemIdentifier == .Files.nextDifference { + return canMoveToDifference() } else if item.itemIdentifier == .Files.prevDifferenceFiles || item.itemIdentifier == .Files.nextDifferenceFiles { - return (document as? VDDocument)?.parentSession != nil + return parentSession != nil } if side == .left { diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift index 0819f61..aefe4ef 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+NSWindowDelegate.swift @@ -11,10 +11,15 @@ let statusBarShowMessageTimeoutPrefName = "filesStatusBarShowMessageTimeout" extension FilesWindowController: NSWindowDelegate { public func windowWillClose(_: Notification) { + isClosed = true + statusMessageTask?.cancel() removeObservers() if let document = document as? VDDocument { document.parentSession?.removeChildDocument(document) } + + leftPanelView.unbindControls() + rightPanelView.unbindControls() } public func windowDidBecomeMain(_: Notification) { @@ -27,8 +32,7 @@ extension FilesWindowController: NSWindowDelegate { [proposedOptions, .autoHideToolbar] } - @objc - func resetStatusBarMessage(_: AnyObject?) { + func resetStatusBarMessage() { guard let diffResult else { return } @@ -47,26 +51,46 @@ extension FilesWindowController: NSWindowDelegate { if leftChanged || rightChanged { if askReload() { + // silent reload discard, the confirm above can be suppressed and the + // reload would then throw the unsaved edits away without a word + guard alertSaveDirtyFiles() else { + return + } + 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: "") + let message = if leftChanged, rightChanged { + NSLocalizedString("Reloaded left and right files", comment: "") } else if leftChanged { - differenceCounters.stringValue = NSLocalizedString("Reloaded left file", comment: "") - } else if rightChanged { - differenceCounters.stringValue = NSLocalizedString("Reloaded right file", comment: "") + NSLocalizedString("Reloaded left file", comment: "") + } else { + NSLocalizedString("Reloaded right file", comment: "") } - perform( - #selector(resetStatusBarMessage), - with: nil, - afterDelay: UserDefaults.standard.double(forKey: statusBarShowMessageTimeoutPrefName) - ) + + // the comparison refreshes the counters, the confirmation travels + // with the request so it is shown by whichever run re-reads the file + reloadRestoringPosition(statusMessage: message) + } + } + } + + func showStatusMessage(_ message: String) { + differenceCounters.stringValue = message + + statusMessageTask?.cancel() + statusMessageTask = Task { + let timeout = UserDefaults.standard.double(forKey: statusBarShowMessageTimeoutPrefName) + + // a cancelled wait means a newer message took over, the counters must stay away + guard await (try? Task.sleep(for: .seconds(timeout))) != nil else { + return } + + resetStatusBarMessage() } } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift index 88c71ea..8c8047a 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Navigate.swift @@ -44,24 +44,20 @@ extension FilesWindowController { currentDiffResult?.findPrevSection(by: currentPos, didWrap: &didWrap) } guard let section else { - if moveToFile, - canNavigateToFile(gotoNext: gotoNext) { - navigateToFile(gotoNext, showAnim: showAnim) + if moveToFile { + navigateToFile(gotoNext, showAnim: showAnim, showsNoFileOSD: showAnim) } return } if didWrap { - if moveToFile { - if canNavigateToFile(gotoNext: gotoNext) { - navigateToFile(gotoNext, showAnim: showAnim) - return - } - if showAnim, - !CommonPrefs.shared.fileWrapsAroundDifferences, - (document as? VDDocument)?.parentSession != nil { - showNoFileOSD(gotoNext) - } + if moveToFile, + navigateToFile( + gotoNext, + showAnim: showAnim, + showsNoFileOSD: showAnim && !CommonPrefs.shared.fileWrapsAroundDifferences + ) { + return } guard CommonPrefs.shared.fileWrapsAroundDifferences else { return @@ -79,46 +75,49 @@ extension FilesWindowController { } } - private func canNavigateToFile(gotoNext: Bool) -> Bool { + // returns whether the parent session holds a file in that direction, the search it + // costs is the same one the move needs so the two cannot be asked separately + @discardableResult + func navigateToFile( + _ navigateToNext: Bool, + showAnim: Bool = false, + showsNoFileOSD: Bool = true + ) -> Bool { guard let document = document as? VDDocument, let parentSession = document.parentSession else { return false } - if gotoNext { - return parentSession.hasNextDifference(from: sessionDiff.leftPath, rightPath: sessionDiff.rightPath) - } - return parentSession.hasPreviousDifference(from: sessionDiff.leftPath, rightPath: sessionDiff.rightPath) - } - - func navigateToFile(_ navigateToNext: Bool, showAnim: Bool = false) { - guard let document = document as? VDDocument, - let parentSession = document.parentSession else { - return - } + var hasFile = false let block: DiffOpenerDelegateBlock = { leftPath, rightPath in if leftPath == nil, rightPath == nil { - self.showNoFileOSD(navigateToNext) + // the controls stay enabled as long as a folder session is behind the + // file, so this direction can have no file at all and the press would + // otherwise do nothing + if showsNoFileOSD { + self.showNoFileOSD(navigateToNext) + } return false } + hasFile = true + + if showAnim { + self.showMoveToFileOSD(!navigateToNext) + } if !self.alertSaveDirtyFiles() { return false } - // the document is marked as dirty when we set the sessionDiff properties - // so we update leftPath and rightPath without recording modifications - document.managedObjectContext?.updateWithoutRecordingModifications { + // moving to another file of the parent session is navigation, not a session edit + document.updateWithoutMarkingEdited { self.sessionDiff.leftPath = leftPath self.sessionDiff.rightPath = rightPath - self.reloadAllMove(toFirstDifference: true) } + self.startReload(toFirstDifference: true) return true } - if showAnim { - showMoveToFileOSD(!navigateToNext) - } if navigateToNext { parentSession.openNextDifference( from: sessionDiff.leftPath, @@ -132,6 +131,8 @@ extension FilesWindowController { block: block ) } + + return hasFile } func showNoFileOSD(_ noNextFile: Bool) { @@ -158,15 +159,15 @@ extension FilesWindowController { topBottomView.animateInside(window.frame) } - func canMoveToDifference(gotoNext: Bool, moveToFile: Bool) -> Bool { + func canMoveToDifference() -> Bool { if let sections = currentDiffResult?.sections, !sections.isEmpty { return true } - guard moveToFile else { - return false - } - - return canNavigateToFile(gotoNext: gotoNext) + // asking the parent session for the next differing file walks its whole tree and + // the answer is asked again at every validation, on a large folder session that + // is half a second each time; the action itself shows the OSD when there is no + // file to move to + return CommonPrefs.shared.fileAutoAdvanceWhenNoMoreDifferences && parentSession != nil } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift index 66fcd61..c8100a0 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+PathControlDelegate.swift @@ -29,7 +29,7 @@ extension FilesWindowController: PathControlDelegate { 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) + startReload(toFirstDifference: false) } public func pathControl(_: NSPathControl, willDisplay openPanel: NSOpenPanel) { @@ -61,4 +61,8 @@ extension FilesWindowController: PathControlDelegate { } return nil } + + public func pathControlCanChoosePath(_: PathControl) -> Bool { + alertSaveDirtyFiles() + } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Read.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Read.swift index 628c97b..e436de9 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Read.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Read.swift @@ -9,81 +9,127 @@ @MainActor extension FilesWindowController { func startComparison() { - reloadAllMove(toFirstDifference: true) + startReload(toFirstDifference: true) } - func reloadAllMove(toFirstDifference moveToFirstDifference: Bool) { + // entry point for every caller, an AppKit action or a delegate + // + // the read runs inline: an action is dispatched while AppKit is still handling the + // event, and a task created there does not start until that handling returns, which + // after a double click takes hundreds of milliseconds + // + // `completion` runs once the result is installed, a request that is refused, queued + // or whose read fails never calls it + func startReload( + toFirstDifference moveToFirstDifference: Bool, + statusMessage: String? = nil, + completion: (() -> Void)? = nil + ) { + // the window can close between the request and the comparison, sessionDiff is + // a managed object and its context goes away with the document + if isClosed { + return + } + + // the caller may have already changed the paths or the options, dropping the + // request would leave the views showing them next to the previous comparison + if isComparing { + var request = pendingReload ?? PendingReload() + + request.toFirstDifference = request.toFirstDifference || moveToFirstDifference + request.statusMessage = statusMessage ?? request.statusMessage + pendingReload = request + + return + } + + beginCompare() + + // the read that fails is the last one started, its path completes the console message + var readingPath = sessionDiff.leftPath ?? "" + + // the panels keep showing the previous comparison when a read throws + let wasLeftDirty = leftView.isDirty + let wasRightDirty = rightView.isDirty + let wasLeftEditAllowed = leftPanelView.isEditAllowed + let wasRightEditAllowed = rightPanelView.isEditAllowed + let previousLeftPath = resolvedLeftPath + let previousRightPath = resolvedRightPath + do { resolvedLeftPath = nil resolvedRightPath = nil + + // the read lowers it for anything that is not plain text, it has to start + // from a known state or an early return keeps the previous refusal forever + leftPanelView.isEditAllowed = true + rightPanelView.isEditAllowed = true + + showReading(path: readingPath) let (leftURL, leftLines) = try readDiffSource(for: .left) + + readingPath = sessionDiff.rightPath ?? "" + showReading(path: readingPath) let (rightURL, rightLines) = try readDiffSource(for: .right) + resolvedLeftPath = leftURL resolvedRightPath = rightURL + let isSameResolvedPath = resolvedLeftPath != nil && resolvedLeftPath == resolvedRightPath - leftPanelView.isEditAllowed = !isSameResolvedPath - rightPanelView.isEditAllowed = !isSameResolvedPath + // reading already refused the edit for anything that is not plain text, + // comparing a file against itself can only forbid it further + leftPanelView.isEditAllowed = leftPanelView.isEditAllowed && !isSameResolvedPath + rightPanelView.isEditAllowed = rightPanelView.isEditAllowed && !isSameResolvedPath - compare( + runComparison( leftLines: leftLines, rightLines: rightLines, moveToFirstDifference: moveToFirstDifference - ) + ) { [self] in + completion?() + + // the message is set before the controls are released, a queued request + // the release starts would otherwise find the counters already restored + if let statusMessage { + showStatusMessage(statusMessage) + } + } } catch let error as NSError { - showError(error) - return + // a queued request would read again the file that just failed + pendingReload = nil + + // the panel whose read succeeded already cleared its + // dirty flag and the edits it still shows are unreachable without it + resolvedLeftPath = previousLeftPath + resolvedRightPath = previousRightPath + leftPanelView.isEditAllowed = wasLeftEditAllowed + rightPanelView.isEditAllowed = wasRightEditAllowed + leftView.isDirty = wasLeftDirty + rightView.isDirty = wasRightDirty + + endCompare() + log(error: error.format(withPath: readingPath)) } } func compare( leftLines: [DiffLineComponent], rightLines: [DiffLineComponent], - moveToFirstDifference: Bool + moveToFirstDifference: Bool, + completion: (() -> Void)? = nil ) { - let newDiffResult = DiffResult() - - diffResult = newDiffResult - currentDiffResult = nil + if isClosed || isComparing { + return + } + beginCompare() - newDiffResult.diff( + runComparison( leftLines: leftLines, rightLines: rightLines, - options: preferences.diffResultOptions + moveToFirstDifference: moveToFirstDifference, + completion: completion ) - setSliderMaxValue() - differenceCounters.update(counters: DiffCountersItem.diffCounter(withResult: newDiffResult)) - refreshLinesStatus() - - // update eol for files - leftPanelView.fileInfoBar.eol = newDiffResult.leftSide.eol - rightPanelView.fileInfoBar.eol = newDiffResult.rightSide.eol - - // force toolbar to enable items - window?.toolbar?.validateVisibleItems() - - synchronizeWindowTitleWithDocumentName() - window?.subtitle = preferences.diffResultOptions.description - - leftView.deselectAll(nil) - rightView.deselectAll(nil) - - if moveToFirstDifference { - // force layout before scrolling, this is necessary on some "slow" machine - leftView.enclosingScrollView?.layoutSubtreeIfNeeded() - rightView.enclosingScrollView?.layoutSubtreeIfNeeded() - moveToDifference(true, showAnim: false, moveToFile: false) - } - } - - func showError(_ error: NSError) { - guard let window else { - return - } - - let alert = NSAlert(error: error) - - alert.beginSheetModal(for: window) } private func readDiffSource( @@ -124,4 +170,138 @@ extension FilesWindowController { return (resolvedURL, lines) } + + // shows the progress and locks the controls + private func beginCompare() { + isComparing = true + comparisonStartedAt = Date() + + setComparisonRunning(true) + } + + private func endCompare() { + isComparing = false + + guard !isClosed, + let pendingReload else { + pendingReload = nil + setComparisonRunning(false) + return + } + + self.pendingReload = nil + + // the queued comparison starts right away, unlocking here would blink the + // counters back with the numbers of the one that just ended + startReload( + toFirstDifference: pendingReload.toFirstDifference, + statusMessage: pendingReload.statusMessage + ) + } + + private func runComparison( + leftLines: [DiffLineComponent], + rightLines: [DiffLineComponent], + moveToFirstDifference: Bool, + completion: (() -> Void)? + ) { + let newDiffResult = DiffResult(options: preferences.diffResultOptions) + + // the reading message belongs to the phase that is over, from here on the + // progress is only the bar + progressView.updateMessage("") + updateProgress(percentage: 0) + + // the stream starts the comparison as it is created, so it is created here and + // not inside the task, which is what waits for the event handling to return + let progress = newDiffResult.diffDetached( + leftLines: leftLines, + rightLines: rightLines + ) + + Task { + // the views keep showing the previous comparison, only the progress moves + for await percentage in progress { + updateProgress(percentage: percentage) + } + + installComparison( + newDiffResult, + moveToFirstDifference: moveToFirstDifference, + completion: completion + ) + } + } + + private func didCompare( + _ newDiffResult: DiffResult, + moveToFirstDifference: Bool + ) { + diffResult = newDiffResult + currentDiffResult = nil + + // both hold lines of the comparison being replaced, the filtered result is only + // reassigned when a line filter is active and the cache is keyed by line identity + filteredDiffResult = nil + cachedLineTextMap.removeAll() + + setSliderMaxValue() + differenceCounters.update(counters: DiffCountersItem.diffCounter(withResult: newDiffResult)) + refreshLinesStatus() + + // update eol for files + leftPanelView.fileInfoBar.eol = newDiffResult.leftSide.eol + rightPanelView.fileInfoBar.eol = newDiffResult.rightSide.eol + + synchronizeWindowTitleWithDocumentName() + window?.subtitle = preferences.diffResultOptions.description + + leftView.deselectAll(nil) + rightView.deselectAll(nil) + + if moveToFirstDifference { + // force layout before scrolling, this is necessary on some "slow" machine + leftView.enclosingScrollView?.layoutSubtreeIfNeeded() + rightView.enclosingScrollView?.layoutSubtreeIfNeeded() + moveToDifference(true, showAnim: false, moveToFile: false) + } + } + + private func updateProgress(percentage: Int) { + progressView.setProgress( + position: Double(percentage), + maxValue: Double(DiffProgress.maxPercentage) + ) + } + + // reading a file blocks the main thread, so the message is drawn right away + private func showReading(path: String?) { + guard let path, + !path.isEmpty else { + return + } + + progressView.displayMessage(String(format: NSLocalizedString("Reading %@", comment: ""), path)) + } + + // installs the result and releases the controls, the completion runs in between so + // it sees the new rows while the controls are still locked + private func installComparison( + _ newDiffResult: DiffResult, + moveToFirstDifference: Bool, + completion: (() -> Void)? + ) { + guard !isClosed else { + endCompare() + return + } + + // installing the result freezes the window for a while, the progress must still + // be up there instead of a status bar that looks idle + didCompare(newDiffResult, moveToFirstDifference: moveToFirstDifference) + logComparisonCompleted(elapsedTimeText: Date().timeIntervalSince(comparisonStartedAt).format()) + completion?() + + endCompare() + } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift index 6921fad..6010ee9 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Save.swift @@ -22,15 +22,17 @@ extension FilesWindowController { case .cancel: return false case .saveBoth: + // a blank compare has no destination yet, so saveView returns without + // writing anything when its save panel is cancelled try saveView(leftView) try saveView(rightView) - return true + return !leftView.isDirty && !rightView.isDirty case .saveOnlyLeft: try saveView(leftView) - return true + return !leftView.isDirty case .saveOnlyRight: try saveView(rightView) - return true + return !rightView.isDirty default: return true } @@ -41,23 +43,20 @@ extension FilesWindowController { } @IBAction func saveLeftFile(_: Any) { - saveViewAndRefreshUI(leftView) + saveViewShowingErrors(leftView) } @IBAction func saveRightFile(_: Any) { - saveViewAndRefreshUI(rightView) + saveViewShowingErrors(rightView) } - func saveViewAndRefreshUI(_ view: FilesTableView) { + func saveViewShowingErrors(_ view: FilesTableView) { guard view.isDirty else { return } do { try saveView(view) - view.diffSide?.resetLineModes() - view.reloadData() - updateDetailLines(lastUsedView.selectedRow) } catch { NSAlert(error: error).runModal() } @@ -114,9 +113,9 @@ extension FilesWindowController { fileInfoBar.fileAttrs = try FileManager.default.attributesOfItem(atPath: path.osPath) view.isDirty = false - setSliderMaxValue() - fileThumbnail.needsDisplay = true - window?.toolbar?.validateVisibleItems() + // refresh here so a failure while saving the other pane cannot leave + // an already written pane rendered as modified + refreshAfterSave(view) NotificationCenter.default.postFileUpdated( leftPath: sessionDiff.leftPath, @@ -125,7 +124,7 @@ extension FilesWindowController { } private func suggestedDestination() -> (directoryPath: String?, fileName: String)? { - let parents = (document as? VDDocument)?.parentSession?.parentPaths( + let parents = parentSession?.parentPaths( from: resolvedLeftPath?.osPath, rightPath: resolvedRightPath?.osPath ) @@ -171,4 +170,14 @@ extension FilesWindowController { } throw FileError.fileNotExists(path: path, side: side) } + + private func refreshAfterSave(_ view: FilesTableView) { + view.diffSide?.resetLineModes() + view.reloadData() + updateDetailLines(lastUsedView.selectedRow) + + setSliderMaxValue() + fileThumbnail.needsDisplay = true + window?.toolbar?.validateVisibleItems() + } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift index 07bbd56..721865a 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+SessionPreferences.swift @@ -24,6 +24,12 @@ extension FilesWindowController { private func updateSessionPreferences(_ returnCode: NSApplication.ModalResponse) { if returnCode == .OK { + // session preferences discard, the new options are applied by a fresh comparison, + // which re-reads the files over the unsaved edits the panels hold + guard alertSaveDirtyFiles() else { + return + } + preferences = sessionPreferencesSheet.preferences sessionDiff.extraData.diffResultOptions = preferences.diffResultOptions startComparison() diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift index fe2f208..a7e0351 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+Slider.swift @@ -21,13 +21,33 @@ extension FilesWindowController { return } - leftPanelView.setSliderMaxValue( - diffResult.leftSide.lines, - right: diffResult.rightSide.lines - ) - rightPanelView.setSliderMaxValue( - diffResult.leftSide.lines, - right: diffResult.rightSide.lines + // both panels share the same value, computing it once per panel used to be the + // longest blocking step of a comparison + let maxColumn = widestColumn( + leftLines: diffResult.leftSide.lines, + rightLines: diffResult.rightSide.lines ) + + leftPanelView.setSliderMaxValue(maxColumn) + rightPanelView.setSliderMaxValue(maxColumn) + } + + private func widestColumn( + leftLines: [DiffLine], + rightLines: [DiffLine] + ) -> Int { + var widest = 0 + + // walking each side on its own does not depend on the two being aligned, + // an edit can leave them with a different number of lines + for line in leftLines { + widest = max(widest, line.text.count) + } + + for line in rightLines { + widest = max(widest, line.text.count) + } + + return widest } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+SplitViewDelegate.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+SplitViewDelegate.swift index 25fa65d..a1cf7de 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+SplitViewDelegate.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+SplitViewDelegate.swift @@ -8,6 +8,6 @@ extension FilesWindowController: NSSplitViewDelegate { func splitViewDidResizeSubviews(_: Notification) { - rowHeightCalculator.reloadData() + rowHeightCalculator.reloadDataIfWidthChanged() } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift index 63d0e7c..a03b886 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+TableView.swift @@ -45,6 +45,7 @@ extension FilesWindowController: NSTableViewDataSource, view.diffLine = diffLine view.font = currentFont view.isSelected = tableView.isRowSelected(row) + view.inlineRanges = inlineDisplayRanges(diffLine, startingColumn: leftPanelView.columnSlider.integerValue) view.formattedText = formattedText(diffLine) view.isWordWrapEnabled = rowHeightCalculator.isWordWrapEnabled view.lineNumberWidth = lineNumberWidth @@ -125,6 +126,11 @@ extension FilesWindowController: NSTableViewDataSource, return false } + // the dropped files replace the displayed ones, so the pending changes must be saved first + guard alertSaveDirtyFiles() else { + return false + } + if arr.count < 2 { guard let path = arr.last?.osPath else { return false @@ -140,12 +146,16 @@ extension FilesWindowController: NSTableViewDataSource, sessionDiff.rightPath = arr[1].osPath } - reloadAllMove(toFirstDifference: false) + startReload(toFirstDifference: false) return true } func tableView(_: NSTableView, menuItem: NSMenuItem, hideMenuItem hide: inout Bool) -> Bool { + if isComparing { + hide = true + return false + } let action = menuItem.action hide = true diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+UICreation.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+UICreation.swift index 322584c..85cddb1 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+UICreation.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+UICreation.swift @@ -73,7 +73,7 @@ extension FilesWindowController { } func createLineDetailTextView() -> LineDetailTextView { - let view = LineDetailTextView(frame: .zero) + let view = LineDetailTextView() view.isSelectable = true view.isEditable = false @@ -110,7 +110,14 @@ extension FilesWindowController { view.orientation = .vertical view.alignment = .width view.distribution = .fill - view.translatesAutoresizingMaskIntoConstraints = false + + // the arranged views are pinned directly to the stack to avoid cross-hierarchy constraints + for arrangedView in views { + NSLayoutConstraint.activate([ + arrangedView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + arrangedView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + ]) + } return view } @@ -148,6 +155,7 @@ extension FilesWindowController { topView.addSubview(leftView) topView.addSubview(rightView) + // the thumbnail is sized and positioned by the top view that owns it NSLayoutConstraint.activate([ leftView.leadingAnchor.constraint(equalTo: topView.leadingAnchor), leftView.topAnchor.constraint(equalTo: topView.topAnchor, constant: 18), @@ -162,4 +170,12 @@ extension FilesWindowController { return topView } + + func createProgressView() -> ProgressBarView { + let view = ProgressBarView(frame: .zero) + + view.translatesAutoresizingMaskIntoConstraints = false + + return view + } } diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift index 89a127f..838f54f 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController+UISetup.swift @@ -6,30 +6,33 @@ // Copyright (c) 2025 visualdiffer.com // -extension FilesWindowController { +extension FilesWindowController: ComparisonActivityDisplayable { + var lockedScopeBar: ScopeBarView { + scopeBar + } + + var lockedPathViews: [PathView] { + [leftPanelView.pathView, rightPanelView.pathView] + } + func initAllViews() { setupWindowLayout() // must be called after setting up all views setupWindow() - leftPanelView.treeView.nextKeyView = rightView } func setupWindowLayout() { - let detailsStackView = createLineDetailsStackWithViews([ - createTopView(fileThumbnail, rightView: filePanels), - linesDetailView, - ]) - if let contentView = window?.contentView { contentView.addSubview(differenceCounters) contentView.addSubview(statusbarText) + contentView.addSubview(progressView) contentView.addSubview(scopeBar) - contentView.addSubview(detailsStackView) + contentView.addSubview(consoleSplitter) } - setupDetailsStackConstraints(detailsStackView) setupConstraints() + setProgressHidden(true) updateUI() @@ -63,22 +66,15 @@ extension FilesWindowController { statusbarText.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -trailingMargin), statusbarText.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -5), statusbarText.widthAnchor.constraint(greaterThanOrEqualToConstant: 120), - ]) - } - - func setupDetailsStackConstraints(_ stackView: NSStackView) { - guard let contentView = window?.contentView else { - return - } - NSLayoutConstraint.activate([ - stackView.topAnchor.constraint(equalTo: scopeBar.bottomAnchor), - stackView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), - stackView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), - stackView.bottomAnchor.constraint(equalTo: differenceCounters.topAnchor, constant: -2), + progressView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: leadingMargin), + progressView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -trailingMargin), + progressView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), - fileThumbnail.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), - fileThumbnail.widthAnchor.constraint(equalToConstant: 15), + consoleSplitter.topAnchor.constraint(equalTo: scopeBar.bottomAnchor), + consoleSplitter.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + consoleSplitter.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + consoleSplitter.bottomAnchor.constraint(equalTo: differenceCounters.topAnchor, constant: -2), // linesDetailView has a fixed height linesDetailView.heightAnchor.constraint(equalToConstant: 46), @@ -102,13 +98,13 @@ extension FilesWindowController { updateTreeViewFont() // invalidate the line text cache - cachedLineTextMap.removeAllObjects() + cachedLineTextMap.removeAll() reloadRowHeights() updateTabWidth() - updateDetailLines(leftView.selectedRow) + updateDetailLines(lastUsedView.selectedRow) updateStatusbarText() } @@ -164,6 +160,7 @@ extension FilesWindowController { scopeBar.findView.delegate = FilesTableViewFindTextDelegate(view: leftView) updateScopeBar() + setupConsoleSplitter() leftPanelView.bindControls() rightPanelView.bindControls() @@ -231,14 +228,15 @@ extension FilesWindowController { @objc func appAppearanceDidChange(_: Notification) { - leftView.reloadData() - rightView.reloadData() + leftView.reloadData(restoreSelection: true) + rightView.reloadData(restoreSelection: true) guard let diffResult else { return } differenceCounters.update(counters: DiffCountersItem.diffCounter(withResult: diffResult)) + updateDetailLines(lastUsedView.selectedRow) } // MARK: - File and view scroll diff --git a/Sources/Features/FilesCompare/Controller/FilesWindowController.swift b/Sources/Features/FilesCompare/Controller/FilesWindowController.swift index 94c1446..9d4cbe5 100644 --- a/Sources/Features/FilesCompare/Controller/FilesWindowController.swift +++ b/Sources/Features/FilesCompare/Controller/FilesWindowController.swift @@ -6,6 +6,10 @@ // Copyright (c) 2010 visualdiffer.com // +// minimum heights of the two panes of the console splitter +private let filePanelsMinHeight: CGFloat = 160.0 +private let consoleMinHeight: CGFloat = 80.0 + class FilesWindowController: NSWindowController { var lineNumberWidth: CGFloat = 0 @@ -14,7 +18,8 @@ class FilesWindowController: NSWindowController { // swiftlint:disable:next implicitly_unwrapped_optional @objc var sessionDiff: SessionDiff! - let cachedLineTextMap: NSMapTable + // keyed by line identity, the dictionary keeps the lines alive as long as they are cached + var cachedLineTextMap = [DiffLine: String]() var currentFont: NSFont var fontZoomFactor: CGFloat = 0 { @@ -28,6 +33,7 @@ class FilesWindowController: NSWindowController { lazy var differenceCounters: DifferenceCounters = createDifferenceCounters() lazy var statusbarText: NSTextField = createStatusbarText() + lazy var progressView: ProgressBarView = createProgressView() lazy var fileThumbnail: FileThumbnailView = createThumbnailView() @@ -40,6 +46,27 @@ class FilesWindowController: NSWindowController { var filteredDiffResult: DiffResult? var currentDiffResult: DiffResult? + // the comparison runs detached, a new one is refused until it completes + var isComparing = false + + // set when a comparison starts, the console reports how long it took + var comparisonStartedAt = Date() + + // a reload asked while a comparison runs, it carries what that request still owes + struct PendingReload { + var toFirstDifference = false + var statusMessage: String? + } + + var pendingReload: PendingReload? + + // a detached comparison outlives the window, the session it reads does not + var isClosed = false + + // restores the counters once a status bar message has been read, a newer message + // replaces the pending one + var statusMessageTask: Task? + var resolvedLeftPath: URL? var resolvedRightPath: URL? @@ -52,6 +79,30 @@ class FilesWindowController: NSWindowController { parent: window ) + // the console is the bottom pane of the window, collapsed until an error is logged + // or the user asks for it + lazy var consoleSplitter: DualPaneSplitView = { + let view = createConsoleSplitter() + + view.addArrangedSubview(detailsStackView) + view.addArrangedSubview(consoleView) + + return view + }() + + lazy var consoleView: ConsoleView = createConsoleView() + + lazy var detailsStackView: NSStackView = createLineDetailsStackWithViews([ + createTopView(fileThumbnail, rightView: filePanels), + linesDetailView, + ]) + + var consoleDelegate = DualPaneSplitViewDelegate( + collapsableSubViewIndex: 1, + minFirstPaneSize: filePanelsMinHeight, + minSecondPaneSize: consoleMinHeight + ) + let filePanels: NSSplitView let leftPanelView: FilePanelView let rightPanelView: FilePanelView @@ -68,6 +119,11 @@ class FilesWindowController: NSWindowController { lazy var sessionPreferencesSheet: FileSessionPreferencesWindow = .init() + // the folder session the compared files were opened from, nil for a standalone window + var parentSession: DiffOpenerDelegate? { + (document as? VDDocument)?.parentSession + } + init() { let window = WindowCancelOperation.createWindow() @@ -76,11 +132,6 @@ class FilesWindowController: NSWindowController { visibleWhitespaces = VisibleWhitespaces() visibleWhitespaces.tabWidth = CommonPrefs.shared.tabWidth - cachedLineTextMap = NSMapTable( - keyOptions: .objectPointerPersonality, - valueOptions: .strongMemory - ) - // panels leftPanelView = FilePanelView(side: .left) rightPanelView = FilePanelView(side: .right) @@ -126,9 +177,9 @@ class FilesWindowController: NSWindowController { filePanel.setSliderChangeAction(target, action: action) } - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + @available(*, unavailable, message: "use init()") + required init?(coder _: NSCoder) { + nil } // MARK: - Zoom Font diff --git a/Sources/Features/FilesCompare/DiffResult/DiffLine.swift b/Sources/Features/FilesCompare/DiffResult/DiffLine.swift index e6f0dc6..5bb26de 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffLine.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffLine.swift @@ -24,11 +24,21 @@ class DiffLine { var number: Int // mode is used by UI to determine how to draw line var mode: DisplayMode = .normal - var component: DiffLineComponent + var component: DiffLineComponent { + didSet { + // the offsets belong to the replaced text, they cannot survive it + inlineRanges = [] + } + } + var isSectionSeparator = false var filteredIndex = 0 var hasIgnoredDifferences: Bool + // character offsets, relative to component.text, that differ from the paired line, + // meaningful only when type is .changed + var inlineRanges = [Range]() + var text: String { component.text } @@ -66,3 +76,15 @@ extension DiffLine: CustomStringConvertible { String(format: "%ld %@ : %@", number, type.description, text) } } + +/// identity semantics, a line is a mutable reference and two distinct lines stay +/// distinct even while they hold the same text +extension DiffLine: Hashable { + static func == (lhs: DiffLine, rhs: DiffLine) -> Bool { + lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} diff --git a/Sources/Features/FilesCompare/DiffResult/DiffProcessor.swift b/Sources/Features/FilesCompare/DiffResult/DiffProcessor.swift index b8f1008..cd94285 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffProcessor.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffProcessor.swift @@ -18,6 +18,7 @@ struct DiffProcessor { private var leftIndex = 0 private var rightIndex = 0 + private var progress: DiffProgress? init( leftLines: [DiffLineComponent], @@ -29,7 +30,10 @@ struct DiffProcessor { self.options = options } - mutating func process(changes: UDiffChange?) { + mutating func process(changes: [DiffChange], progress: DiffProgress? = nil) { + self.progress = progress + progress?.begin(total: leftLines.count + rightLines.count) + sections.removeAll(keepingCapacity: true) summary.reset() @@ -39,22 +43,20 @@ struct DiffProcessor { leftIndex = 0 rightIndex = 0 - var script = changes - - while let localScript = script { + for change in changes { updateMatchingLines( - leftCount: Int(localScript.line0), - rightCount: Int(localScript.line1) + leftCount: change.line0, + rightCount: change.line1 ) - let commonLines = Int(min(localScript.deleted, localScript.inserted)) - let diffLines = Int(localScript.deleted - localScript.inserted) + let commonLines = min(change.deleted, change.inserted) + let diffLines = change.deleted - change.inserted var start = leftSide.lines.count // Create a new section to distinguish adjacent // .deleted from .added lines - if commonLines > 0, localScript.deleted != localScript.inserted { + if commonLines > 0, change.deleted != change.inserted { sections.append(DiffSection( start: start, end: leftSide.lines.count + commonLines - 1 @@ -80,8 +82,6 @@ struct DiffProcessor { start: start, end: leftSide.lines.count - 1 )) - - script = localScript.link } updateMatchingLines( @@ -105,6 +105,7 @@ struct DiffProcessor { ) leftSide.add(line: line) leftIndex += 1 + progress?.report(processed: leftIndex + rightIndex) } while rightIndex < rightCount { @@ -115,6 +116,7 @@ struct DiffProcessor { ) rightSide.add(line: line) rightIndex += 1 + progress?.report(processed: leftIndex + rightIndex) } updateIgnored(from: startIndex) @@ -156,8 +158,12 @@ struct DiffProcessor { component: rightLines[rightIndex] ) rightSide.add(line: rightLine) + + InlineDiff.apply(to: leftLine, and: rightLine, options: options) + leftIndex += 1 rightIndex += 1 + progress?.report(processed: leftIndex + rightIndex) } } @@ -174,6 +180,7 @@ struct DiffProcessor { ) leftSide.add(line: line) leftIndex += 1 + progress?.report(processed: leftIndex + rightIndex) } for _ in 0 ..< lineCount { let line = DiffLine.missingLine() @@ -199,6 +206,7 @@ struct DiffProcessor { ) rightSide.add(line: line) rightIndex += 1 + progress?.report(processed: leftIndex + rightIndex) } } } diff --git a/Sources/Features/FilesCompare/DiffResult/DiffProgress.swift b/Sources/Features/FilesCompare/DiffResult/DiffProgress.swift new file mode 100644 index 0000000..0550fcf --- /dev/null +++ b/Sources/Features/FilesCompare/DiffResult/DiffProgress.swift @@ -0,0 +1,37 @@ +// +// DiffProgress.swift +// VisualDiffer +// +// Created by davide ficano on 07/09/26. +// Copyright (c) 2026 visualdiffer.com +// + +// reports how far a comparison went as a percentage of the processed lines +// +// the handler is notified only when the percentage grows, so the reported value never +// goes backwards and the notifications cannot weigh on the comparison +final class DiffProgress { + static let maxPercentage = 100 + + private let handler: (Int) -> Void + + private var total = 1 + private var percentage = 0 + + init(handler: @escaping (Int) -> Void) { + self.handler = handler + } + + func begin(total: Int) { + self.total = max(total, 1) + } + + func report(processed: Int) { + let value = Self.maxPercentage * min(processed, total) / total + + if value > percentage { + percentage = value + handler(value) + } + } +} diff --git a/Sources/Features/FilesCompare/DiffResult/DiffResult+Detached.swift b/Sources/Features/FilesCompare/DiffResult/DiffResult+Detached.swift new file mode 100644 index 0000000..2ffe2b7 --- /dev/null +++ b/Sources/Features/FilesCompare/DiffResult/DiffResult+Detached.swift @@ -0,0 +1,35 @@ +// +// DiffResult+Detached.swift +// VisualDiffer +// +// Created by davide ficano on 07/09/26. +// Copyright (c) 2026 visualdiffer.com +// + +// the detached task owns the result until the stream finishes, the caller reads it +// back only once the iteration is over +extension DiffResult: @unchecked Sendable {} + +extension DiffResult { + // identical to .diff() but runs detached, the returned stream yields the percentage + // of processed lines and finishes when the comparison is complete + func diffDetached( + leftLines: [DiffLineComponent], + rightLines: [DiffLineComponent] + ) -> AsyncStream { + // a progress notification is worth nothing once a newer one exists, keeping just + // the last one prevents a busy consumer from throttling the comparison + AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in + // the comparison is long and never suspends, on the cooperative pool it would + // hold one of its threads and that pool is only as wide as the core count + DispatchQueue.global(qos: .userInitiated).async { + self.diff( + leftLines: leftLines, + rightLines: rightLines, + progress: DiffProgress { continuation.yield($0) } + ) + continuation.finish() + } + } + } +} diff --git a/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift b/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift index 1412ff6..38a737c 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffResult+Dump.swift @@ -84,6 +84,7 @@ DiffResult.writeLine(fileHandle, line: "type = \(type) number = \(number) mode = \(mode)") // DiffResult.writeLine(fileHandle, line: "text = \(text)") DiffResult.writeLine(fileHandle, line: "isSectionSeparator = \(isSectionSeparator) filteredIndex = \(filteredIndex)") + DiffResult.writeLine(fileHandle, line: "inlineRanges = \(inlineRanges)") } } diff --git a/Sources/Features/FilesCompare/DiffResult/DiffResult+Lines.swift b/Sources/Features/FilesCompare/DiffResult/DiffResult+Lines.swift index b118a67..e5ba906 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffResult+Lines.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffResult+Lines.swift @@ -135,7 +135,10 @@ extension LineEditOperation { extension DiffResult { static func justDifferentLines(_ result: DiffResult) -> DiffResult { - let onlyMismatches = DiffResult(sections: DiffSection.compact(sections: result.sections)) + let onlyMismatches = DiffResult( + sections: DiffSection.compact(sections: result.sections), + options: result.options + ) onlyMismatches.inheritSideMetadata(from: result) let leftSide = onlyMismatches.leftSide @@ -188,7 +191,7 @@ extension DiffResult { static func justMatchingLines(_ result: DiffResult) -> DiffResult { // difference sections are not visible - let onlyMatches = DiffResult() + let onlyMatches = DiffResult(options: result.options) onlyMatches.inheritSideMetadata(from: result) let leftSide = onlyMatches.leftSide @@ -228,7 +231,6 @@ extension DiffResult { return onlyMatches } - // a filtered result must inherit the per-side metadata of its source result private func inheritSideMetadata(from result: DiffResult) { leftSide.eol = result.leftSide.eol rightSide.eol = result.rightSide.eol diff --git a/Sources/Features/FilesCompare/DiffResult/DiffResult.Options.swift b/Sources/Features/FilesCompare/DiffResult/DiffResult.Options.swift index 8c0decd..4b9fff9 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffResult.Options.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffResult.Options.swift @@ -92,4 +92,10 @@ extension DiffResult.Options { return leftLine.text != rightLine.text } + + /// the key the comparison engine compares, every path classifying a pair of lines + /// must build it the same way or the same pair would be classified differently + func key(for component: DiffLineComponent) -> SequenceDiff.TextKey { + SequenceDiff.TextKey(applyTransformations(component: component)) + } } diff --git a/Sources/Features/FilesCompare/DiffResult/DiffResult.swift b/Sources/Features/FilesCompare/DiffResult/DiffResult.swift index 3a27d27..ade17a9 100644 --- a/Sources/Features/FilesCompare/DiffResult/DiffResult.swift +++ b/Sources/Features/FilesCompare/DiffResult/DiffResult.swift @@ -22,8 +22,16 @@ class DiffResult { private(set) var sections = [DiffSection]() private(set) var summary = DiffSummary() - convenience init(sections: [DiffSection]) { - self.init() + // the options the comparison ran with, the editing paths must classify a pair + // of lines with the same equality the engine used + let options: Options + + init(options: Options = []) { + self.options = options + } + + convenience init(sections: [DiffSection], options: Options) { + self.init(options: options) self.sections = sections } @@ -41,42 +49,30 @@ class DiffResult { // periphery:ignore func diff( leftText: String, - rightText: String, - options: Options = [] + rightText: String ) { diff( leftLines: DiffLineComponent.splitLines(leftText), - rightLines: DiffLineComponent.splitLines(rightText), - options: options + rightLines: DiffLineComponent.splitLines(rightText) ) } func diff( leftLines: [DiffLineComponent], rightLines: [DiffLineComponent], - options: Options = [] + progress: DiffProgress? = nil ) { - let stringifier: (Any) -> String = { - guard let component = $0 as? DiffLineComponent else { - preconditionFailure("Expected a DiffLineComponent") - } - - return options.applyTransformations(component: component) - } - - let udiff = UnifiedDiff( - originalLines: leftLines, - revisedLines: rightLines, - stringifier: stringifier + let changes = SequenceDiff.changes( + left: leftLines.map { options.key(for: $0) }, + right: rightLines.map { options.key(for: $0) } ) - let script = udiff.diff_2(false) var processor = DiffProcessor( leftLines: leftLines, rightLines: rightLines, options: options ) - processor.process(changes: script) + processor.process(changes: changes, progress: progress) leftSide = processor.leftSide rightSide = processor.rightSide @@ -123,26 +119,36 @@ class DiffResult { } var index = startIndex - DiffLineComponent.enumerateLines(text: text) { component in + DiffLineComponent.enumerateLines(text: text) { pastedComponent in // adds missing lines to both diff sides to accommodate insertion at the specified index if index == destination.lines.count { destination.insert(DiffLine.missingLine(), at: index) otherSide.insert(DiffLine.missingLine(), at: index) } + let component = self.adoptingEOL(of: destination, for: pastedComponent) let destLine = destination.lines[index] if destLine.type == .missing { destLine.component = component let otherLine = otherSide.lines[index] - // simple comparison - if otherLine.text == component.text { + + if self.matches(otherLine.component, component) { otherLine.type = .matching destLine.type = .matching + + let hasIgnored = self.options.containsIgnoreFor( + differenceBetween: otherLine.component, + and: component + ) + otherLine.hasIgnoredDifferences = hasIgnored + destLine.hasIgnoredDifferences = hasIgnored } else { if otherLine.type == .missing { destLine.type = type } else { otherLine.type = .changed destLine.type = .changed + + InlineDiff.apply(to: otherLine, and: destLine, options: self.options) } } } else { @@ -179,4 +185,25 @@ class DiffResult { func refreshSummary() { summary.refresh(leftSide.lines) } + + // a weaker comparison than the engine one would show a pair as matching while the + // two lines differ, and the difference would appear out of nowhere on the next comparison + private func matches(_ leftLine: DiffLineComponent, _ rightLine: DiffLineComponent) -> Bool { + options.key(for: leftLine) == options.key(for: rightLine) + } + + // pasted text always carries a unix line ending, the destination one must win, + // as it does when copying lines between the two sides + private func adoptingEOL( + of destination: DiffSide, + for component: DiffLineComponent + ) -> DiffLineComponent { + guard destination.eol != .mixed, + destination.eol != .missing, + component.eol != .missing else { + return component + } + + return DiffLineComponent(text: component.text, eol: destination.eol) + } } diff --git a/Sources/Features/FilesCompare/DiffResult/Engine/DiffChange.swift b/Sources/Features/FilesCompare/DiffResult/Engine/DiffChange.swift new file mode 100644 index 0000000..d23b9ba --- /dev/null +++ b/Sources/Features/FilesCompare/DiffResult/Engine/DiffChange.swift @@ -0,0 +1,27 @@ +// +// DiffChange.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +/// one place where some elements are deleted and some are inserted +/// +/// `line0` and `line1` are the first affected elements in the two sequences (origin 0), +/// `deleted` is the number of elements removed from the left sequence and `inserted` +/// the number of elements added from the right one. +/// When `deleted` is 0 then `line0` is the element before which the insertion was done, +/// and vice versa for `inserted` and `line1`. +struct DiffChange: Equatable { + let line0: Int + let line1: Int + let deleted: Int + let inserted: Int +} + +extension DiffChange: CustomStringConvertible { + var description: String { + "\(line0 + 1), \(line1 + 1) inserted \(inserted), deleted \(deleted)" + } +} diff --git a/Sources/Features/FilesCompare/DiffResult/Engine/DiffFileData.swift b/Sources/Features/FilesCompare/DiffResult/Engine/DiffFileData.swift new file mode 100644 index 0000000..b692046 --- /dev/null +++ b/Sources/Features/FilesCompare/DiffResult/Engine/DiffFileData.swift @@ -0,0 +1,384 @@ +// +// DiffFileData.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +/// data on one input sequence being compared +final class DiffFileData { + /// how a single element is classified before the comparison runs + private enum Discard { + case keep + case discardable + case provisional + } + + private static let manyMatches = 5 + private static let matchScaleDivisor = 64 + private static let provisionalRunDivisor = 4 + private static let nonProvisionalRunLength = 3 + private static let provisionalScanLimit = 8 + + /// vector, indexed by element number, containing an equivalence code for each element, + /// it is this vector that is actually compared with that of another sequence + private let equivalences: [Int] + + /// array, indexed by real origin-1 element number, containing true for an element + /// that is an insertion or a deletion + private(set) var changedFlags = [Bool]() + + /// vector, like `equivalences` except that the elements for discarded ones + /// have been squeezed out + private(set) var undiscarded: [Int] + + /// vector mapping virtual element numbers (not counting discarded elements) to real + /// ones (counting those elements), both are origin-0 + private var realIndexes: [Int] + + /// total number of nondiscarded elements + private(set) var nonDiscardedCount = 0 + + /// when true nothing is discarded and the comparison considers the whole sequence + private let ignoresDiscards: Bool + + private let codeCount: Int + + var count: Int { + equivalences.count + } + + init( + equivalences: [Int], + codeCount: Int, + ignoresDiscards: Bool + ) { + self.equivalences = equivalences + self.codeCount = codeCount + self.ignoresDiscards = ignoresDiscards + undiscarded = Array(repeating: 0, count: equivalences.count) + realIndexes = Array(repeating: 0, count: equivalences.count) + } + + /// Discard elements that have no matches in another sequence. + /// + /// A discarded element will not be considered by the actual comparison algorithm, + /// it will be as if that element were not in the sequence. + /// The `realIndexes` table maps virtual element numbers (which don't count the discarded + /// elements) into real element numbers, this is how the actual comparison algorithm + /// produces results that are comprehensible when the discarded elements are counted. + /// + /// When an element is discarded it is also marked as a deletion or insertion + /// so that it will be reported in the edit script. + /// - Parameter other: the sequence being compared against + func discardConfusingElements(against other: DiffFileData) { + clearChangedFlags() + + // set up table of which elements are going to be discarded + var discards = discardable(counts: other.equivalenceCounts()) + + // don't really discard the provisional elements except when they occur + // in a run of discardables, with nonprovisionals at the beginning and end + filterDiscards(&discards) + + // actually discard the elements + discard(discards) + } + + /// Adjust inserts/deletes of blank elements to join changes as much as possible. + /// + /// We do something when a run of changed elements include a blank element at one end + /// and have an excluded blank element at the other. + /// We are free to choose which blank element is included, the comparison always chooses + /// the one at the beginning, but usually it is cleaner to consider the following blank + /// element to be the "change". The only exception is if the preceding blank element + /// would join this change to other changes. + /// - Parameter other: the sequence being compared against + func shiftBoundaries(against other: DiffFileData) { + let endIndex = count + var index = 0 + var otherIndex = 0 + var preceding = -1 + var otherPreceding = -1 + + while true { + // scan forwards to find beginning of another run of changes, + // also keep track of the corresponding point in the other sequence + while index < endIndex, !changedFlags[1 + index] { + while true { + let isChanged = other.changedFlags[1 + otherIndex] + + otherIndex += 1 + + if !isChanged { + break + } + // non-corresponding elements in the other sequence + // will count as the preceding batch of changes + otherPreceding = otherIndex + } + index += 1 + } + + if index == endIndex { + break + } + var start = index + let otherStart = otherIndex + + while true { + // now find the end of this run of changes + while index < endIndex, changedFlags[1 + index] { + index += 1 + } + let end = index + + // if the first changed element matches the following unchanged one, + // and this run does not follow right after a previous run, + // and there are no elements deleted from the other sequence here, + // then classify the first changed element as unchanged + // and the following element as changed in its place + + // you might ask, how could this run follow right after another? + // only because the previous run was shifted here + guard end != endIndex, + equivalences[start] == equivalences[end], + !other.changedFlags[1 + otherIndex], + !((preceding >= 0 && start == preceding) + || (otherPreceding >= 0 && otherStart == otherPreceding)) + else { + break + } + + changedFlags[1 + end] = true + changedFlags[1 + start] = false + start += 1 + index += 1 + // since one element-that-matches is now before this run instead of after, + // we must advance in the other sequence to keep in synch + otherIndex += 1 + } + + preceding = index + otherPreceding = otherIndex + } + } + + /// marks a nondiscarded element as an insertion or a deletion + func markChanged(virtualIndex: Int) { + changedFlags[1 + realIndexes[virtualIndex]] = true + } + + /// allocate a flag for each element, saying whether that element is an insertion + /// or a deletion, with an extra always false element at each end of the vector + private func clearChangedFlags() { + changedFlags = Array(repeating: false, count: count + 2) + } + + private func equivalenceCounts() -> [Int] { + var counts = Array(repeating: 0, count: codeCount) + + for code in equivalences { + counts[code] += 1 + } + + return counts + } + + /// Mark to be discarded each element that matches no element of another sequence, + /// if an element matches many elements mark it as provisionally discardable. + /// - Parameter counts: the count of each equivalence code for the other sequence + private func discardable(counts: [Int]) -> [Discard] { + var discards = Array(repeating: Discard.keep, count: count) + + // multiply many by approximate square root of number of elements, + // that is the threshold for provisionally discardable elements + let many = Self.scaled( + Self.manyMatches, + bySquareRootOf: count / Self.matchScaleDivisor + ) + + for (index, code) in equivalences.enumerated() { + let matches = counts[code] + + if matches == 0 { + discards[index] = .discardable + } else if matches > many { + discards[index] = .provisional + } + } + + return discards + } + + /// don't really discard the provisional elements except when they occur in a run + /// of discardables, with nonprovisionals at the beginning and end + private func filterDiscards(_ discards: inout [Discard]) { + var index = 0 + + while index < count { + // cancel provisional discards not in middle of run of discards + if discards[index] == .provisional { + discards[index] = .keep + } else if discards[index] != .keep { + // we have found a nonprovisional discard + index = filterRun(from: index, discards: &discards) + } + index += 1 + } + } + + /// filters a single run of discardable elements + /// - Returns: the last element index consumed by the run + private func filterRun(from start: Int, discards: inout [Discard]) -> Int { + var provisional = 0 + var runEnd = start + + // find end of this run of discardable elements, + // count how many are provisionally discardable + while runEnd < count, discards[runEnd] != .keep { + if discards[runEnd] == .provisional { + provisional += 1 + } + runEnd += 1 + } + + // cancel provisional discards at end, and shrink the run + while runEnd > start, discards[runEnd - 1] == .provisional { + runEnd -= 1 + discards[runEnd] = .keep + provisional -= 1 + } + // now we have the length of a run of discardable elements + // whose first and last are not provisional + let length = runEnd - start + + // if 1/4 of the elements in the run are provisional, + // cancel discarding of all provisional elements in the run + if provisional * Self.provisionalRunDivisor > length { + var index = runEnd + + while index > start { + index -= 1 + + if discards[index] == .provisional { + discards[index] = .keep + } + } + + return start + } + cancelProvisionalSubruns(from: start, length: length, discards: &discards) + cancelProvisionalsAtRunEdges(from: start, length: length, discards: &discards) + + return start + length - 1 + } + + /// cancel any subrun of a minimum number of provisionals within the larger run + private func cancelProvisionalSubruns( + from start: Int, + length: Int, + discards: inout [Discard] + ) { + // minimum is approximate square root of length/4, + // a subrun of two or more provisionals can stand when length is at least 16, + // a subrun of 4 or more can stand when length is at least 64 + let minimum = Self.scaled( + 1, + bySquareRootOf: length / Self.provisionalRunDivisor + ) + 1 + + var offset = 0 + var consecutive = 0 + + while offset < length { + if discards[start + offset] == .provisional { + consecutive += 1 + + if consecutive == minimum { + // back up to start of subrun, to cancel it all + offset -= consecutive + } else if consecutive > minimum { + discards[start + offset] = .keep + } + } else { + consecutive = 0 + } + offset += 1 + } + } + + /// scan from both ends of the run until 3 or more nonprovisionals in a row are found, + /// or until the first nonprovisional at least 8 elements in, cancelling any provisional + /// met until that point + private func cancelProvisionalsAtRunEdges( + from start: Int, + length: Int, + discards: inout [Discard] + ) { + cancelProvisionals(from: start, step: 1, length: length, discards: &discards) + cancelProvisionals(from: start + length - 1, step: -1, length: length, discards: &discards) + } + + private func cancelProvisionals( + from origin: Int, + step: Int, + length: Int, + discards: inout [Discard] + ) { + var consecutive = 0 + + for offset in 0 ..< length { + let index = origin + step * offset + + if offset >= Self.provisionalScanLimit, discards[index] == .discardable { + break + } + + if discards[index] == .provisional { + consecutive = 0 + discards[index] = .keep + } else if discards[index] == .keep { + consecutive = 0 + } else { + consecutive += 1 + } + + if consecutive == Self.nonProvisionalRunLength { + break + } + } + } + + /// actually discard the elements + /// - Parameter discards: flags elements to be discarded + private func discard(_ discards: [Discard]) { + var virtualIndex = 0 + + for index in 0 ..< count { + if ignoresDiscards || discards[index] == .keep { + undiscarded[virtualIndex] = equivalences[index] + realIndexes[virtualIndex] = index + virtualIndex += 1 + } else { + changedFlags[1 + index] = true + } + } + nonDiscardedCount = virtualIndex + } + + /// doubles value once for every pair of bits of total, the approximate square root + /// the original algorithm computes shifting total two bits at a time + private static func scaled(_ value: Int, bySquareRootOf total: Int) -> Int { + var result = value + var remaining = total >> 2 + + while remaining > 0 { + result *= 2 + remaining >>= 2 + } + + return result + } +} diff --git a/Sources/Features/FilesCompare/DiffResult/Engine/EquivalenceTable.swift b/Sources/Features/FilesCompare/DiffResult/Engine/EquivalenceTable.swift new file mode 100644 index 0000000..0652311 --- /dev/null +++ b/Sources/Features/FilesCompare/DiffResult/Engine/EquivalenceTable.swift @@ -0,0 +1,37 @@ +// +// EquivalenceTable.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +/// translates sequence elements into equivalence codes, so the comparison algorithm +/// only ever compares integers and never touches the original elements +/// +/// The same table must encode both sequences, otherwise equal elements would get +/// different codes. +struct EquivalenceTable { + private var codes = [Key: Int]() + // code 0 is never assigned, the comparison algorithm uses it as the unset marker + private var nextCode = 1 + + /// one more than the greatest assigned code + var codeCount: Int { + nextCode + } + + mutating func encode(_ keys: [Key]) -> [Int] { + keys.map { key in + if let code = codes[key] { + return code + } + let code = nextCode + + codes[key] = code + nextCode += 1 + + return code + } + } +} diff --git a/Sources/Features/FilesCompare/DiffResult/Engine/SequenceDiff.swift b/Sources/Features/FilesCompare/DiffResult/Engine/SequenceDiff.swift new file mode 100644 index 0000000..8b3d344 --- /dev/null +++ b/Sources/Features/FilesCompare/DiffResult/Engine/SequenceDiff.swift @@ -0,0 +1,348 @@ +// +// SequenceDiff.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +/// GNU diff comparison, it finds the differences between two sequences of hashable +/// elements and reports them as an edit script +/// +/// Ported from the legacy UnifiedDiff Objective-C engine, the algorithm is unchanged: +/// a bidirectional breadth-first search of the edit matrix, preceded by the heuristic +/// that discards the elements matching nothing or matching too many elements of the +/// other sequence, and followed by the boundary shifting that makes the hunks prettier. +final class SequenceDiff { + /// text element compared by exact code units + /// + /// String and Character consider canonically equivalent spellings equal, for example + /// "é" and "e" followed by a combining acute accent, so using them directly as keys + /// would silently hide a real difference between two files. + struct TextKey: Hashable { + let text: String + + init(_ text: String) { + self.text = text + } + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.text.utf8.elementsEqual(rhs.text.utf8) + } + + func hash(into hasher: inout Hasher) { + var copy = text + + copy.withUTF8 { + hasher.combine(bytes: UnsafeRawBufferPointer($0)) + } + } + } + + // the diagonals run from -rightCount to leftCount, plus a sentinel at each end + private static let diagonalPadding = 3 + + private let leftData: DiffFileData + private let rightData: DiffFileData + + /// vectors being compared, they hold the equivalence codes of the nondiscarded elements + private var leftVector = [Int]() + private var rightVector = [Int]() + + /// vector, indexed by diagonal, containing the X coordinate of the point furthest + /// along the given diagonal in the forward search of the edit matrix + private var forwardDiagonals = [Int]() + + /// vector, indexed by diagonal, containing the X coordinate of the point furthest + /// along the given diagonal in the backward search of the edit matrix + private var backwardDiagonals = [Int]() + + private var diagonalOffset = 0 + + private init( + leftData: DiffFileData, + rightData: DiffFileData + ) { + self.leftData = leftData + self.rightData = rightData + } + + /// Compares two sequences and reports their differences. + /// + /// Each element is translated to an equivalence code, the original elements are no + /// longer needed to compute the differences. + /// - Parameters: + /// - ignoresDiscards: when true the algorithm returns a guaranteed minimal set of + /// changes, skipping the heuristic that discards confusing elements. It must be + /// used on short sequences, where discarding the elements matching too many + /// others would report almost everything as changed + /// - Returns: the changes in ascending element order + static func changes( + left: [Key], + right: [Key], + ignoresDiscards: Bool = false + ) -> [DiffChange] { + var table = EquivalenceTable() + let leftCodes = table.encode(left) + let rightCodes = table.encode(right) + let codeCount = table.codeCount + + let diff = SequenceDiff( + leftData: DiffFileData( + equivalences: leftCodes, + codeCount: codeCount, + ignoresDiscards: ignoresDiscards + ), + rightData: DiffFileData( + equivalences: rightCodes, + codeCount: codeCount, + ignoresDiscards: ignoresDiscards + ) + ) + + return diff.changes() + } + + private func changes() -> [DiffChange] { + // some elements are obviously insertions or deletions because they don't match + // anything, detect them now and avoid even thinking about them + // in the main comparison algorithm + leftData.discardConfusingElements(against: rightData) + rightData.discardConfusingElements(against: leftData) + + // now do the main comparison algorithm, considering just the nondiscarded elements + leftVector = leftData.undiscarded + rightVector = rightData.undiscarded + + let diagonals = leftData.nonDiscardedCount + rightData.nonDiscardedCount + Self.diagonalPadding + + diagonalOffset = rightData.nonDiscardedCount + 1 + forwardDiagonals = Array(repeating: 0, count: diagonals) + backwardDiagonals = Array(repeating: 0, count: diagonals) + + compareSequences( + xoff: 0, + xlim: leftData.nonDiscardedCount, + yoff: 0, + ylim: rightData.nonDiscardedCount + ) + + forwardDiagonals = [] + backwardDiagonals = [] + + // modify the results slightly to make them prettier + // in cases where that can validly be done + leftData.shiftBoundaries(against: rightData) + rightData.shiftBoundaries(against: leftData) + + return buildScript() + } + + /// Compare in detail contiguous subsequences of the two sequences which are known, + /// as a whole, to match each other. + /// + /// The results are recorded by marking as changed each element that is an insertion + /// or a deletion. The subsequence of the left side is `[xoff, xlim)` and likewise + /// `[yoff, ylim)` for the right one, all element numbers are origin-0 and the + /// discarded elements are not counted. + private func compareSequences( + xoff: Int, + xlim: Int, + yoff: Int, + ylim: Int + ) { + var xoff = xoff + var xlim = xlim + var yoff = yoff + var ylim = ylim + + // slide down the bottom initial diagonal + while xoff < xlim, yoff < ylim, leftVector[xoff] == rightVector[yoff] { + xoff += 1 + yoff += 1 + } + // slide up the top initial diagonal + while xlim > xoff, ylim > yoff, leftVector[xlim - 1] == rightVector[ylim - 1] { + xlim -= 1 + ylim -= 1 + } + + // handle simple cases + if xoff == xlim { + while yoff < ylim { + rightData.markChanged(virtualIndex: yoff) + yoff += 1 + } + } else if yoff == ylim { + while xoff < xlim { + leftData.markChanged(virtualIndex: xoff) + xoff += 1 + } + } else { + // find a point of correspondence in the middle of the sequences + let diagonal = middleSnake(xoff: xoff, xlim: xlim, yoff: yoff, ylim: ylim) + let backward = backwardDiagonals[diagonalOffset + diagonal] + + // use that point to split this problem into two subproblems + compareSequences(xoff: xoff, xlim: backward, yoff: yoff, ylim: backward - diagonal) + // this used to use the forward value instead of the backward one, but that is + // incorrect, it is not necessarily the case that the diagonal has a snake + // from the backward point to the forward one + compareSequences(xoff: backward, xlim: xlim, yoff: backward - diagonal, ylim: ylim) + } + } + + /// Find the midpoint of the shortest edit script for a specified portion + /// of the two sequences. + /// + /// We scan from the beginnings of the sequences, and simultaneously from the ends, + /// doing a breadth-first search through the space of edit-sequence. + /// When the two searches meet, we have found the midpoint of the shortest edit sequence. + /// + /// This function assumes that the first elements of the specified portions of the two + /// sequences do not match, and likewise that the last elements do not match, the caller + /// must trim the matching elements from the beginning and the end of the portions it + /// is going to specify. + /// + /// Note that if we return the "wrong" diagonal value, or if the backward value at that + /// diagonal is "wrong", the worst this can do is cause suboptimal output, it cannot + /// cause incorrect output. + /// - Returns: the number of the diagonal on which the midpoint lies, the diagonal number + /// equals the number of inserted elements minus the number of deleted ones, counting + /// only the elements before the midpoint + private func middleSnake( + xoff: Int, + xlim: Int, + yoff: Int, + ylim: Int + ) -> Int { + // minimum and maximum valid diagonal + let dmin = xoff - ylim + let dmax = xlim - yoff + // center diagonal of the top-down and of the bottom-up search + let fmid = xoff - yoff + let bmid = xlim - ylim + // limits of the top-down and of the bottom-up search + var fmin = fmid + var fmax = fmid + var bmin = bmid + var bmax = bmid + // true if southeast corner is on an odd diagonal with respect to the northwest + let odd = ((fmid - bmid) & 1) != 0 + + forwardDiagonals[diagonalOffset + fmid] = xoff + backwardDiagonals[diagonalOffset + bmid] = xlim + + while true { + // extend the top-down search by an edit step in each diagonal + if fmin > dmin { + fmin -= 1 + forwardDiagonals[diagonalOffset + fmin - 1] = -1 + } else { + fmin += 1 + } + + if fmax < dmax { + fmax += 1 + forwardDiagonals[diagonalOffset + fmax + 1] = -1 + } else { + fmax -= 1 + } + var diagonal = fmax + + while diagonal >= fmin { + let low = forwardDiagonals[diagonalOffset + diagonal - 1] + let high = forwardDiagonals[diagonalOffset + diagonal + 1] + var x = low >= high ? low + 1 : high + var y = x - diagonal + + while x < xlim, y < ylim, leftVector[x] == rightVector[y] { + x += 1 + y += 1 + } + forwardDiagonals[diagonalOffset + diagonal] = x + + if odd, bmin <= diagonal, diagonal <= bmax, + backwardDiagonals[diagonalOffset + diagonal] <= x { + return diagonal + } + diagonal -= 2 + } + + // similarly extend the bottom-up search + if bmin > dmin { + bmin -= 1 + backwardDiagonals[diagonalOffset + bmin - 1] = .max + } else { + bmin += 1 + } + + if bmax < dmax { + bmax += 1 + backwardDiagonals[diagonalOffset + bmax + 1] = .max + } else { + bmax -= 1 + } + diagonal = bmax + + while diagonal >= bmin { + let low = backwardDiagonals[diagonalOffset + diagonal - 1] + let high = backwardDiagonals[diagonalOffset + diagonal + 1] + var x = low < high ? low : high - 1 + var y = x - diagonal + + while x > xoff, y > yoff, leftVector[x - 1] == rightVector[y - 1] { + x -= 1 + y -= 1 + } + backwardDiagonals[diagonalOffset + diagonal] = x + + if !odd, fmin <= diagonal, diagonal <= fmax, + x <= forwardDiagonals[diagonalOffset + diagonal] { + return diagonal + } + diagonal -= 2 + } + } + } + + /// Scan the tables of which elements are inserted and deleted, producing an edit script. + /// + /// The tables are walked backwards, so the changes are collected in reverse order + /// and reversed before being returned. + private func buildScript() -> [DiffChange] { + var changes = [DiffChange]() + // the flags are origin-1, index 0 and the last one are the always false sentinels + var index0 = leftData.count + var index1 = rightData.count + let leftChanged = leftData.changedFlags + let rightChanged = rightData.changedFlags + + while index0 >= 0 || index1 >= 0 { + if leftChanged[index0] || rightChanged[index1] { + let line0 = index0 + let line1 = index1 + + // find how many elements changed here in each sequence + while leftChanged[index0] { + index0 -= 1 + } + + while rightChanged[index1] { + index1 -= 1 + } + changes.append(DiffChange( + line0: index0, + line1: index1, + deleted: line0 - index0, + inserted: line1 - index1 + )) + } + // we have reached elements in the two sequences that match each other + index0 -= 1 + index1 -= 1 + } + + return Array(changes.reversed()) + } +} diff --git a/Sources/Features/FilesCompare/DiffResult/InlineDiff.swift b/Sources/Features/FilesCompare/DiffResult/InlineDiff.swift new file mode 100644 index 0000000..d3358a4 --- /dev/null +++ b/Sources/Features/FilesCompare/DiffResult/InlineDiff.swift @@ -0,0 +1,189 @@ +// +// InlineDiff.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +/// character level differences between the two lines of a changed pair +/// +/// The ranges are character offsets into the line text, ascending and non overlapping. +/// A pure insertion leaves the left side without ranges, a pure deletion leaves +/// the right side without ranges. +struct InlineDiff { + /// above this length the character comparison degrades to a single range per side, + /// generated or minified lines would otherwise make the comparison too slow + private static let maxDiffableLength = 2000 + + /// above this many character pairs left once the common prefix and suffix are gone + /// the two lines share almost nothing, the exact comparison would cost far more than + /// the single range per side it refines + private static let maxDiffablePairs = 16384 + + /// two differences separated by an equal run no longer than this are reported as a + /// single one, a lone equal character in the middle of a rewritten field would + /// otherwise scatter the highlight over it + private static let maxAggregatedRunLength = 1 + + let leftRanges: [Range] + let rightRanges: [Range] + + // only ignoreCharacterCase is honoured, the whitespace options drive the line + // comparison and are not applied inside a line + static func between( + _ left: DiffLineComponent, + _ right: DiffLineComponent, + options: DiffResult.Options + ) -> InlineDiff { + let leftCount = left.text.count + let rightCount = right.text.count + let (prefix, suffix) = commonAffixes( + left.text, + right.text, + shortest: min(leftCount, rightCount), + options: options + ) + let leftRange = prefix ..< leftCount - suffix + let rightRange = prefix ..< rightCount - suffix + + // with one side left empty the other one is a pure insertion or deletion, and + // past the length or the pair limit the exact ranges are not worth their cost, + // all degrade to the single range already delimited by the prefix and the suffix + if leftRange.isEmpty + || rightRange.isEmpty + || leftCount > maxDiffableLength + || rightCount > maxDiffableLength + || leftRange.count * rightRange.count > maxDiffablePairs { + return InlineDiff( + leftRanges: ranges(from: leftRange.lowerBound, to: leftRange.upperBound), + rightRanges: ranges(from: rightRange.lowerBound, to: rightRange.upperBound) + ) + } + // the discard heuristic must be skipped here, on a single line it would report + // every frequent character as changed + let changes = SequenceDiff.changes( + left: keys(of: left.text, in: leftRange, options: options), + right: keys(of: right.text, in: rightRange, options: options), + ignoresDiscards: true + ) + var leftRanges = [Range]() + var rightRanges = [Range]() + + // the engine compared the characters left between the prefix and the suffix, its + // offsets are relative to them + for change in aggregated(changes) { + if change.deleted > 0 { + let start = prefix + change.line0 + leftRanges.append(start ..< start + change.deleted) + } + + if change.inserted > 0 { + let start = prefix + change.line1 + rightRanges.append(start ..< start + change.inserted) + } + } + + return InlineDiff(leftRanges: leftRanges, rightRanges: rightRanges) + } + + /// one key per character of the range, so the reported offsets stay character offsets + /// while the comparison keeps the exact code unit semantics the diff engine needs + private static func keys( + of text: String, + in range: Range, + options: DiffResult.Options + ) -> [SequenceDiff.TextKey] { + text.dropFirst(range.lowerBound) + .prefix(range.count) + .map { key(for: $0, options: options) } + } + + private static func key( + for character: Character, + options: DiffResult.Options + ) -> SequenceDiff.TextKey { + options.contains(.ignoreCharacterCase) + ? SequenceDiff.TextKey(character.lowercased()) + : SequenceDiff.TextKey(String(character)) + } + + private static func ranges(from lower: Int, to upper: Int) -> [Range] { + lower < upper ? [lower ..< upper] : [] + } + + // the first line takes the ranges of the left side + static func apply( + to firstLine: DiffLine, + and secondLine: DiffLine, + options: DiffResult.Options + ) { + let inlineDiff = between(firstLine.component, secondLine.component, options: options) + + firstLine.inlineRanges = inlineDiff.leftRanges + secondLine.inlineRanges = inlineDiff.rightRanges + } + + // the common prefix and the common suffix cannot hold a difference, they are walked + // on the text so that the characters they cover are never turned into keys + private static func commonAffixes( + _ leftText: String, + _ rightText: String, + shortest: Int, + options: DiffResult.Options + ) -> (prefix: Int, suffix: Int) { + var prefix = 0 + + for (leftCharacter, rightCharacter) in zip(leftText, rightText) { + if key(for: leftCharacter, options: options) != key(for: rightCharacter, options: options) { + break + } + prefix += 1 + } + // the prefix is skipped so the two runs cannot overlap on the shorter line + let available = shortest - prefix + var suffix = 0 + + for (leftCharacter, rightCharacter) in zip(leftText.reversed(), rightText.reversed()) { + if suffix >= available { + break + } + + if key(for: leftCharacter, options: options) != key(for: rightCharacter, options: options) { + break + } + suffix += 1 + } + + return (prefix, suffix) + } + + /// joins the differences a short run of equal characters keeps apart, so a field + /// rewritten as a whole is reported as a whole + /// + /// The run is merged into the difference that swallows it, the two sides keep the + /// same ranges they would have with the run reported as changed. + private static func aggregated(_ changes: [DiffChange]) -> [DiffChange] { + var merged = [DiffChange]() + + for change in changes { + // the equal characters between two differences are the same on both sides, + // so the left run measures the right one too + guard let previous = merged.last, + change.line0 - (previous.line0 + previous.deleted) <= maxAggregatedRunLength + else { + merged.append(change) + continue + } + + merged[merged.count - 1] = DiffChange( + line0: previous.line0, + line1: previous.line1, + deleted: change.line0 + change.deleted - previous.line0, + inserted: change.line1 + change.inserted - previous.line1 + ) + } + + return merged + } +} diff --git a/Sources/Features/FilesCompare/Extensions/DiffResult/DiffLine+Color.swift b/Sources/Features/FilesCompare/Extensions/DiffResult/DiffLine+Color.swift index 0a16419..57d48a9 100644 --- a/Sources/Features/FilesCompare/Extensions/DiffResult/DiffLine+Color.swift +++ b/Sources/Features/FilesCompare/Extensions/DiffResult/DiffLine+Color.swift @@ -12,6 +12,20 @@ extension DiffLine { return CommonPrefs.shared.fileColor(color) } + // a color file without the changedInline entry falls back to the swapped line colors, + // so the highlight keeps working instead of silently disappearing, the swap contrasts + // by construction because the two colors already contrast + var inlineColors: ColorSet? { + if let inlineColors = CommonPrefs.shared.fileColor(.changedInline) { + return inlineColors + } + guard let colors else { + return nil + } + + return ColorSet(text: colors.background, background: colors.text) + } + func color(for colorKey: ColorSet.Key, isSelected: Bool) -> NSColor { var color: NSColor? diff --git a/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift b/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift index 0a54460..df50a8a 100644 --- a/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift +++ b/Sources/Features/FoldersCompare/Components/DisplayFiltersScopeBar.swift @@ -36,13 +36,14 @@ private enum ScopeGroupOptions: Int { } class DisplayFiltersScopeBar: ScopeBarView { - var actionDelegate: DisplayFiltersScopeBarDelegate? + weak var actionDelegate: DisplayFiltersScopeBarDelegate? let findView = FindText(frame: .zero) func initScopeBar(_ actionDelegate: DisplayFiltersScopeBarDelegate) { self.actionDelegate = actionDelegate findView.placeholder = NSLocalizedString("Find File Name <⌘F>", comment: "") + findView.showsFileFilters = true accessoryView = findView reload(groups: [ diff --git a/Sources/Features/FoldersCompare/Components/FilePreviewView.swift b/Sources/Features/FoldersCompare/Components/FilePreviewView.swift new file mode 100644 index 0000000..c4c7753 --- /dev/null +++ b/Sources/Features/FoldersCompare/Components/FilePreviewView.swift @@ -0,0 +1,59 @@ +// +// FilePreviewView.swift +// VisualDiffer +// +// Created by davide ficano on 28/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +import Quartz + +class FilePreviewView: NSView { + // the Quick Look view initializer is failable, when it fails the pane stays empty + private let previewView = QLPreviewView(frame: .zero, style: .normal) + + init(menu: NSMenu) { + super.init(frame: .zero) + setupViews(menu: menu) + } + + @available(*, unavailable, message: "use init(menu:)") + required init?(coder _: NSCoder) { + nil + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + + NSColor.textBackgroundColor.setFill() + bounds.fill() + } + + // a nil item leaves the pane empty, this side has no file to preview + func show(_ item: QLPreviewItem?) { + // assigning the same item again makes Quick Look render it from scratch + if let previewView, previewView.previewItem !== item { + previewView.previewItem = item + } + } + + private func setupViews(menu: NSMenu) { + // the contextual menu must be shared with the preview because it is hit first + self.menu = menu + + guard let previewView else { + return + } + + previewView.menu = menu + previewView.translatesAutoresizingMaskIntoConstraints = false + addSubview(previewView) + + NSLayoutConstraint.activate([ + previewView.topAnchor.constraint(equalTo: topAnchor), + previewView.bottomAnchor.constraint(equalTo: bottomAnchor), + previewView.leadingAnchor.constraint(equalTo: leadingAnchor), + previewView.trailingAnchor.constraint(equalTo: trailingAnchor), + ]) + } +} diff --git a/Sources/Features/FoldersCompare/Components/FolderPanelView.swift b/Sources/Features/FoldersCompare/Components/FolderPanelView.swift index bbde32d..c37bcfe 100644 --- a/Sources/Features/FoldersCompare/Components/FolderPanelView.swift +++ b/Sources/Features/FoldersCompare/Components/FolderPanelView.swift @@ -6,6 +6,8 @@ // Copyright (c) 2025 visualdiffer.com // +private let bottomBarInset: CGFloat = 3 + class FolderPanelView: TablePanelView { override var pathViewDelegate: PathControlDelegate? { willSet { @@ -14,26 +16,23 @@ class FolderPanelView: TablePanelView { } init() { - super.init(treeView: FoldersOutlineView(frame: .zero), bottomBar: NSTextField(frame: .zero)) + super.init(treeView: FoldersOutlineView(frame: .zero), bottomBar: NSTextField.hintWithTitle("")) treeView.addColumns() } override func setupBottomBarConstraints() { - super.setupBottomBarConstraints() - + // no super: it pins the scroll view straight to bottomBar.top, leaving no room for the inset NSLayoutConstraint.activate([ - bottomBar.bottomAnchor.constraint(equalTo: bottomAnchor), - bottomBar.heightAnchor.constraint(equalToConstant: 22), - bottomBar.trailingAnchor.constraint(equalTo: trailingAnchor), - bottomBar.leadingAnchor.constraint(equalTo: leadingAnchor), + scrollView.bottomAnchor.constraint(equalTo: bottomBar.topAnchor, constant: -bottomBarInset), + bottomBar.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -bottomBarInset), + bottomBar.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -bottomBarInset), + bottomBar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: bottomBarInset), ]) } override func setupBottomBar() { - bottomBar.centerVertically() - bottomBar.translatesAutoresizingMaskIntoConstraints = false - - bottomBar.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + bottomBar.alignment = .center + bottomBar.lineBreakMode = .byClipping } override func updateBottomBar() { diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Menu.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Menu.swift index 6f3ab83..032da18 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Menu.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+Menu.swift @@ -9,9 +9,7 @@ @objc protocol FoldersOutlineViewContextMenu: NSObjectProtocol { @MainActor - func compareFiles(_ sender: AnyObject?) - @MainActor - func compareFolders(_ sender: AnyObject?) + func compareItems(_ sender: AnyObject?) @MainActor func copyFileNames(_ sender: AnyObject?) @MainActor @@ -67,13 +65,8 @@ public extension FoldersOutlineView { private static func addCompareItems(_ menu: NSMenu) { menu.addItem( - withTitle: NSLocalizedString("Compare Files", comment: ""), - action: #selector(FoldersOutlineViewContextMenu.compareFiles), - keyEquivalent: "" - ) - menu.addItem( - withTitle: NSLocalizedString("Compare Folders", comment: ""), - action: #selector(FoldersOutlineViewContextMenu.compareFolders), + withTitle: NSLocalizedString("Compare", comment: ""), + action: #selector(FoldersOutlineViewContextMenu.compareItems), keyEquivalent: "" ) } diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift index 2717113..aedea02 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView+VisibleItem.swift @@ -28,14 +28,7 @@ extension FoldersOutlineView { return arr } - @discardableResult - func select( - visibleItems items: [VisibleItem], - scrollToFirst: Bool = false, - center: Bool = false, - selectLinked: Bool = false, - byExtendingSelection: Bool = false - ) -> Bool { + func rows(forItems items: [VisibleItem]) -> IndexSet { var indexes = IndexSet() for vi in items { @@ -45,8 +38,19 @@ extension FoldersOutlineView { } } - return select( - rows: indexes, + return indexes + } + + @discardableResult + func select( + visibleItems items: [VisibleItem], + scrollToFirst: Bool = false, + center: Bool = false, + selectLinked: Bool = false, + byExtendingSelection: Bool = false + ) -> Bool { + select( + rows: rows(forItems: items), scrollToFirst: scrollToFirst, center: center, selectLinked: selectLinked, @@ -107,7 +111,7 @@ extension FoldersOutlineView { parent = parent?.parent } - for parent in parents.reversed() { + for parent in parents.reversed() where !isItemExpanded(parent) { expandItem(parent) } } diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift index 59d2f5e..756ee6a 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineView.swift @@ -13,6 +13,7 @@ protocol FoldersOutlineViewDelegate: NSOutlineViewDelegate { func foldersOutlineView(_ view: FoldersOutlineView, doubleClickFileObject clickedRow: Int) func selectionChanged(in view: FoldersOutlineView) func setLastUsedViewResponder(_ view: FoldersOutlineView) + func execute(_ view: FoldersOutlineView) } public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositionable, ViewLinkable { @@ -136,12 +137,20 @@ public class FoldersOutlineView: NSOutlineView, @preconcurrency DisplayPositiona let key = str[str.startIndex].asciiValue ?? 0 if key == NSCarriageReturnCharacter || key == NSEnterCharacter { - handleEnterKeypressed(selectedRow) + if event.modifierFlags.contains(.option) { + (delegate as? FoldersOutlineViewDelegate)?.execute(self) + } else { + handleEnterKeypressed(selectedRow) + } } else if event.keyCode == KeyCode.forwardDeleteCharacter { // see http://www.evernote.com/shard/s106/sh/1ed6fa73-10bd-445e-a086-0c8fcdf1af43/0285d57405aa350a98fcb5c6ffc09067 (delegate as? FoldersOutlineViewContextMenu)?.deleteFiles(nil) } else if str == " " { QLPreviewPanel.toggle() + } else if event.keyCode == KeyCode.tabCharacter, let linkedView { + // tab moves the focus between the two panels + // the key view loop is not used because it walks out of the panels + window?.makeFirstResponder(linkedView) } else { super.keyDown(with: event) } diff --git a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineViewFindTextDelegate.swift b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineViewFindTextDelegate.swift index c68bde1..d1c4541 100644 --- a/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineViewFindTextDelegate.swift +++ b/Sources/Features/FoldersCompare/Components/FoldersOutlineView/FoldersOutlineViewFindTextDelegate.swift @@ -27,25 +27,22 @@ class FoldersOutlineViewFindTextDelegate: @preconcurrency FindTextDelegate { return false } - func find(findText _: FindText, searchPattern pathPattern: String) -> Bool { - // use the search pattern exactly as entered because path normalization changes - // components such as the current and parent directories and can turn a bare file - // name into an absolute path - let globPattern = pathPattern.convertGlobMetaCharsToRegexpMetaChars() - let re = try? NSRegularExpression( - pattern: globPattern, - options: .caseInsensitive - ) + func find(findText: FindText, searchPattern pattern: String) -> Bool { + let options = findText.findOptions - guard let re, + guard let matcher = options.matcher(for: pattern), let firstChild = view.dataSource?.outlineView?(view, child: 0, ofItem: nil) as? VisibleItem, let rootVisibleItem = firstChild.item.parent?.visibleItem else { return false } - // enable the search full path if the pattern contains a path separator - let searchFullPath = pathPattern.contains("/") - rootVisibleItem.findFileName(regex: re, searchFullPath: searchFullPath, items: &fileNames) + let filter = FileNameFilter( + matches: matcher, + includesFiles: options.matchFiles, + includesFolders: options.matchFolders + ) + + fileNames = rootVisibleItem.findItems(matching: filter) return true } @@ -57,4 +54,24 @@ class FoldersOutlineViewFindTextDelegate: @preconcurrency FindTextDelegate { func clearMatches(in _: FindText) { fileNames.removeAll() } + + func selectAllMatches(in _: FindText, side: DisplaySide) { + var expandedParents = Set() + + // expand every parent first because the rows to select are known only when all matches are visible + for vi in fileNames { + // items sharing the parent share the whole ancestor chain, expanding it once is enough + guard let parent = vi.item.parent, + expandedParents.insert(ObjectIdentifier(parent)).inserted else { + continue + } + + view.expandParents(of: vi) + } + + // the matches belong to the tree of view, the rows of the two sides are aligned + let target = side == view.side ? view : view.linkedView + + target?.select(rows: view.rows(forItems: fileNames), scrollToFirst: true, center: true) + } } diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift index 6334e42..134b9b4 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/FileOperationManagerDelegateImpl.swift @@ -13,24 +13,14 @@ class FileOperationManagerDelegateImpl: FileOperationManagerDelegate { self.progressIndicatorController = progressIndicatorController } + // waitPause must block the worker thread, not the main thread func waitPause(for _: FileOperationManager) { - guard let pic = progressIndicatorController else { - return - } - - DispatchQueue.main.sync { - pic.waitPause() - } + progressIndicatorController?.waitPause() } + // isRunning is polled once per item, it must not hop to the main thread func isRunning(_: FileOperationManager) -> Bool { - guard let pic = progressIndicatorController else { - return false - } - - return DispatchQueue.main.sync { - pic.isRunning() - } + progressIndicatorController?.isRunning() ?? false } func fileManager( diff --git a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift index f149d18..ef9a580 100644 --- a/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift +++ b/Sources/Features/FoldersCompare/Controller/FileSystemController/Progress/ProgressIndicatorController.swift @@ -37,12 +37,16 @@ class ProgressIndicatorController: NSWindowController { errorView.errors } - private var running = false - private var isPaused = false private var yesToAll = false private var noToAll = false - private let pauseCondition = NSCondition() + // waitPause must block the worker thread, not the main thread + private nonisolated(unsafe) var isPaused = false + private nonisolated let pauseCondition = NSCondition() + + // isRunning is polled once per item, it must not hop to the main thread + private nonisolated(unsafe) var running = false + private nonisolated let runningLock = NSLock() // MARK: - Views @@ -110,9 +114,9 @@ class ProgressIndicatorController: NSWindowController { setupViews() } - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") + @available(*, unavailable, message: "use init()") + required init?(coder _: NSCoder) { + nil } private func setupViews() { @@ -199,59 +203,56 @@ class ProgressIndicatorController: NSWindowController { @objc func stop(_ sender: AnyObject) { - guard let sender = sender as? NSButton else { - return - } - - if !running { + if !isRunning() { closeSheet(sender) return } + // the lock must not be held across the alert, otherwise the worker + // blocks the main thread while the modal run loop drains the main queue pauseCondition.lock() isPaused = true + pauseCondition.unlock() - let retVal = NSAlert.showModalConfirm( - messageText: NSLocalizedString("Are you sure to stop the operation?", comment: ""), - informativeText: NSLocalizedString("If the operation takes a long time to run, you can stop it, but the results could be inaccurate", comment: ""), - suppressPropertyName: CommonPrefs.Name.confirmStopLongOperation.rawValue - ) + // the worker must be resumed on every exit path + defer { + pauseCondition.lock() + isPaused = false + pauseCondition.broadcast() + pauseCondition.unlock() + } + + let retVal = NSAlert.showModalStopLongRunningOperation() + + // the operation can complete while the alert is on screen, the + // completion path has already chosen whether to keep the sheet open + guard isRunning() else { + return + } if retVal { - sender.isEnabled = false + stopButton.isEnabled = false stopRun() - } else { - // The operation has been completed while - // the alert was on screen so dismiss the panel - if !running { - closeSheet(sender) - } } - isPaused = false - pauseCondition.signal() - pauseCondition.unlock() } private func closeSheet(_ sender: AnyObject) { - if let window { - window.endSheet(window) - window.orderOut(sender) - } + window?.orderOut(sender) } func startRun() { - running = true + runningLock.withLock { running = true } } func stopRun() { - running = false + runningLock.withLock { running = false } fileOpCancelled = true } - func isRunning() -> Bool { - running + nonisolated func isRunning() -> Bool { + runningLock.withLock { running } } - func waitPause() { + nonisolated func waitPause() { pauseCondition.lock() while isPaused { pauseCondition.wait() diff --git a/Sources/Features/FoldersCompare/Controller/FolderReader/MainThreadFolderReaderDelegateBridge.swift b/Sources/Features/FoldersCompare/Controller/FolderReader/MainThreadFolderReaderDelegateBridge.swift index 3783c43..73604f7 100644 --- a/Sources/Features/FoldersCompare/Controller/FolderReader/MainThreadFolderReaderDelegateBridge.swift +++ b/Sources/Features/FoldersCompare/Controller/FolderReader/MainThreadFolderReaderDelegateBridge.swift @@ -16,14 +16,9 @@ class MainThreadFolderReaderDelegateBridge: FolderReaderDelegate { self.controller = controller } + // the flag is lock-based so the poll no longer hops to the main thread func isRunning(_: FolderReader) -> Bool { - guard let controller else { - return false - } - - return DispatchQueue.main.sync { - controller.running - } + controller?.running ?? false } func progress(_ folderReader: FolderReader, status: FolderReaderStatus) { @@ -40,10 +35,12 @@ class MainThreadFolderReaderDelegateBridge: FolderReaderDelegate { DispatchQueue.main.sync { controller.rootFoldersDidRead(folderReader: folderReader, foldersOnRoot: folderCount) } + // the item callbacks are the barrier that keeps the + // reader from mutating the item while the main thread reads it case let .willTraverse(item): - DispatchQueue.main.async { controller.willTraverse(item) } + DispatchQueue.main.sync { controller.willTraverse(item) } case let .didTraverse(item): - DispatchQueue.main.async { controller.didTraverse(folderReader: folderReader, item) } + DispatchQueue.main.sync { controller.didTraverse(folderReader: folderReader, item) } } } diff --git a/Sources/Features/FoldersCompare/Controller/FolderSelectionInfo.swift b/Sources/Features/FoldersCompare/Controller/FolderSelectionInfo.swift index c023cdb..89fbc0b 100644 --- a/Sources/Features/FoldersCompare/Controller/FolderSelectionInfo.swift +++ b/Sources/Features/FoldersCompare/Controller/FolderSelectionInfo.swift @@ -16,7 +16,7 @@ struct SelectionType: OptionSet { @MainActor struct FolderSelectionInfo: @preconcurrency CustomDebugStringConvertible { - private(set) var selType: SelectionType = [] + private var selType: SelectionType = [] private(set) var nullFilesCount = 0 private(set) var foldersCount = 0 private(set) var filesCount = 0 @@ -30,6 +30,30 @@ struct FolderSelectionInfo: @preconcurrency CustomDebugStringConvertible { // contains all valid folders and files indexes private(set) var validObjectsIndexes: [Int] + var hasFiles: Bool { + filesCount > 0 + } + + var hasFolders: Bool { + foldersCount > 0 + } + + var hasFilesOrFolders: Bool { + hasFiles || hasFolders + } + + var hasFilePair: Bool { + hasPair(of: .file) + } + + var hasFolderPair: Bool { + hasPair(of: .folder) + } + + var hasPairOnSingleSide: Bool { + validObjectsIndexes.count == 2 + } + init(view: FoldersOutlineView) { self.view = view let indexes = view.selectedRowIndexes @@ -85,4 +109,31 @@ struct FolderSelectionInfo: @preconcurrency CustomDebugStringConvertible { hasMultipleSel ) } + + // two items of the same type arranged as a pair: both on one side, or one per side + private func hasPair(of type: SelectionType) -> Bool { + guard selType == type, + let linkedSelInfo = view.linkedView?.selectionInfo else { + return false + } + + if linkedSelInfo.selType.isEmpty { + return count(of: type) == 2 + } + if linkedSelInfo.selType == type { + return count(of: type) == 1 && linkedSelInfo.count(of: type) == 1 + } + return false + } + + private func count(of countType: SelectionType) -> Int { + switch countType { + case .file: + filesCount + case .folder: + foldersCount + default: + 0 + } + } } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift index 869f880..c5ae40b 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Comparison.swift @@ -10,6 +10,16 @@ import UserNotifications extension CompareItem: @unchecked Sendable {} +extension FoldersWindowController: ComparisonActivityDisplayable { + var lockedScopeBar: ScopeBarView { + scopeBar + } + + var lockedPathViews: [PathView] { + [leftPanelView.pathView, rightPanelView.pathView] + } +} + public extension FoldersWindowController { // MARK: - Reload folders @@ -173,12 +183,8 @@ public extension FoldersWindowController { PowerAssertion.shared.setDisableSystemSleep(true, with: NSLocalizedString("Reading and comparing folders", comment: "")) running = true - setProgressHidden(false) + setComparisonRunning(true) progressView.updateMessage(sessionDiff.leftPath ?? "") - leftPanelView.pathView.isEnabled = false - rightPanelView.pathView.isEnabled = false - - scopeBar.setEnabledAllGroups(false) } func did(endAt: Date, startedAt: Date) { @@ -251,17 +257,9 @@ public extension FoldersWindowController { updateBottomBar(rightView) updateStatusBar() - scopeBar.setEnabledAllGroups(true) + setComparisonRunning(false) - setProgressHidden(true) - - leftPanelView.pathView.isEnabled = true - rightPanelView.pathView.isEnabled = true - - // force toolbar to enable items - window?.toolbar?.validateVisibleItems() - - consoleView.log(info: String(format: NSLocalizedString("Comparison completed in %@", comment: ""), elapsedTimeText)) + logComparisonCompleted(elapsedTimeText: elapsedTimeText) showCompareCompleteNotification(elapsedTimeText) } @@ -314,11 +312,8 @@ public extension FoldersWindowController { } extension FoldersWindowController: ItemComparatorDelegate { + // the flag is lock-based so the poll no longer hops to the main thread public nonisolated func isRunning(_: ItemComparator) -> Bool { - DispatchQueue.main.sync { [weak self] in - MainActor.assumeIsolated { - self?.running ?? false - } - } + running } } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+ConsoleViewDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+ConsoleViewDelegate.swift index 255037a..0bba11b 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+ConsoleViewDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+ConsoleViewDelegate.swift @@ -6,26 +6,12 @@ // Copyright (c) 2025 visualdiffer.com // -extension FoldersWindowController: @preconcurrency ConsoleViewDelegate { - // MARK: - Delegate - - func hide(console: ConsoleView) { - consoleSplitter.toggleSubview(at: 1) - console.focus() - } - - // MARK: - ConsoleSplitView - - func log(error: String) { - showConsoleView() - consoleView.log(error: error) - } - - func showConsoleView() { - consoleSplitter.expandSubview(at: 1) - consoleView.focus() +extension FoldersWindowController: ConsoleDisplayable { + var consoleFocusView: NSView { + lastUsedView } + // a menu item needs a real selector, a protocol extension cannot provide one @objc func toggleLogConsole(_: AnyObject?) { hide(console: consoleView) diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Document.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Document.swift index b6a53d4..323f7fa 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Document.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Document.swift @@ -54,14 +54,6 @@ extension FoldersWindowController: DiffOpenerDelegate { findDifference(from: leftPath, rightPath: rightPath, direction: .previous, block: block) } - public func hasNextDifference(from leftPath: String?, rightPath: String?) -> Bool { - findNearestDifferenceItem(from: leftPath, rightPath: rightPath, direction: .next) != nil - } - - public func hasPreviousDifference(from leftPath: String?, rightPath: String?) -> Bool { - findNearestDifferenceItem(from: leftPath, rightPath: rightPath, direction: .previous) != nil - } - public func parentPaths(from leftPath: String?, rightPath: String?) -> (leftParentPath: String, rightParentPath: String)? { guard let item = resolveCompareItem(fromLeftPath: leftPath, rightPath: rightPath), let leftParent = item.parent?.path, @@ -139,7 +131,14 @@ extension FoldersWindowController: DiffOpenerDelegate { direction: direction ) - if let (item, row) = foundItem, block(item.path, item.linkedItem?.path) { + // the block answers for the missing file too, the caller would otherwise have to + // search the tree a second time to know there is none + guard let (item, row) = foundItem else { + _ = block(nil, nil) + return + } + + if block(item.path, item.linkedItem?.path) { leftView.select( rows: IndexSet(integer: row), scrollToFirst: true, @@ -232,4 +231,39 @@ extension FoldersWindowController: DiffOpenerDelegate { } return view.row(forItem: vi) } + + func compareSelectedItems() { + let leftSelItems = leftView.selectedItems(findLeafPaths: false) + let rightSelItems = rightView.selectedItems(findLeafPaths: false) + var leftItem: CompareItem? + var rightItem: CompareItem? + + switch leftSelItems.count { + case 2: + leftItem = leftSelItems[0] + rightItem = leftSelItems[1] + case 1: + leftItem = leftSelItems.last + rightItem = rightSelItems.last + case 0: + guard rightSelItems.count == 2 else { + return + } + + leftItem = rightSelItems[0] + rightItem = rightSelItems[1] + default: + return + } + do { + if let document = try VDDocumentController.shared.openDifferDocument( + leftURL: leftItem?.toURL(), + rightURL: rightItem?.toURL() + ) { + addChildDocument(document) + } + } catch { + NSAlert(error: error).runModal() + } + } } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift index 9aeaa09..9a02703 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Exclude.swift @@ -17,7 +17,7 @@ extension FoldersWindowController { } let fsi = lastUsedView.selectionInfo - let containsFolders = fsi.foldersCount > 0 + let containsFolders = fsi.hasFolders var predicateTemplate: NSPredicate var propertyKey: (CompareItem) -> String? diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FilePreview.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FilePreview.swift new file mode 100644 index 0000000..e08669f --- /dev/null +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FilePreview.swift @@ -0,0 +1,78 @@ +// +// FoldersWindowController+FilePreview.swift +// VisualDiffer +// +// Created by davide ficano on 28/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +import Quartz + +enum FilePreviewMode { + // each panel previews the file selected on its own side, the two can be on different rows + case selectedFiles + // both panels preview the two items of the same row + case sameRow +} + +extension FoldersWindowController { + // the two panels are paired so they are always collapsed or expanded together + var isFilePreviewHidden: Bool { + leftPreviewSplitter.hasSubviewCollapsed + } + + @objc + func toggleFilePreview(_: AnyObject?) { + leftPreviewSplitter.toggleSubview() + updateFilePreview(from: lastUsedView) + } + + @objc + func hideFilePreview(_: AnyObject?) { + leftPreviewSplitter.collapseSubview() + updateFilePreview(from: lastUsedView) + } + + @objc + func showPreviewSelectedFiles(_: AnyObject?) { + filePreviewMode = .selectedFiles + updateFilePreview(from: lastUsedView) + } + + @objc + func showPreviewSameRow(_: AnyObject?) { + filePreviewMode = .sameRow + updateFilePreview(from: lastUsedView) + } + + func updateFilePreview(from view: FoldersOutlineView) { + if isFilePreviewHidden { + // a hidden preview would keep the Quick Look resources of the last item + leftPreviewView.show(nil) + rightPreviewView.show(nil) + return + } + + switch filePreviewMode { + case .selectedFiles: + // each panel keeps its own selection, so a side is empty until something is picked on it + leftPreviewView.show(previewItem(in: leftView, at: leftView.selectedRow)) + rightPreviewView.show(previewItem(in: rightView, at: rightView.selectedRow)) + case .sameRow: + // both sides are row aligned so the same index addresses the linked item + let row = view.selectedRow + leftPreviewView.show(previewItem(in: leftView, at: row)) + rightPreviewView.show(previewItem(in: rightView, at: row)) + } + } + + private func previewItem(in view: FoldersOutlineView, at row: Int) -> QLPreviewItem? { + guard row >= 0, + let vi = view.item(atRow: row) as? VisibleItem, + vi.item.path != nil else { + return nil + } + + return vi + } +} diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift index 29a3bdb..d8a4634 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineView.swift @@ -328,6 +328,7 @@ extension FoldersWindowController: NSOutlineViewDelegate, updateBottomBar(view) updateStatusBar() previewPanel?.reloadData() + updateFilePreview(from: view) } public func setLastUsedViewResponder(_ view: FoldersOutlineView) { @@ -340,6 +341,15 @@ extension FoldersWindowController: NSOutlineViewDelegate, } } + public func execute(_ view: FoldersOutlineView) { + guard !running, + view.selectionInfo.comparableType != nil else { + return + } + + compareSelectedItems() + } + // MARK: - OutlineViewExpandItemDelegate implementation public func itemDidExpand(_ item: Any?, outlineView: NSOutlineView) { diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift index fb4f370..ba988a0 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+FoldersOutlineViewContextMenu.swift @@ -8,38 +8,8 @@ extension FoldersWindowController: FoldersOutlineViewContextMenu { @objc - func compareFiles(_: AnyObject?) { - let leftSelItems = leftView.selectedItems() - let rightSelItems = rightView.selectedItems() - var leftItem: CompareItem? - var rightItem: CompareItem? - - switch leftSelItems.count { - case 2: - leftItem = leftSelItems[0] - rightItem = leftSelItems[1] - case 1: - leftItem = leftSelItems.last - rightItem = rightSelItems.last - case 0: - leftItem = rightSelItems[0] - rightItem = rightSelItems[1] - default: - return - } - do { - _ = try VDDocumentController.shared.openDifferDocument( - leftURL: leftItem?.toURL(), - rightURL: rightItem?.toURL() - ) - } catch { - NSAlert(error: error).runModal() - } - } - - @objc - func compareFolders(_ sender: AnyObject?) { - compareFiles(sender) + func compareItems(_: AnyObject?) { + compareSelectedItems() } @objc diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift index 2d3ed45..8700b0c 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+Menu.swift @@ -181,6 +181,31 @@ extension FoldersWindowController { action: #selector(findPrevious), keyEquivalent: "G" ) + + let selectAllFoundItem = NSMenuItem( + title: NSLocalizedString("Select All Found", comment: ""), + action: nil, + keyEquivalent: "" + ) + let selectAllFoundSub = NSMenu(title: NSLocalizedString("Select All Found", comment: "")) + selectAllFoundSub.addItem( + withTitle: NSLocalizedString("Left Side", comment: ""), + action: #selector(selectAllFound), + keyEquivalent: "" + ).tag = SelectionSide.left.rawValue + selectAllFoundSub.addItem( + withTitle: NSLocalizedString("Right Side", comment: ""), + action: #selector(selectAllFound), + keyEquivalent: "" + ).tag = SelectionSide.right.rawValue + selectAllFoundSub.addItem( + withTitle: NSLocalizedString("Both Sides", comment: ""), + action: #selector(selectAllFound), + keyEquivalent: "" + ).tag = SelectionSide.both.rawValue + findSub.setSubmenu(selectAllFoundSub, for: selectAllFoundItem) + findSub.addItem(selectAllFoundItem) + menu.setSubmenu(findSub, for: findItem) menu.addItem(findItem) @@ -189,13 +214,8 @@ extension FoldersWindowController { private static func addCompareItems(_ menu: NSMenu) { menu.addItem( - withTitle: NSLocalizedString("Compare Files", comment: ""), - action: #selector(compareFiles), - keyEquivalent: "" - ) - menu.addItem( - withTitle: NSLocalizedString("Compare Folders", comment: ""), - action: #selector(compareFolders), + withTitle: NSLocalizedString("Compare", comment: ""), + action: #selector(compareItems), keyEquivalent: "" ) } @@ -331,6 +351,40 @@ extension FoldersWindowController { return menu } + private static func addPreviewModeItems(_ menu: NSMenu) { + menu.addItem( + withTitle: NSLocalizedString("Selected Files", comment: ""), + action: #selector(showPreviewSelectedFiles), + keyEquivalent: "" + ) + menu.addItem( + withTitle: NSLocalizedString("Same Row", comment: ""), + action: #selector(showPreviewSameRow), + keyEquivalent: "" + ) + } + + private static func addFilePreviewItems(_ menu: NSMenu) { + let previewItem = NSMenuItem( + title: NSLocalizedString("Preview", comment: ""), + action: nil, + keyEquivalent: "" + ) + let previewSub = NSMenu(title: NSLocalizedString("Preview", comment: "")) + let showPreview = previewSub.addItem( + withTitle: NSLocalizedString("Show Preview", comment: ""), + action: #selector(toggleFilePreview), + keyEquivalent: "p" + ) + showPreview.keyEquivalentModifierMask = [.option, .command] + + previewSub.addItem(NSMenuItem.separator()) + addPreviewModeItems(previewSub) + + menu.setSubmenu(previewSub, for: previewItem) + menu.addItem(previewItem) + } + private static func viewMenu() -> NSMenu { let menu = NSMenu(title: NSLocalizedString("View", comment: "")) @@ -376,12 +430,6 @@ extension FoldersWindowController { ) collapseAllItem.keyEquivalentModifierMask = .control - menu.addItem( - withTitle: NSLocalizedString("Swap Sides", comment: ""), - action: #selector(swapSides), - keyEquivalent: "" - ) - menu.addItem( withTitle: NSLocalizedString("Refresh", comment: ""), action: #selector(refresh), @@ -415,6 +463,8 @@ extension FoldersWindowController { menu.setSubmenu(fontSub, for: fontItem) menu.addItem(fontItem) + addFilePreviewItems(menu) + let logConsole = NSMenuItem( title: NSLocalizedString("Show Log Console", comment: ""), action: #selector(toggleLogConsole), @@ -454,6 +504,25 @@ extension FoldersWindowController { mainMenu.item(withTag: MainMenu.actions.rawValue)?.submenu = StaticMenus.actions mainMenu.item(withTag: MainMenu.view.rawValue)?.submenu = StaticMenus.view } + + // a right click doesn't make its window key, so target-less items would be dispatched + // through the responder chain of the front window, acting on another comparison + func createFilePreviewMenu() -> NSMenu { + let menu = NSMenu(title: NSLocalizedString("Contextual Menu", comment: "")) + + Self.addPreviewModeItems(menu) + + menu.addItem(NSMenuItem.separator()) + menu.addItem( + withTitle: NSLocalizedString("Hide", comment: ""), + action: #selector(hideFilePreview), + keyEquivalent: "" + ) + + menu.items.forEach { $0.target = self } + + return menu + } } // swiftlint:enable file_length diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift index f72cf35..fb88723 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+MenuDelegate.swift @@ -58,8 +58,9 @@ extension FoldersWindowController: NSMenuDelegate, return fsi.validateSetAsBaseFolderOtherSide() } else if action == #selector(setAsBaseFoldersBothSides) { return fsi.validateSetAsBaseFoldersBothSides() - } else if action == #selector(compareFiles) { - return fsi.validateCompareFiles() + } else if action == #selector(compareItems) { + menuItem.title = fsi.compareItemsTitle + return fsi.comparableType != nil } else if action == #selector(excludeByName) { var fileName: String? = "" if !fsi.validateExclude(byName: &fileName) { @@ -116,8 +117,8 @@ extension FoldersWindowController: NSMenuDelegate, return scopeBar.findView.hasMatches } else if action == #selector(findPrevious) { return scopeBar.findView.hasMatches - } else if action == #selector(compareFolders) { - return fsi.validateCompareFolders() + } else if action == #selector(selectAllFound) { + return scopeBar.findView.hasMatches } else if action == #selector(moveFiles) { switch fsi.view.side { case .left: @@ -135,6 +136,19 @@ extension FoldersWindowController: NSMenuDelegate, menuItem.title = NSLocalizedString("Hide Log Console", comment: "") } return true + } else if action == #selector(toggleFilePreview) { + if isFilePreviewHidden { + menuItem.title = NSLocalizedString("Show Preview", comment: "") + } else { + menuItem.title = NSLocalizedString("Hide Preview", comment: "") + } + return true + } else if action == #selector(showPreviewSelectedFiles) { + menuItem.state = filePreviewMode == .selectedFiles ? .on : .off + return !isFilePreviewHidden + } else if action == #selector(showPreviewSameRow) { + menuItem.state = filePreviewMode == .sameRow ? .on : .off + return !isFilePreviewHidden } else if action == #selector(togglePreviewPanel) { if QLPreviewPanel.sharedPreviewPanelExists(), QLPreviewPanel.shared().isVisible { menuItem.title = NSLocalizedString("Close Quick Look", comment: "") @@ -172,6 +186,7 @@ extension FoldersWindowController: NSMenuDelegate, func tableView(_ tableView: NSTableView, menuItem: NSMenuItem, hideMenuItem hide: inout Bool) -> Bool { if running { + hide = true return false } guard let folderView = tableView as? FoldersOutlineView else { diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift index 388f247..2eb70a7 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSToolbarDelegate.swift @@ -25,6 +25,7 @@ extension NSToolbarItem.Identifier { static let differenceNavigation = NSToolbarItem.Identifier("DifferenceNavigation") static let openWith = NSToolbarItem.Identifier("OpenWith") static let showInFinder = NSToolbarItem.Identifier("ShowInFinder") + static let compareItems = NSToolbarItem.Identifier("CompareItems") } } @@ -69,6 +70,7 @@ extension FoldersWindowController: NSToolbarDelegate, NSToolbarItemValidation { .Folders.differenceNavigation, .Folders.openWith, .Folders.showInFinder, + .Folders.compareItems, ] } @@ -218,6 +220,15 @@ extension FoldersWindowController: NSToolbarDelegate, NSToolbarItemValidation { target: self, action: #selector(showInFinder) ) + } else if itemIdentifier == .Folders.compareItems { + return NSToolbarItem( + identifier: itemIdentifier, + label: NSLocalizedString("Compare", comment: ""), + tooltip: NSLocalizedString("Compare selected files or folders", comment: ""), + image: VDSymbol.Toolbar.compareItems.image(), + target: self, + action: #selector(compareItems) + ) } return nil @@ -249,6 +260,9 @@ extension FoldersWindowController: NSToolbarDelegate, NSToolbarItemValidation { var path: String? return fsi.validateOpen(withApp: &path) } + if item.itemIdentifier == .Folders.compareItems { + return fsi.comparableType != nil + } return true } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSWindowDelegate.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSWindowDelegate.swift index 78f2459..837b672 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSWindowDelegate.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+NSWindowDelegate.swift @@ -19,6 +19,9 @@ extension FoldersWindowController: NSWindowDelegate { removeObservers() removeAllChildrenDocuments() + + leftPanelView.unbindControls() + rightPanelView.unbindControls() } public func window(_: NSWindow, willUseFullScreenPresentationOptions proposedOptions: NSApplication.PresentationOptions = []) -> NSApplication.PresentationOptions { diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UICreation.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UICreation.swift index 4b110c4..9721324 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UICreation.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UICreation.swift @@ -6,9 +6,18 @@ // Copyright (c) 2025 visualdiffer.com // +// height given to the preview pane until the user resizes it +private let previewPaneDefaultHeight: CGFloat = 200.0 + +// the panels are laid out by nested split views, so their frame is still the seed one when the +// constraints are first evaluated: every pane the seed gets divided into must clear the panel +// minimum content height, 40, or that first pass is unsatisfiable and AppKit logs a conflict. +// the value never reaches the screen, from the first layout on the size comes from the window +private let folderPanelsSeedHeight: CGFloat = 400.0 + extension FoldersWindowController { func createFolderPanelsSplitView() -> NSSplitView { - let view = NSSplitView(frame: NSRect(x: 0, y: 0, width: 1, height: 0)) + let view = NSSplitView(frame: NSRect(x: 0, y: 0, width: 1, height: folderPanelsSeedHeight)) view.dividerStyle = .thin view.isVertical = true @@ -76,18 +85,16 @@ extension FoldersWindowController { return view } - func createConsoleView() -> ConsoleView { - let view = ConsoleView(frame: NSRect(x: 0, y: 0, width: 1, height: 0)) - view.delegate = self - - return view - } + func createPreviewSplitter(panel: FolderPanelView, preview: FilePreviewView) -> DualPaneSplitView { + // the two splitters are laid out side by side, a zero width makes the parent + // split view divide by zero and give the whole width to the first one + let view = DualPaneSplitView(frame: NSRect(x: 0, y: 0, width: 1, height: 0)) - func createConsoleSplitter() -> DualPaneSplitView { - let frame = window?.contentView?.bounds ?? .zero - let view = DualPaneSplitView(frame: .zero) - view.translatesAutoresizingMaskIntoConstraints = false - view.firstViewSize = frame.height * 3 / 4 + // the panel and the preview share the same background, the thin style would hide the divider + view.dividerStyle = .paneSplitter + view.collapsablePaneSize = previewPaneDefaultHeight + view.addArrangedSubview(panel) + view.addArrangedSubview(preview) return view } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift index 72de2f3..484d19d 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController+UISetup.swift @@ -11,8 +11,6 @@ extension FoldersWindowController { setupWindowLayout() setupFoldersLayout() - leftPanelView.treeView.nextKeyView = rightView - updateTreeViewFont() adjustTableColumnsWidth() @@ -125,6 +123,7 @@ extension FoldersWindowController { updateScopeBar() setupSortDescriptors() setupConsoleSplitter() + setupPreviewSplitters() leftPanelView.bindControls() rightPanelView.bindControls() @@ -144,9 +143,14 @@ extension FoldersWindowController { view.sortDescriptors = [sessionDiff.columnSortDescriptor()] } - func setupConsoleSplitter() { - consoleSplitter.delegate = consoleDelegate - consoleSplitter.collapseSubview(at: 1) + func setupPreviewSplitters() { + leftPreviewSplitter.delegate = previewDelegate + rightPreviewSplitter.delegate = previewDelegate + + // paired, so collapsing, expanding and resizing one panel drives the other + leftPreviewSplitter.setSynchronized(splitView: rightPreviewSplitter) + + leftPreviewSplitter.collapseSubview() } func updateBottomBar(_ view: FoldersOutlineView) { @@ -176,16 +180,4 @@ extension FoldersWindowController { ) differenceCounters.update(counters: items) } - - func setProgressHidden(_ hidden: Bool) { - if hidden { - differenceCounters.isHidden = false - statusbarText.isHidden = false - progressView.isHidden = true - } else { - differenceCounters.isHidden = true - statusbarText.isHidden = true - progressView.isHidden = false - } - } } diff --git a/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift b/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift index c1489a0..0a9a376 100644 --- a/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift +++ b/Sources/Features/FoldersCompare/Controller/FoldersWindowController.swift @@ -10,9 +10,13 @@ import Quartz import UserNotifications import os.log -// the heights are in flipped coordinates -private let splitViewMinHeight: CGFloat = 160.0 -private let splitViewMaxHeight: CGFloat = 80.0 +// minimum heights of the two panes of the console splitter +private let foldersMinHeight: CGFloat = 160.0 +private let consoleMinHeight: CGFloat = 80.0 + +// minimum heights of the two panes of a preview splitter +private let previewPanelMinHeight: CGFloat = 120.0 +private let previewPaneMinHeight: CGFloat = 60.0 public class FoldersWindowController: NSWindowController, UNUserNotificationCenterDelegate { @@ -27,7 +31,10 @@ public class FoldersWindowController: NSWindowController, // swiftlint:disable:next implicitly_unwrapped_optional @objc dynamic var sessionDiff: SessionDiff! var dontResizeColumns = false - var running = false + + private nonisolated(unsafe) var runningFlag = false + private nonisolated let runningLock = NSLock() + var previewPanel: QLPreviewPanel? var hideEmptyFolders = false @@ -75,8 +82,8 @@ public class FoldersWindowController: NSWindowController, let view = createConsoleSplitter() let folderPanels = createFolderPanelsSplitView() - folderPanels.addArrangedSubview(leftPanelView) - folderPanels.addArrangedSubview(rightPanelView) + folderPanels.addArrangedSubview(leftPreviewSplitter) + folderPanels.addArrangedSubview(rightPreviewSplitter) view.addArrangedSubview(folderPanels) view.addArrangedSubview(consoleView) @@ -84,6 +91,25 @@ public class FoldersWindowController: NSWindowController, return view }() + // each panel keeps its preview below its own bottom bar + lazy var leftPreviewSplitter: DualPaneSplitView = createPreviewSplitter( + panel: leftPanelView, + preview: leftPreviewView + ) + + lazy var rightPreviewSplitter: DualPaneSplitView = createPreviewSplitter( + panel: rightPanelView, + preview: rightPreviewView + ) + + lazy var filePreviewMenu = createFilePreviewMenu() + + lazy var leftPreviewView = FilePreviewView(menu: filePreviewMenu) + + lazy var rightPreviewView = FilePreviewView(menu: filePreviewMenu) + + var filePreviewMode: FilePreviewMode = .sameRow + lazy var consoleView: ConsoleView = createConsoleView() // hold it to be sure it isn't deallocated when used from another element @@ -92,8 +118,15 @@ public class FoldersWindowController: NSWindowController, var consoleDelegate = DualPaneSplitViewDelegate( collapsableSubViewIndex: 1, - minSize: splitViewMinHeight, - maxSize: splitViewMaxHeight + minFirstPaneSize: foldersMinHeight, + minSecondPaneSize: consoleMinHeight + ) + + // the delegate keeps no per split view state so a single one serves both panels + var previewDelegate = DualPaneSplitViewDelegate( + collapsableSubViewIndex: 1, + minFirstPaneSize: previewPanelMinHeight, + minSecondPaneSize: previewPaneMinHeight ) lazy var leftPanelView: FolderPanelView = .createFolderPanel( @@ -116,6 +149,11 @@ public class FoldersWindowController: NSWindowController, lazy var statusbarText: NSTextField = createStatusbarText() + nonisolated var running: Bool { + get { runningLock.withLock { runningFlag } } + set { runningLock.withLock { runningFlag = newValue } } + } + init() { sessionChildren = [] let window = WindowCancelOperation.createWindow() @@ -174,22 +212,6 @@ public class FoldersWindowController: NSWindowController, lastUsedView.restoreSelectionAndFocusPosition(selectedVisibleItems) } - @objc - func swapSides(_: AnyObject) { - leftVisibleItems?.swap() - - let path = sessionDiff.leftPath - sessionDiff.leftPath = sessionDiff.rightPath - sessionDiff.rightPath = path - - let readOnly = sessionDiff.leftReadOnly - sessionDiff.leftReadOnly = sessionDiff.rightReadOnly - sessionDiff.rightReadOnly = readOnly - - leftView.reloadData() - rightView.reloadData() - } - @objc func compareInfo(_: AnyObject) { guard let window else { @@ -212,6 +234,23 @@ public class FoldersWindowController: NSWindowController, window?.makeFirstResponder(scopeBar) } + @objc + func selectAllFound(_ sender: AnyObject) { + guard let sender = sender as? NSMenuItem else { + return + } + + let side = SelectionSide(menuItem: sender) + let findView = scopeBar.findView + + if side.contains(.left) { + findView.delegate?.selectAllMatches(in: findView, side: .left) + } + if side.contains(.right) { + findView.delegate?.selectAllMatches(in: findView, side: .right) + } + } + @objc func findPrevious(_: AnyObject) { scopeBar.findView.moveToMatch(false) @@ -238,12 +277,14 @@ public class FoldersWindowController: NSWindowController, @objc func stopRefresh(_: AnyObject) { - let retVal = NSAlert.showModalConfirm( - messageText: NSLocalizedString("Are you sure to stop the operation?", comment: ""), - informativeText: NSLocalizedString("If the operation takes a long time to run, you can stop it, but the results could be inaccurate", comment: ""), - suppressPropertyName: CommonPrefs.Name.confirmStopLongOperation.rawValue - ) - if retVal { + guard running else { + return + } + + let retVal = NSAlert.showModalStopLongRunningOperation() + + // the comparison can complete while the alert is on screen + if retVal, running { showConsoleView() consoleView.log(warning: NSLocalizedString("Stopped comparison", comment: "")) running = false diff --git a/Sources/Features/FoldersCompare/Extensions/NSAlert+StopLongRunningOperation.swift b/Sources/Features/FoldersCompare/Extensions/NSAlert+StopLongRunningOperation.swift new file mode 100644 index 0000000..8e42669 --- /dev/null +++ b/Sources/Features/FoldersCompare/Extensions/NSAlert+StopLongRunningOperation.swift @@ -0,0 +1,17 @@ +// +// NSAlert+StopLongRunningOperation.swift +// VisualDiffer +// +// Created by davide ficano on 15/09/26. +// Copyright (c) 2026 visualdiffer.com +// + +extension NSAlert { + static func showModalStopLongRunningOperation() -> Bool { + NSAlert.showModalConfirm( + messageText: NSLocalizedString("Are you sure to stop the operation?", comment: ""), + informativeText: NSLocalizedString("If the operation takes a long time to run, you can stop it, but the results could be inaccurate", comment: ""), + suppressPropertyName: CommonPrefs.Name.confirmStopLongOperation.rawValue + ) + } +} diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift index c5514e6..abfc3c7 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+CompareActionValidator.swift @@ -8,31 +8,34 @@ @MainActor extension FolderSelectionInfo { - func validateCompareFiles() -> Bool { - guard let linkedSelInfo = view.linkedView?.selectionInfo else { - return false + var comparableType: SelectionType? { + if hasFilePair { + return .file } - - if selType == .file, linkedSelInfo.selType.isEmpty { - return filesCount == 2 - } - if selType == .file, linkedSelInfo.selType == .file { - return filesCount == 1 && linkedSelInfo.filesCount == 1 + if hasFolderPair { + return .folder } - return false + return nil } - func validateCompareFolders() -> Bool { - guard let linkedSelInfo = view.linkedView?.selectionInfo else { - return false - } - - if selType == .folder, linkedSelInfo.selType.isEmpty { - return foldersCount == 2 - } - if selType == .folder, linkedSelInfo.selType == .folder { - return foldersCount == 1 && linkedSelInfo.foldersCount == 1 + var compareItemsTitle: String { + switch (comparableType, hasPairOnSingleSide, view.side) { + case (.file?, false, _): + NSLocalizedString("Compare Files", comment: "") + case (.file?, true, .left): + NSLocalizedString("Compare Files on Left", comment: "") + case (.file?, true, .right): + NSLocalizedString("Compare Files on Right", comment: "") + case (.folder?, false, _): + NSLocalizedString("Compare Folders", comment: "") + case (.folder?, true, .left): + NSLocalizedString("Compare Folders on Left", comment: "") + case (.folder?, true, .right): + NSLocalizedString("Compare Folders on Right", comment: "") + case (_, _, .left): + NSLocalizedString("Compare on Left", comment: "") + case (_, _, .right): + NSLocalizedString("Compare on Right", comment: "") } - return false } } diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FileSystemActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FileSystemActionValidator.swift index 8ecfe32..4df2f4b 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FileSystemActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FileSystemActionValidator.swift @@ -15,11 +15,11 @@ extension FolderSelectionInfo { case .right: !sessionDiff.leftReadOnly } - return isValid && (foldersCount > 0 || filesCount > 0) + return isValid && hasFilesOrFolders } func validateCopyFilesToExternal() -> Bool { - foldersCount > 0 || filesCount > 0 + hasFilesOrFolders } func validateMoveFiles(_ sessionDiff: SessionDiff) -> Bool { @@ -29,7 +29,7 @@ extension FolderSelectionInfo { case .right: !sessionDiff.leftReadOnly && !sessionDiff.rightReadOnly } - return isValid && (foldersCount > 0 || filesCount > 0) + return isValid && hasFilesOrFolders } func validateMoveFilesToExternal(_ sessionDiff: SessionDiff) -> Bool { @@ -39,7 +39,7 @@ extension FolderSelectionInfo { case .right: !sessionDiff.rightReadOnly } - return isValid && (foldersCount > 0 || filesCount > 0) + return isValid && hasFilesOrFolders } func validateSyncFiles(_ sessionDiff: SessionDiff) -> Bool { @@ -49,7 +49,7 @@ extension FolderSelectionInfo { case .right: !sessionDiff.leftReadOnly } - return isValid && (foldersCount > 0 || filesCount > 0) + return isValid && hasFilesOrFolders } func validateDeleteFiles(_ sessionDiff: SessionDiff) -> Bool { @@ -59,7 +59,7 @@ extension FolderSelectionInfo { case .right: !sessionDiff.rightReadOnly } - return isValid && (foldersCount > 0 || filesCount > 0) + return isValid && hasFilesOrFolders } func validateFileTouch(_ sessionDiff: SessionDiff) -> Bool { @@ -69,10 +69,10 @@ extension FolderSelectionInfo { case .right: !sessionDiff.rightReadOnly } - return isValid && (foldersCount > 0 || filesCount > 0) + return isValid && hasFilesOrFolders } func validateClipboardCopy() -> Bool { - !selType.isDisjoint(with: [.folder, .file]) || hasValidPaths + hasFilesOrFolders || hasValidPaths } } diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift index b1e2e59..1aba490 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FilterActionValidator.swift @@ -9,13 +9,13 @@ @MainActor extension FolderSelectionInfo { func validateExclude(byName outExcludedFileName: inout String?) -> Bool { - if selType.isEmpty || selType == .nullfile { + if !hasFilesOrFolders { return false } if outExcludedFileName != nil { // Only one element selected if filesCount + foldersCount == 1 { - if let row = filesCount > 0 ? filesIndexes.first : foldersIndexes.first, + if let row = hasFiles ? filesIndexes.first : foldersIndexes.first, let vi = view.item(atRow: row) as? VisibleItem { let item = vi.item outExcludedFileName = foldersCount == 1 ? item.pathRelativeToRoot : item.fileName @@ -30,7 +30,7 @@ extension FolderSelectionInfo { func validateExclude(byExt outExcludedExt: inout String?) -> Bool { // Only valid for selection containing only files (null files are skipped) - if filesCount == 0 || foldersCount > 0 { + if !hasFiles || hasFolders { return false } let indexes = filesIndexes diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FolderActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FolderActionValidator.swift index 3c81868..e879bd5 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FolderActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+FolderActionValidator.swift @@ -17,20 +17,10 @@ extension FolderSelectionInfo { } func validateSetAsBaseFoldersBothSides() -> Bool { - guard let linkedSelInfo = view.linkedView?.selectionInfo else { - return false - } - - if selType == .folder, linkedSelInfo.selType.isEmpty { - return foldersCount == 2 - } - if selType == .folder, linkedSelInfo.selType == .folder { - return foldersCount == 1 && linkedSelInfo.foldersCount == 1 - } - return false + hasFolderPair } func validateExpandSelectedSubfolders() -> Bool { - filesCount == 0 && foldersCount > 0 + !hasFiles && hasFolders } } diff --git a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift index a8f25a2..e251950 100644 --- a/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift +++ b/Sources/Features/FoldersCompare/Validators/FolderSelectionInfo+ViewerActionValidator.swift @@ -9,7 +9,7 @@ @MainActor extension FolderSelectionInfo { func validateShowInFinder() -> Bool { - !selType.isDisjoint(with: [.folder, .file]) + hasFilesOrFolders } func validateOpen(withApp outSelectedPath: inout String?) -> Bool { @@ -27,6 +27,6 @@ extension FolderSelectionInfo { } func validatePreviewPanel() -> Bool { - foldersCount > 0 || filesCount > 0 + hasFilesOrFolders } } diff --git a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift index 3d78382..060e8cc 100644 --- a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift +++ b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem+Find.swift @@ -6,32 +6,17 @@ // Copyright (c) 2010 visualdiffer.com // -extension VisibleItem { - func findFileName( - regex: NSRegularExpression, - searchFullPath usePath: Bool, - items: inout [VisibleItem] - ) { - let fileName = if usePath { - item.path ?? item.linkedItem?.path - } else { - item.fileName ?? item.linkedItem?.fileName - } - guard let fileName else { - return - } - - if regex.firstMatch( - in: fileName, - options: [], - range: NSRange(location: 0, length: fileName.utf16.count) - ) != nil { - items.append(self) - } +struct FileNameFilter { + let matches: (String) -> Bool + let includesFiles: Bool + let includesFolders: Bool +} - for vi in children { - vi.findFileName(regex: regex, searchFullPath: usePath, items: &items) - } +extension VisibleItem { + func findItems(matching filter: FileNameFilter) -> [VisibleItem] { + var foundItems = [VisibleItem]() + findItems(matching: filter, into: &foundItems) + return foundItems } func findItems(_ isIncluded: (VisibleItem) -> Bool) -> [VisibleItem] { @@ -52,6 +37,26 @@ extension VisibleItem { findItems { $0.item.isFolder } } + private func findItems( + matching filter: FileNameFilter, + into foundItems: inout [VisibleItem] + ) { + guard let fileName = item.fileName ?? item.linkedItem?.fileName else { + return + } + + let isIncluded = item.isFolder ? filter.includesFolders : filter.includesFiles + + if isIncluded, filter.matches(fileName) { + foundItems.append(self) + } + + // the children are always visited, a folder excluded from the search can contain matching files + for vi in children { + vi.findItems(matching: filter, into: &foundItems) + } + } + private func findItems( _ isIncluded: (VisibleItem) -> Bool, into foundItems: inout [VisibleItem] diff --git a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift index 0c2f01d..f23265f 100644 --- a/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift +++ b/Sources/Features/FoldersCompare/VisibleItem/VisibleItem.swift @@ -48,20 +48,6 @@ public class VisibleItem: NSObject { children.removeAll() } - func swap() { - guard let linkedItem else { - return - } - - let tempItem = item - item = linkedItem.item - linkedItem.item = tempItem - - for vi in children { - vi.swap() - } - } - var childrenAllFiltered: Bool { if item.isFolder { for vi in children where !vi.item.isFiltered { diff --git a/Sources/Features/HistoryController/HistoryController.swift b/Sources/Features/HistoryController/HistoryController.swift index 0a19b78..e693dc1 100644 --- a/Sources/Features/HistoryController/HistoryController.swift +++ b/Sources/Features/HistoryController/HistoryController.swift @@ -222,6 +222,7 @@ class HistoryController: NSObject, } if let delegate, + tableView.selectedRow >= 0, let entity = results.fetchedObjects?[tableView.selectedRow], let str = event.charactersIgnoringModifiers, !str.isEmpty, diff --git a/Sources/Features/Preferences/Box/PreferencesBox.swift b/Sources/Features/Preferences/Box/PreferencesBox.swift index 7aad6b3..f390751 100644 --- a/Sources/Features/Preferences/Box/PreferencesBox.swift +++ b/Sources/Features/Preferences/Box/PreferencesBox.swift @@ -7,6 +7,8 @@ // class PreferencesBox: NSBox { + static let minPopupWidth: CGFloat = 140 + private var defaultDelegate: StandardUserPreferencesBoxDataSource private var checkboxes: [CommonPrefs.Name: PreferencesCheckbox] @@ -23,9 +25,9 @@ class PreferencesBox: NSBox { 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() { @@ -73,4 +75,14 @@ class PreferencesBox: NSBox { checkbox.isEnabled = delegate.preferenceBox(self, isEnabled: checkbox.prefName) } } + + // a popup carries no minimum of its own, this is what makes the box declare the width it + // needs, defaultHigh because NSTabView lays the panel out at 20 points while still detached + // and a required minimum could not hold there + func popupMinWidthConstraint(_ popup: NSPopUpButton) -> NSLayoutConstraint { + let constraint = popup.widthAnchor.constraint(greaterThanOrEqualToConstant: Self.minPopupWidth) + constraint.priority = .defaultHigh + + return constraint + } } diff --git a/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift b/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift index 75f0754..1145a50 100644 --- a/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift +++ b/Sources/Features/Preferences/Main/Controllers/BasePreferences.swift @@ -10,6 +10,13 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, static let prefsToolbarIdentifier = NSToolbar.Identifier("PreferencesToolbar") static let lastVisiblePrefTab = "lastVisiblePrefTab" + // the widest panel needs 513 points and the full toolbar 479, this covers both, the panels + // declare their own width so minWindowWidth() enlarges the window if one ever needs more + private static let defaultContentWidth: CGFloat = 520 + + // the inset a noTabsBezelBorder tab view keeps above and below its content + private static let tabViewVerticalBorder: CGFloat = 20 + private lazy var tabView: NSTabView = createTabView() private lazy var prefPanel: NSWindow = createPrefPanel() @@ -24,9 +31,9 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, 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() { @@ -45,7 +52,7 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, ] let view = NSPanel( - contentRect: .zero, + contentRect: NSRect(origin: .zero, size: NSSize(width: Self.defaultContentWidth, height: 0)), styleMask: styleMask, backing: .buffered, defer: false @@ -118,15 +125,13 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, } func selectLastUsedTab() { - var selectedItemItenIdentifier: NSToolbarItem.Identifier? - var selectedItemIndex = NSNotFound + // the first tab is the default, on the first run there is no stored tab to restore + var selectedItemItenIdentifier = toolbarIdentifiers.first + var selectedItemIndex = 0 if let lastPrefTab = UserDefaults.standard.string(forKey: Self.lastVisiblePrefTab) { let index = tabView.indexOfTabViewItem(withIdentifier: lastPrefTab) - if index == NSNotFound { - selectedItemItenIdentifier = toolbarIdentifiers.first - selectedItemIndex = 0 - } else { + if index != NSNotFound { selectedItemItenIdentifier = NSToolbarItem.Identifier(lastPrefTab) selectedItemIndex = index } @@ -150,6 +155,10 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, prefPanel.center() prefPanel.makeKeyAndOrderFront(self) + + // reopening the panel does not always reselect the tab, and without a tab change nothing + // else recomputes the geometry + resize() } // This method is called when the Preference panel is implemented as NSWindowController @@ -184,39 +193,34 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, // MARK: - Position and size - func contentRect() -> NSRect { - var contentRect = NSRect.zero - - if let selectedTabView = tabView.selectedTabViewItem?.view { - for view in selectedTabView.subviews { - // the result of Swift's GCRect.union is different from NSUnionRect - // so we stay on it - // swiftlint:disable:next legacy_nsgeometry_functions - contentRect = NSUnionRect(contentRect, view.frame) - } - } - - return contentRect - } - func toolbarHeight(_: NSWindow) -> CGFloat { + // both terms are derived from the window frame, the content view height cannot be used + // because it is already laid out for the new panel while the frame is still the old one let windowFrame = NSWindow.contentRect(forFrameRect: prefPanel.frame, styleMask: prefPanel.styleMask) - return windowFrame.size.height - (prefPanel.contentView?.frame.size.height ?? 0) + + return windowFrame.size.height - prefPanel.contentLayoutRect.size.height } func minWindowHeight() -> CGFloat { - let contentRect = contentRect() - // Border top + toolbar - return contentRect.size.height + 40 + toolbarHeight(prefPanel) + guard let selectedView = tabView.selectedTabViewItem?.view else { + return 0 + } + + // the panel fittingSize is the only quantity already updated when the tab changes, both + // the tab view frames and its own fittingSize still describe the previous panel + return selectedView.fittingSize.height + Self.tabViewVerticalBorder + toolbarHeight(prefPanel) } func resize() { let windowFrame = NSWindow.contentRect(forFrameRect: prefPanel.frame, styleMask: prefPanel.styleMask) let height = minWindowHeight() + // the width comes from the panel constraints, a wider panel enlarges the window and the + // width the user chose by dragging is never reduced + let width = max(windowFrame.size.width, minWindowWidth()) let frameRect = NSRect( x: windowFrame.origin.x, y: windowFrame.origin.y + windowFrame.size.height - height, - width: windowFrame.size.width, + width: width, height: height ) prefPanel.setFrame( @@ -230,10 +234,6 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, resize() } - func windowDidBecomeKey(_: Notification) { - resize() - } - func windowWillResize(_: NSWindow, to frameSize: NSSize) -> NSSize { // Only allow horizontal sizing NSSize(width: frameSize.width, height: prefPanel.frame.size.height) @@ -246,4 +246,16 @@ class BasePreferences: NSWindowController, NSToolbarDelegate, NSTabViewDelegate, func windowShouldClose(_ sender: NSWindow) -> Bool { sender.makeFirstResponder(nil) // validate editing } + + func minWindowWidth() -> CGFloat { + guard let selectedView = tabView.selectedTabViewItem?.view else { + return 0 + } + + // the frames union used for the height reports the current layout, already squeezed when + // the panel is too narrow, fittingSize is the width the constraints actually require + let tabViewBorder = tabView.frame.size.width - tabView.contentRect.size.width + + return selectedView.fittingSize.width + tabViewBorder + } } diff --git a/Sources/Features/Preferences/Main/Controllers/Preferences.swift b/Sources/Features/Preferences/Main/Controllers/Preferences.swift index b7e6632..2a17d5a 100644 --- a/Sources/Features/Preferences/Main/Controllers/Preferences.swift +++ b/Sources/Features/Preferences/Main/Controllers/Preferences.swift @@ -137,11 +137,12 @@ class Preferences: BasePreferences { } override func tabView(_ tabView: NSTabView, didSelect tabViewItem: NSTabViewItem?) { - super.tabView(tabView, didSelect: tabViewItem) - + // super resizes the panel by measuring it, so the data must be loaded first if let dataSource = tabViewItem?.view as? PreferencesPanelDataSource { dataSource.reloadData() } + + super.tabView(tabView, didSelect: tabViewItem) } } diff --git a/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsFoldersBox.swift b/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsFoldersBox.swift index 7ee79d6..58f9b13 100644 --- a/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsFoldersBox.swift +++ b/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsFoldersBox.swift @@ -20,15 +20,18 @@ class ConfirmationsFoldersBox: PreferencesBox { let stackView = NSStackView.preferences(with: [ createCheckBox( title: NSLocalizedString("Confirm opening a large number of windows in Finder", comment: ""), - prefName: .confirmShowInFinder + prefName: .confirmShowInFinder, + isNegated: true ), createCheckBox( title: NSLocalizedString("Confirm stopping the operation in progress", comment: ""), - prefName: .confirmStopLongOperation + prefName: .confirmStopLongOperation, + isNegated: true ), createCheckBox( title: NSLocalizedString("Warn that opened files may not be visible in Finder", comment: ""), - prefName: .confirmShowInFinderNotVisibleFiles + prefName: .confirmShowInFinderNotVisibleFiles, + isNegated: true ), separator, createCheckBox( diff --git a/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsPreferencesPanel.swift index 79b5455..961bbc0 100644 --- a/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/Confirmations/ConfirmationsPreferencesPanel.swift @@ -13,9 +13,9 @@ class ConfirmationsPreferencesPanel: NSView, PreferencesPanelDataSource { 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() { @@ -33,6 +33,7 @@ class ConfirmationsPreferencesPanel: NSView, PreferencesPanelDataSource { stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), stackView.topAnchor.constraint(equalTo: topAnchor), + panelBottomConstraint(stackView), ]) for view in views { diff --git a/Sources/Features/Preferences/Main/Panels/Folder/FolderPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/Folder/FolderPreferencesPanel.swift index 1a66d01..448e9b5 100644 --- a/Sources/Features/Preferences/Main/Panels/Folder/FolderPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/Folder/FolderPreferencesPanel.swift @@ -17,6 +17,11 @@ class FolderPreferencesPanel: NSView, PreferencesPanelDataSource { setupViews() } + @available(*, unavailable, message: "use init(frame:)") + required init?(coder _: NSCoder) { + nil + } + private func setupViews() { let views = [ FolderComparisonBox(title: NSLocalizedString("Comparison and Display Defaults for New Folder Documents", comment: "")), @@ -33,6 +38,7 @@ class FolderPreferencesPanel: NSView, PreferencesPanelDataSource { stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), stackView.topAnchor.constraint(equalTo: topAnchor), + panelBottomConstraint(stackView), ]) for view in views { @@ -42,11 +48,6 @@ class FolderPreferencesPanel: NSView, PreferencesPanelDataSource { } } - @available(*, unavailable) - required init(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - func reloadData() { guard let stackView = subviews[0] as? NSStackView else { return diff --git a/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift index 8abbb85..1756059 100644 --- a/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/Font/FontPreferencesPanel.swift @@ -27,9 +27,9 @@ class FontPreferencesPanel: NSView, NSFontChanging, PreferencesPanelDataSource { 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() { @@ -54,6 +54,7 @@ class FontPreferencesPanel: NSView, NSFontChanging, PreferencesPanelDataSource { restoreDefaults.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), restoreDefaults.topAnchor.constraint(equalTo: fileFontBox.bottomAnchor, constant: 5), + panelBottomConstraint(restoreDefaults, constant: -5), ]) } diff --git a/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift b/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift index 1281c1c..a8be79c 100644 --- a/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift +++ b/Sources/Features/Preferences/Main/Panels/General/AppearanceBox.swift @@ -47,7 +47,8 @@ class AppearanceBox: PreferencesBox { NSLayoutConstraint.activate([ appearancePopup.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5), appearancePopup.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), - appearancePopup.widthAnchor.constraint(greaterThanOrEqualToConstant: 140), + popupMinWidthConstraint(appearancePopup), + appearancePopup.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -5), ]) } diff --git a/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift b/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift index 3a4b1fe..9e20144 100644 --- a/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift +++ b/Sources/Features/Preferences/Main/Panels/General/FolderComparisonBox.swift @@ -158,7 +158,8 @@ class FolderComparisonBox: PreferencesBox { makeFirstResponder: Bool ) { let supportTolerance = ComparatorOptions(rawValue: comparatorOptions).contains(.timestamp) - timeToleranceView.isHidden = !supportTolerance + // hiding the row made the stack view reclaim its space, changing the height with no resize + timeToleranceView.isEnabled = supportTolerance if supportTolerance, makeFirstResponder { timeToleranceView.becomeFirstResponder() diff --git a/Sources/Features/Preferences/Main/Panels/General/GeneralPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/General/GeneralPreferencesPanel.swift index 19fc15e..5068e53 100644 --- a/Sources/Features/Preferences/Main/Panels/General/GeneralPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/General/GeneralPreferencesPanel.swift @@ -22,9 +22,9 @@ class GeneralPreferencesPanel: NSView, PreferencesPanelDataSource { 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() { @@ -47,6 +47,7 @@ class GeneralPreferencesPanel: NSView, PreferencesPanelDataSource { stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), stackView.topAnchor.constraint(equalTo: topAnchor, constant: 5), + panelBottomConstraint(stackView, constant: -5), appearanceBox.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), appearanceBox.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), diff --git a/Sources/Features/Preferences/Main/Panels/General/PreferredEditorBox.swift b/Sources/Features/Preferences/Main/Panels/General/PreferredEditorBox.swift index 5779e8f..5c556c3 100644 --- a/Sources/Features/Preferences/Main/Panels/General/PreferredEditorBox.swift +++ b/Sources/Features/Preferences/Main/Panels/General/PreferredEditorBox.swift @@ -66,6 +66,9 @@ class PreferredEditorBox: PreferencesBox { editorPopup.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 5), editorPopup.trailingAnchor.constraint(equalTo: removeButton.leadingAnchor, constant: -5), editorPopup.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), + // the popup has a low compression resistance so it can fill the box, without a + // floor the box declares no width at all and the panel reports less than it needs + popupMinWidthConstraint(editorPopup), removeButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), removeButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 5), diff --git a/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardPreferencesPanel.swift index 78c8633..c0e1d9a 100644 --- a/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/Keyboard/KeyboardPreferencesPanel.swift @@ -17,9 +17,9 @@ class KeyboardPreferencesPanel: NSView, PreferencesPanelDataSource { 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() { @@ -33,6 +33,7 @@ class KeyboardPreferencesPanel: NSView, PreferencesPanelDataSource { documentBox.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), documentBox.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), documentBox.topAnchor.constraint(equalTo: topAnchor, constant: 5), + panelBottomConstraint(documentBox, constant: -5), ]) } diff --git a/Sources/Features/Preferences/Main/Panels/Text/TextPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/Text/TextPreferencesPanel.swift index f43aa13..0797ae7 100644 --- a/Sources/Features/Preferences/Main/Panels/Text/TextPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/Text/TextPreferencesPanel.swift @@ -27,9 +27,9 @@ class TextPreferencesPanel: NSView, PreferencesPanelDataSource { 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() { @@ -47,6 +47,7 @@ class TextPreferencesPanel: NSView, PreferencesPanelDataSource { stackView.topAnchor.constraint(equalTo: topAnchor, constant: 5), stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), stackView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), + panelBottomConstraint(stackView, constant: -5), ]) for view in stackView.views { diff --git a/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift b/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift index aa530b1..55c6bde 100644 --- a/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift +++ b/Sources/Features/Preferences/Main/Panels/TrustedPaths/TrustedPathsPreferencesPanel.swift @@ -161,7 +161,7 @@ class TrustedPathsPreferencesPanel: NSView, NSTableViewDataSource, NSTableViewDe pathTableScrollView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), pathTableScrollView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5), pathTableScrollView.topAnchor.constraint(equalTo: subTitle.bottomAnchor, constant: 2), - pathTableScrollView.heightAnchor.constraint(equalToConstant: 200), + pathTableScrollView.heightAnchor.constraint(greaterThanOrEqualToConstant: 200), pathTableScrollView.bottomAnchor.constraint(equalTo: actionBar.topAnchor, constant: -2), actionBar.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5), diff --git a/Sources/Features/Preferences/PreferencesPanelDataSource.swift b/Sources/Features/Preferences/PreferencesPanelDataSource.swift index 1485260..265bb7a 100644 --- a/Sources/Features/Preferences/PreferencesPanelDataSource.swift +++ b/Sources/Features/Preferences/PreferencesPanelDataSource.swift @@ -10,3 +10,15 @@ protocol PreferencesPanelDataSource { @MainActor func reloadData() } + +extension PreferencesPanelDataSource where Self: NSView { + // defaultHigh because NSTabView lays the panel out at 20 points while it is still detached, + // there the content cannot fit and a required constraint would break the box heights + @MainActor + func panelBottomConstraint(_ view: NSView, constant: CGFloat = 0) -> NSLayoutConstraint { + let constraint = view.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: constant) + constraint.priority = .defaultHigh + + return constraint + } +} diff --git a/Sources/Legacy/DiffEngine/UDiffScriptBuilder.h b/Sources/Legacy/DiffEngine/UDiffScriptBuilder.h deleted file mode 100644 index 64e8bc5..0000000 --- a/Sources/Legacy/DiffEngine/UDiffScriptBuilder.h +++ /dev/null @@ -1,59 +0,0 @@ -// -// ScriptBuilder.h -// VisualDiffer -// -// Created by davide ficano on 23/05/12. -// Copyright (c) 2012 visualdiffer.com -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -/** The result of comparison is an "edit script": a chain of change objects. - Each change represents one place where some lines are deleted - and some are inserted. - - LINE0 and LINE1 are the first affected lines in the two files (origin 0). - DELETED is the number of lines deleted here from file 0. - INSERTED is the number of lines inserted here in file 1. - - If DELETED is 0 then LINE0 is the number of the line before - which the insertion was done; vice versa for INSERTED and LINE1. */ - -@interface UDiffChange : NSObject -/** Previous or next edit command. */ -@property (strong, nullable) UDiffChange* link; -/** Line number of 1st deleted line. */ -@property (assign) int line0; -/** Line number of 1st inserted line. */ -@property (assign) int line1; -/** # lines of file 1 changed here. */ -@property (assign) int inserted; -/** # lines of file 0 changed here. */ -@property (assign) int deleted; - -+ (instancetype)changeWithRange:(int)line0 line1:(int)line1 deleted:(int)deleted inserted:(int)inserted old:(nullable UDiffChange*)old; -- (instancetype)initWithRange:(int)line0 line1:(int)line1 deleted:(int)deleted inserted:(int)inserted old:(nullable UDiffChange*)old; -@end - - -@interface UDiffScriptBuilder : NSObject -/** Scan the tables of which lines are inserted and deleted, - producing an edit script. - @param changed0 true for lines in first file which do not match 2nd - @param len0 number of lines in first file - @param changed1 true for lines in 2nd file which do not match 1st - @param len1 number of lines in 2nd file - @return a linked list of changes - or null - */ -- (nullable UDiffChange*)build_script:(BOOL*)changed0 len0:(NSUInteger)len0 changed1:(BOOL*)changed1 len1:(NSUInteger)len1; -@end - -@interface UDiffReverseScript : UDiffScriptBuilder -@end; - -@interface UDiffForwardScript : UDiffScriptBuilder -@end; - -NS_ASSUME_NONNULL_END diff --git a/Sources/Legacy/DiffEngine/UDiffScriptBuilder.m b/Sources/Legacy/DiffEngine/UDiffScriptBuilder.m deleted file mode 100644 index fd77b3d..0000000 --- a/Sources/Legacy/DiffEngine/UDiffScriptBuilder.m +++ /dev/null @@ -1,104 +0,0 @@ -// -// ScriptBuilder.m -// VisualDiffer -// -// Created by davide ficano on 23/05/12. -// Copyright (c) 2012 visualdiffer.com -// - -#import "UDiffScriptBuilder.h" - -@implementation UDiffChange - -+ (instancetype)changeWithRange:(int)pline0 line1:(int)pline1 deleted:(int)pdeleted inserted:(int)pinserted old:(UDiffChange*)pold { - return [[self alloc] initWithRange:pline0 line1:pline1 deleted:pdeleted inserted:pinserted old:pold]; -} - -/** Cons an additional entry onto the front of an edit script OLD. - LINE0 and LINE1 are the first affected lines in the two files (origin 0). - DELETED is the number of lines deleted here from file 0. - INSERTED is the number of lines inserted here in file 1. - - If DELETED is 0 then LINE0 is the number of the line before - which the insertion was done; vice versa for INSERTED and LINE1. */ -- (instancetype)initWithRange:(int)pline0 line1:(int)pline1 deleted:(int)pdeleted inserted:(int)pinserted old:(UDiffChange*)pold { - self = [super init]; - - if (self) { - _line0 = pline0; - _line1 = pline1; - _inserted = pinserted; - _deleted = pdeleted; - _link = pold; - } - - return self; -} - -- (NSString *)description { - return [NSString stringWithFormat:@"%d, %d inserted %d, deleted %d", self.line0 + 1, self.line1 + 1, self.inserted, self.deleted]; -} - -@end - -@implementation UDiffScriptBuilder - -- (UDiffChange*)build_script:(BOOL*)changed0 len0:(NSUInteger)len0 changed1:(BOOL*)changed1 len1:(NSUInteger)len1 { - return nil; -} - -@end - -/** Scan the tables of which lines are inserted and deleted, - producing an edit script in reverse order. */ - -@implementation UDiffReverseScript -- (UDiffChange*)build_script:(BOOL*)changed0 len0:(NSUInteger)len0 changed1:(BOOL*)changed1 len1:(NSUInteger)len1 { - UDiffChange* script = nil; - int i0 = 0, i1 = 0; - while (i0 < (NSInteger)len0 || i1 < (NSInteger)len1) { - if (changed0[1+i0] || changed1[1+i1]) { - int line0 = i0, line1 = i1; - - /* Find # lines changed here in each file. */ - while (changed0[1+i0]) ++i0; - while (changed1[1+i1]) ++i1; - - /* Record this change. */ - script = [UDiffChange changeWithRange:line0 line1:line1 deleted:i0 - line0 inserted:i1 - line1 old:script]; - } - - /* We have reached lines in the two files that match each other. */ - i0++; i1++; - } - - return script; -} -@end; - -@implementation UDiffForwardScript -- (UDiffChange*)build_script:(BOOL*)changed0 len0:(NSUInteger)len0 changed1:(BOOL*)changed1 len1:(NSUInteger)len1 { - UDiffChange* script = nil; - int i0 = (int)len0, i1 = (int)len1; - - while (i0 >= 0 || i1 >= 0) - { - if (changed0[i0] || changed1[i1]) - { - int line0 = i0, line1 = i1; - - /* Find # lines changed here in each file. */ - while (changed0[i0]) --i0; - while (changed1[i1]) --i1; - - /* Record this change. */ - script = [UDiffChange changeWithRange:i0 line1:i1 deleted:line0 - i0 inserted:line1 - i1 old:script]; - } - - /* We have reached lines in the two files that match each other. */ - i0--; i1--; - } - - return script; -} -@end; diff --git a/Sources/Legacy/DiffEngine/UnifiedDiff.h b/Sources/Legacy/DiffEngine/UnifiedDiff.h deleted file mode 100644 index dbebf01..0000000 --- a/Sources/Legacy/DiffEngine/UnifiedDiff.h +++ /dev/null @@ -1,79 +0,0 @@ -// -// UnifiedDiff.h -// VisualDiffer -// -// Created by davide ficano on 23/05/12. -// Copyright (c) 2012 visualdiffer.com -// - -#import - -@class UDiffScriptBuilder; -@class UDiffChange; - -NS_ASSUME_NONNULL_BEGIN - -@interface UDiffFileData : NSObject { - /** Vector, indexed by line number, containing an equivalence code for - each line. It is this vector that is actually compared with that - of another file to generate differences. */ - int* equivs; - - int* pequiv_max; - BOOL* pno_discards; -} - -/** Number of elements (lines) in this file. */ -@property (assign) NSUInteger buffered_lines; -/** Array, indexed by real origin-1 line number, containing true for a line that is an insertion or a deletion. */ -@property (assign, nullable) BOOL* changed_flag; -/** Vector, like the previous one except that the elements for discarded lines have been squeezed out. */ -@property (assign) int* undiscarded; -/** Vector mapping virtual line numbers (not counting discarded lines) to real ones (counting those lines). Both are origin-0. */ -@property (assign) int* realindexes; -/** Total number of nondiscarded lines. */ -@property (assign) int nondiscarded_lines; - -+ (instancetype)fileData:(NSArray*)data h:(NSMutableDictionary*)h equivMax:(int*)ppequiv_max noDiscards:(BOOL*)ppnoDiscards - stringifier:(NSString* (^)(id obj))block; -- (instancetype)initWithData:(NSArray*)data h:(NSMutableDictionary*)h equivMax:(int*)ppequiv_max noDiscards:(BOOL*)ppnoDiscards - stringifier:(NSString* (^)(id obj))block; - -@end - -@interface UnifiedDiff : NSObject { -@private - /** 1 more than the maximum equivalence value used for this or its - sibling file. */ - int equiv_max; - - /** When set to true, the comparison uses a heuristic to speed it up. - With this heuristic, for files with a constant small density - of changes, the algorithm is linear in the file size. */ - BOOL heuristic; - - /** When set to true, the algorithm returns a guarranteed minimal - set of changes. This makes things slower, sometimes much slower. */ - BOOL no_discards; - - int* xvec, *yvec; /* Vectors being compared. */ - int* fdiag; /* Vector, indexed by diagonal, containing - the X coordinate of the point furthest - along the given diagonal in the forward - search of the edit matrix. */ - int* bdiag; /* Vector, indexed by diagonal, containing - the X coordinate of the point furthest - along the given diagonal in the backward - search of the edit matrix. */ - int fdiagoff, bdiagoff; - int cost; - - BOOL inhibit; -} - -- (instancetype)initWithOriginalLines:(NSArray*)a revisedLines:(NSArray*)b stringifier:(NSString* (^)(id obj))block; -- (nullable UDiffChange*)diff_2:(BOOL)reverse; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Legacy/DiffEngine/UnifiedDiff.m b/Sources/Legacy/DiffEngine/UnifiedDiff.m deleted file mode 100644 index 38ba620..0000000 --- a/Sources/Legacy/DiffEngine/UnifiedDiff.m +++ /dev/null @@ -1,739 +0,0 @@ -// -// UnifiedDiff.m -// VisualDiffer -// -// Created by davide ficano on 23/05/12. -// Copyright (c) 2012 visualdiffer.com -// - -#import "UnifiedDiff.h" -#import "UDiffScriptBuilder.h" - -/** Snakes bigger than this are considered "big". */ -static const NSInteger SNAKE_LIMIT = 20; - -static UDiffScriptBuilder* forwardScript; -static UDiffScriptBuilder* reverseScript; - -@interface UDiffFileData() -- (char*)discardable:(int*)counts NS_RETURNS_INNER_POINTER; -- (void)filterDiscards:(char*)discards; -- (void)discard:(char*)discards; -@end - -@interface UnifiedDiff() -- (UDiffChange*)diff:(UDiffScriptBuilder*)bld; -@end - -@implementation UDiffFileData - -+ (instancetype)fileData:(NSArray*)data h:(NSMutableDictionary*)h equivMax:(int*)ppequiv_max noDiscards:(BOOL*)ppno_discards - stringifier:(NSString* (^)(id obj))block { - return [[self alloc] initWithData:data h:h equivMax:ppequiv_max noDiscards:ppno_discards stringifier:block]; -} - -- (instancetype)initWithData:(NSArray*)data h:(NSMutableDictionary*)h equivMax:(int*)ppequiv_max noDiscards:(BOOL*)ppno_discards - stringifier:(NSString* (^)(id obj))block { - self = [super init]; - - if (self) { - pequiv_max = ppequiv_max; - pno_discards = ppno_discards; - _buffered_lines = data.count; - - equivs = calloc(_buffered_lines, sizeof(int)); - _undiscarded = calloc(_buffered_lines, sizeof(int)); - _realindexes = calloc(_buffered_lines, sizeof(int)); - _changed_flag = NULL; - - int i = 0; - for (id o in data) { - NSString* str = block(o); - NSNumber* ir = h[str]; - if (ir == nil) { - h[str] = @(equivs[i] = *pequiv_max); - (*pequiv_max)++; - } else { - equivs[i] = [ir intValue]; - } - ++i; - } - } - - return self; -} - -- (void)dealloc { - free(equivs); - free(_undiscarded); - free(_realindexes); - free(_changed_flag); - -} - -/** Allocate changed array for the results of comparison. */ -- (void)clear { - /* Allocate a flag for each line of each file, saying whether that line - is an insertion or deletion. - Allocate an extra element, always zero, at each end of each vector. - */ - _changed_flag = calloc(_buffered_lines + 2, sizeof(BOOL)); -} - -/** Return equiv_count[I] as the number of lines in this file - that fall in equivalence class I. - @return the array of equivalence class counts. - */ -- (int*)equivCount { - int equiv_max = *pequiv_max; - int* equiv_count = calloc(equiv_max, sizeof(int)); - for (int i = 0; i < (NSInteger)_buffered_lines; ++i) - ++equiv_count[equivs[i]]; - return equiv_count; -} - -/** Discard lines that have no matches in another file. - - A line which is discarded will not be considered by the actual - comparison algorithm; it will be as if that line were not in the file. - The file's `realindexes' table maps virtual line numbers - (which don't count the discarded lines) into real line numbers; - this is how the actual comparison algorithm produces results - that are comprehensible when the discarded lines are counted. -

- When we discard a line, we also mark it as a deletion or insertion - so that it will be printed in the output. - @param f the other file - */ -- (void)discard_confusing_lines:(UDiffFileData*)f { - [self clear]; - /* Set up table of which lines are going to be discarded. */ - int* tempEquivCount = [f equivCount]; - char* discarded = [self discardable:tempEquivCount]; - - /* Don't really discard the provisional lines except when they occur - in a run of discardables, with nonprovisionals at the beginning - and end. */ - [self filterDiscards:discarded]; - - /* Actually discard the lines. */ - [self discard:discarded]; - - free(discarded); - free(tempEquivCount); -} - -/** Mark to be discarded each line that matches no line of another file. - If a line matches many lines, mark it as provisionally discardable. - @see equivCount() - @param counts The count of each equivalence number for the other file. - @return 0=nondiscardable, 1=discardable or 2=provisionally discardable - for each line - */ - -- (char*)discardable:(int*)counts { - NSInteger end = (NSInteger)_buffered_lines; - char* discards = calloc(end, sizeof(char)); - int many = 5; - NSUInteger tem = end / 64; - - /* Multiply MANY by approximate square root of number of lines. - That is the threshold for provisionally discardable lines. */ - while ((tem = tem >> 2) > 0) - many *= 2; - - for (int i = 0; i < end; i++) - { - int nmatch; - if (equivs[i] == 0) - continue; - nmatch = counts[equivs[i]]; - if (nmatch == 0) - discards[i] = 1; - else if (nmatch > many) - discards[i] = 2; - } - return discards; -} - -/** Don't really discard the provisional lines except when they occur - in a run of discardables, with nonprovisionals at the beginning - and end. */ - -- (void)filterDiscards:(char*)discards { - NSInteger end = (NSInteger)_buffered_lines; - - for (int i = 0; i < end; i++) - { - /* Cancel provisional discards not in middle of run of discards. */ - if (discards[i] == 2) - discards[i] = 0; - else if (discards[i] != 0) - { - /* We have found a nonprovisional discard. */ - int j; - int length; - int provisional = 0; - - /* Find end of this run of discardable lines. - Count how many are provisionally discardable. */ - for (j = i; j < end; j++) - { - if (discards[j] == 0) - break; - if (discards[j] == 2) - ++provisional; - } - - /* Cancel provisional discards at end, and shrink the run. */ - while (j > i && discards[j - 1] == 2) { - discards[--j] = 0; --provisional; - } - - /* Now we have the length of a run of discardable lines - whose first and last are not provisional. */ - length = j - i; - - /* If 1/4 of the lines in the run are provisional, - cancel discarding of all provisional lines in the run. */ - if (provisional * 4 > length) - { - while (j > i) - if (discards[--j] == 2) - discards[j] = 0; - } - else - { - int consec; - int minimum = 1; - int tem = length / 4; - - /* MINIMUM is approximate square root of LENGTH/4. - A subrun of two or more provisionals can stand - when LENGTH is at least 16. - A subrun of 4 or more can stand when LENGTH >= 64. */ - while ((tem = tem >> 2) > 0) - minimum *= 2; - minimum++; - - /* Cancel any subrun of MINIMUM or more provisionals - within the larger run. */ - for (j = 0, consec = 0; j < length; j++) - if (discards[i + j] != 2) - consec = 0; - else if (minimum == ++consec) - /* Back up to start of subrun, to cancel it all. */ - j -= consec; - else if (minimum < consec) - discards[i + j] = 0; - - /* Scan from beginning of run - until we find 3 or more nonprovisionals in a row - or until the first nonprovisional at least 8 lines in. - Until that point, cancel any provisionals. */ - for (j = 0, consec = 0; j < length; j++) - { - if (j >= 8 && discards[i + j] == 1) - break; - if (discards[i + j] == 2) { - consec = 0; discards[i + j] = 0; - } - else if (discards[i + j] == 0) - consec = 0; - else - consec++; - if (consec == 3) - break; - } - - /* I advances to the last line of the run. */ - i += length - 1; - - /* Same thing, from end. */ - for (j = 0, consec = 0; j < length; j++) - { - if (j >= 8 && discards[i - j] == 1) - break; - if (discards[i - j] == 2) { - consec = 0; discards[i - j] = 0; - } - else if (discards[i - j] == 0) - consec = 0; - else - consec++; - if (consec == 3) - break; - } - } - } - } -} - -/** Actually discard the lines. - @param discards flags lines to be discarded - */ -- (void)discard:(char*)discards { - NSInteger end = (NSInteger)_buffered_lines; - int j = 0; - for (int i = 0; i < end; ++i) - if (*pno_discards || discards[i] == 0) - { - _undiscarded[j] = equivs[i]; - _realindexes[j++] = i; - } - else - _changed_flag[1+i] = true; - _nondiscarded_lines = j; -} - -/** Adjust inserts/deletes of blank lines to join changes - as much as possible. - - We do something when a run of changed lines include a blank - line at one end and have an excluded blank line at the other. - We are free to choose which blank line is included. - `compareseq' always chooses the one at the beginning, - but usually it is cleaner to consider the following blank line - to be the "change". The only exception is if the preceding blank line - would join this change to other changes. - @param f the file being compared against - */ - -- (void)shift_boundaries:(UDiffFileData*)f { - BOOL* changed = _changed_flag; - BOOL* other_changed = f.changed_flag; - int i = 0; - int j = 0; - NSInteger i_end = (NSInteger)_buffered_lines; - int preceding = -1; - int other_preceding = -1; - - for (;;) - { - int start, end, other_start; - - /* Scan forwards to find beginning of another run of changes. - Also keep track of the corresponding point in the other file. */ - - while (i < i_end && !changed[1+i]) - { - while (other_changed[1+j++]) - /* Non-corresponding lines in the other file - will count as the preceding batch of changes. */ - other_preceding = j; - i++; - } - - if (i == i_end) - break; - - start = i; - other_start = j; - - for (;;) - { - /* Now find the end of this run of changes. */ - - while (i < i_end && changed[1+i]) i++; - end = i; - - /* If the first changed line matches the following unchanged one, - and this run does not follow right after a previous run, - and there are no lines deleted from the other file here, - then classify the first changed line as unchanged - and the following line as changed in its place. */ - - /* You might ask, how could this run follow right after another? - Only because the previous run was shifted here. */ - - if (end != i_end - && equivs[start] == equivs[end] - && !other_changed[1+j] - && end != i_end - && !((preceding >= 0 && start == preceding) - || (other_preceding >= 0 - && other_start == other_preceding))) - { - changed[1+end++] = true; - changed[1+start++] = false; - ++i; - /* Since one line-that-matches is now before this run - instead of after, we must advance in the other file - to keep in synch. */ - ++j; - } - else - break; - } - - preceding = i; - other_preceding = j; - } -} -@end - -#pragma mark - -#pragma mark UnifiedDiff methods - -@interface UnifiedDiff() -@property (strong) UDiffFileData* filevec0; -@property (strong) UDiffFileData* filevec1; -@end - -@implementation UnifiedDiff - -/** Prepare to find differences between two arrays. Each element of - the arrays is translated to an "equivalence number" based on - the result of equals. The original Object arrays - are no longer needed for computing the differences. They will - be needed again later to print the results of the comparison as - an edit script, if desired. - */ -- (instancetype)initWithOriginalLines:(NSArray*)a revisedLines:(NSArray*)b stringifier:(NSString* (^)(id obj))block { - static dispatch_once_t pred; - dispatch_once(&pred, ^{ - forwardScript = [[UDiffForwardScript alloc] init]; - reverseScript = [[UDiffReverseScript alloc] init]; - }); - self = [super init]; - - if (self) { - equiv_max = 1; - heuristic = NO; - no_discards = NO; - inhibit = NO; - NSMutableDictionary* h = [NSMutableDictionary dictionaryWithCapacity:a.count + b.count]; - _filevec0 = [UDiffFileData fileData:a h:h equivMax:&equiv_max noDiscards:&no_discards stringifier:block]; - _filevec1 = [UDiffFileData fileData:b h:h equivMax:&equiv_max noDiscards:&no_discards stringifier:block]; - } - - return self; -} - -/** Find the midpoint of the shortest edit script for a specified - portion of the two files. - - We scan from the beginnings of the files, and simultaneously from the ends, - doing a breadth-first search through the space of edit-sequence. - When the two searches meet, we have found the midpoint of the shortest - edit sequence. - - The value returned is the number of the diagonal on which the midpoint lies. - The diagonal number equals the number of inserted lines minus the number - of deleted lines (counting only lines before the midpoint). - The edit cost is stored into COST; this is the total number of - lines inserted or deleted (counting only lines before the midpoint). - - This function assumes that the first lines of the specified portions - of the two files do not match, and likewise that the last lines do not - match. The caller must trim matching lines from the beginning and end - of the portions it is going to specify. - - Note that if we return the "wrong" diagonal value, or if - the value of bdiag at that diagonal is "wrong", - the worst this can do is cause suboptimal diff output. - It cannot cause incorrect diff output. */ - -- (int)diag:(int)xoff xlim:(int)xlim yoff:(int)yoff ylim:(int)ylim { - int* fd = fdiag; // Give the compiler a chance. - int* bd = bdiag; // Additional help for the compiler. - int* xv = xvec; // Still more help for the compiler. - int* yv = yvec; // And more and more . . . - int dmin = xoff - ylim; // Minimum valid diagonal. - int dmax = xlim - yoff; // Maximum valid diagonal. - int fmid = xoff - yoff; // Center diagonal of top-down search. - int bmid = xlim - ylim; // Center diagonal of bottom-up search. - int fmin = fmid, fmax = fmid; // Limits of top-down search. - int bmin = bmid, bmax = bmid; // Limits of bottom-up search. - /* True if southeast corner is on an odd - diagonal with respect to the northwest. */ - BOOL odd = (fmid - bmid & 1) != 0; - - fd[fdiagoff + fmid] = xoff; - bd[bdiagoff + bmid] = xlim; - - for (int c = 1;; ++c) - { - int d; /* Active diagonal. */ - BOOL big_snake = NO; - - /* Extend the top-down search by an edit step in each diagonal. */ - if (fmin > dmin) - fd[fdiagoff + --fmin - 1] = -1; - else - ++fmin; - if (fmax < dmax) - fd[fdiagoff + ++fmax + 1] = -1; - else - --fmax; - for (d = fmax; d >= fmin; d -= 2) - { - int x, y, oldx, tlo = fd[fdiagoff + d - 1], thi = fd[fdiagoff + d + 1]; - - if (tlo >= thi) - x = tlo + 1; - else - x = thi; - oldx = x; - y = x - d; - while (x < xlim && y < ylim && xv[x] == yv[y]) { - ++x; ++y; - } - if (x - oldx > SNAKE_LIMIT) - big_snake = true; - fd[fdiagoff + d] = x; - if (odd && bmin <= d && d <= bmax && bd[bdiagoff + d] <= fd[fdiagoff + d]) - { - cost = 2 * c - 1; - return d; - } - } - - /* Similar extend the bottom-up search. */ - if (bmin > dmin) - bd[bdiagoff + --bmin - 1] = INT_MAX; - else - ++bmin; - if (bmax < dmax) - bd[bdiagoff + ++bmax + 1] = INT_MAX; - else - --bmax; - for (d = bmax; d >= bmin; d -= 2) - { - int x, y, oldx, tlo = bd[bdiagoff + d - 1], thi = bd[bdiagoff + d + 1]; - - if (tlo < thi) - x = tlo; - else - x = thi - 1; - oldx = x; - y = x - d; - while (x > xoff && y > yoff && xv[x - 1] == yv[y - 1]) { - --x; --y; - } - if (oldx - x > SNAKE_LIMIT) - big_snake = true; - bd[bdiagoff + d] = x; - if (!odd && fmin <= d && d <= fmax && bd[bdiagoff + d] <= fd[fdiagoff + d]) - { - cost = 2 * c; - return d; - } - } - - /* Heuristic: check occasionally for a diagonal that has made - lots of progress compared with the edit distance. - If we have any such, find the one that has made the most - progress and return it as if it had succeeded. - - With this heuristic, for files with a constant small density - of changes, the algorithm is linear in the file size. */ - - if (c > 200 && big_snake && heuristic) - { - int best = 0; - int bestpos = -1; - - for (d = fmax; d >= fmin; d -= 2) - { - int dd = d - fmid; - int x = fd[fdiagoff + d]; - int y = x - d; - int v = (x - xoff) * 2 - dd; - if (v > 12 * (c + (dd < 0 ? -dd : dd))) - { - if (v > best - && xoff + SNAKE_LIMIT <= x && x < xlim - && yoff + SNAKE_LIMIT <= y && y < ylim) - { - /* We have a good enough best diagonal; - now insist that it end with a significant snake. */ - int k; - - for (k = 1; xvec[x - k] == yvec[y - k]; k++) - if (k == SNAKE_LIMIT) - { - best = v; - bestpos = d; - break; - } - } - } - } - if (best > 0) - { - cost = 2 * c - 1; - return bestpos; - } - - best = 0; - for (d = bmax; d >= bmin; d -= 2) - { - int dd = d - bmid; - int x = bd[bdiagoff + d]; - int y = x - d; - int v = (xlim - x) * 2 + dd; - if (v > 12 * (c + (dd < 0 ? -dd : dd))) - { - if (v > best - && xoff < x && x <= xlim - SNAKE_LIMIT - && yoff < y && y <= ylim - SNAKE_LIMIT) - { - /* We have a good enough best diagonal; - now insist that it end with a significant snake. */ - int k; - - for (k = 0; xvec[x + k] == yvec[y + k]; k++) - if (k == SNAKE_LIMIT) - { - best = v; - bestpos = d; - break; - } - } - } - } - if (best > 0) - { - cost = 2 * c - 1; - return bestpos; - } - } - } -} - -/** Compare in detail contiguous subsequences of the two files - which are known, as a whole, to match each other. - - The results are recorded in the vectors filevec[N].changed_flag, by - storing a 1 in the element for each line that is an insertion or deletion. - - The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1. - - Note that XLIM, YLIM are exclusive bounds. - All line numbers are origin-0 and discarded lines are not counted. */ - -- (void)compareseq:(int)xoff xlim:(int)xlim yoff:(int)yoff ylim:(int)ylim { - /* Slide down the bottom initial diagonal. */ - while (xoff < xlim && yoff < ylim && xvec[xoff] == yvec[yoff]) { - ++xoff; ++yoff; - } - /* Slide up the top initial diagonal. */ - while (xlim > xoff && ylim > yoff && xvec[xlim - 1] == yvec[ylim - 1]) { - --xlim; --ylim; - } - - /* Handle simple cases. */ - if (xoff == xlim) - while (yoff < ylim) - self.filevec1.changed_flag[1 + self.filevec1.realindexes[yoff++]] = YES; - else if (yoff == ylim) - while (xoff < xlim) - self.filevec0.changed_flag[1 + self.filevec0.realindexes[xoff++]] = YES; - else - { - /* Find a point of correspondence in the middle of the files. */ - - int d = [self diag:xoff xlim:xlim yoff:yoff ylim:ylim]; - int c = cost; -// int f = fdiag[fdiagoff + d]; - int b = bdiag[bdiagoff + d]; - - if (c == 1) - { - /* This should be impossible, because it implies that - one of the two subsequences is empty, - and that case was handled above without calling `diag'. - Let's verify that this is true. */ -// /throw new IllegalArgumentException("Empty subsequence"); - } - else - { - /* Use that point to split this problem into two subproblems. */ - [self compareseq:xoff xlim:b yoff:yoff ylim:b - d]; - /* This used to use f instead of b, - but that is incorrect! - It is not necessarily the case that diagonal d - has a snake from b to f. */ - [self compareseq:b xlim:xlim yoff:b - d ylim:ylim]; - } - } -} - -/** Discard lines from one file that have no matches in the other file. - */ - -- (void)discard_confusing_lines { - [self.filevec0 discard_confusing_lines:self.filevec1]; - [self.filevec1 discard_confusing_lines:self.filevec0]; -} - -/** Adjust inserts/deletes of blank lines to join changes - as much as possible. - */ - -- (void)shift_boundaries { - if (inhibit) - return; - [self.filevec0 shift_boundaries:self.filevec1]; - [self.filevec1 shift_boundaries:self.filevec0]; -} - -/* Report the differences of two files. */ -- (UDiffChange*)diff_2:(BOOL)reverse { - return [self diff:reverse ? reverseScript : forwardScript]; -} - -/** Get the results of comparison as an edit script. The script - is described by a list of changes. The standard ScriptBuilder - implementations provide for forward and reverse edit scripts. - Alternate implementations could, for instance, list common elements - instead of differences. - @param bld an object to build the script from change flags - @return the head of a list of changes - */ -- (UDiffChange*)diff:(UDiffScriptBuilder*)bld { - - /* Some lines are obviously insertions or deletions - because they don't match anything. Detect them now, - and avoid even thinking about them in the main comparison algorithm. */ - - [self discard_confusing_lines]; - - /* Now do the main comparison algorithm, considering just the - undiscarded lines. */ - - xvec = self.filevec0.undiscarded; - yvec = self.filevec1.undiscarded; - - int diags = self.filevec0.nondiscarded_lines + self.filevec1.nondiscarded_lines + 3; - fdiag = calloc(diags, sizeof(int)); - fdiagoff = self.filevec1.nondiscarded_lines + 1; - bdiag = calloc(diags, sizeof(int)); - bdiagoff = self.filevec1.nondiscarded_lines + 1; - - [self compareseq:0 - xlim:self.filevec0.nondiscarded_lines - yoff:0 - ylim:self.filevec1.nondiscarded_lines]; - fdiag = nil; - bdiag = nil; - - /* Modify the results slightly to make them prettier - in cases where that can validly be done. */ - - [self shift_boundaries]; - - /* Get the results of comparison in the form of a chain - of struct change's -- an edit script. */ - return [bld build_script:self.filevec0.changed_flag - len0:self.filevec0.buffered_lines - changed1:self.filevec1.changed_flag - len1:self.filevec1.buffered_lines]; -} - -/** Data on one input file being compared. - */ - - -@end diff --git a/Sources/SharedKit/Extensions/CoreData/NSManagedObjectContext+Helpers.swift b/Sources/SharedKit/Extensions/CoreData/NSManagedObjectContext+Helpers.swift deleted file mode 100644 index 3147db5..0000000 --- a/Sources/SharedKit/Extensions/CoreData/NSManagedObjectContext+Helpers.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// NSManagedObjectContext+Helpers.swift -// VisualDiffer -// -// Created by davide ficano on 10/10/15. -// Copyright (c) 2015 visualdiffer.com -// - -extension NSManagedObjectContext { - /// - /// Wrap the block around disable/enable undo registration pair - /// - /// Parameters: - /// - block: the code to execute with registration disabled - @MainActor - func updateWithoutRecordingModifications(_ block: () -> Void) { - undoManager?.disableUndoRegistration() - - block() - - processPendingChanges() - undoManager?.enableUndoRegistration() - } -} diff --git a/Sources/SharedKit/Extensions/String/String+Highlights.swift b/Sources/SharedKit/Extensions/String/String+Highlights.swift index cfabb45..304e487 100644 --- a/Sources/SharedKit/Extensions/String/String+Highlights.swift +++ b/Sources/SharedKit/Extensions/String/String+Highlights.swift @@ -58,4 +58,26 @@ extension String { highlightStyle: hightlightStyle ) } + + // the offsets must be ascending and non overlapping, the string is walked once, + // the offsets starting past the end are dropped and the others are clamped + func characterRanges(from offsets: [Range]) -> [Range] { + let length = count + var ranges = [Range]() + var start = startIndex + var startOffset = 0 + + for offsetRange in offsets where offsetRange.lowerBound < length { + let clampedUpper = min(offsetRange.upperBound, length) + let lower = index(start, offsetBy: offsetRange.lowerBound - startOffset) + let upper = index(lower, offsetBy: clampedUpper - offsetRange.lowerBound) + + ranges.append(lower ..< upper) + + start = upper + startOffset = clampedUpper + } + + return ranges + } } diff --git a/Sources/SharedKit/Extensions/String/String+RegularExpression.swift b/Sources/SharedKit/Extensions/String/String+RegularExpression.swift index be93ee1..a110a68 100644 --- a/Sources/SharedKit/Extensions/String/String+RegularExpression.swift +++ b/Sources/SharedKit/Extensions/String/String+RegularExpression.swift @@ -8,53 +8,6 @@ import Foundation -private struct GlobConverter { - private var regex = [NSRegularExpression]() - private var templates = [String]() - - init() { - let globs = [ - ("([.^$+(){}\\[\\]\\\\|])", "\\\\$1"), - ("\\?", "(.|[\r\n])"), - ("\\*", "(.|[\r\n])*"), - ] - - for (regexString, templateString) in globs { - do { - let re = try NSRegularExpression( - pattern: regexString, - options: .caseInsensitive - ) - regex.append(re) - templates.append(templateString) - } catch { - preconditionFailure("invalid static regex pattern '\(regexString)': \(error)") - } - } - } - - private func replace(index: Int, in str: String) -> String { - let re = regex[index] - let template = templates[index] - - return re.stringByReplacingMatches( - in: str, - options: [], - range: NSRange(location: 0, length: str.utf16.count), - withTemplate: template - ) - } - - func convert(_ str: String) -> String { - var re = str - - for i in 0 ..< regex.count { - re = replace(index: i, in: re) - } - return re - } -} - public extension String { /** * Replace tagged expressions ($1, $2, ...) present in template @@ -124,21 +77,4 @@ public extension String { } return inString } - - /** - * Convert a glob string to a valid regular expression string - * Strings like "*m" are converted to ".*m" - * Brackets and "?" are correctly escaped - */ - private func _convertGlobMetaCharsToRegexpMetaChars() -> String { - enum Static { - static let converter: GlobConverter = .init() - } - - return Static.converter.convert(self) - } - - func convertGlobMetaCharsToRegexpMetaChars() -> String { - _convertGlobMetaCharsToRegexpMetaChars() - } } diff --git a/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift b/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift index 9c84644..7c72a1b 100644 --- a/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift +++ b/Sources/SharedKit/Extensions/Workspace/NSWorkspace+Finder.swift @@ -6,7 +6,6 @@ // Copyright (c) 2014 visualdiffer.com // -let suppressShowInFinder = "suppressShowInFinder" let maxFoldersToShowWithoutAlert = 8 extension NSWorkspace { @@ -21,7 +20,7 @@ extension NSWorkspace { let confirmOpen = NSAlert.showModalConfirm( messageText: String(format: NSLocalizedString("Are you sure you want to open %lu Finder windows?", comment: ""), paths.count), informativeText: NSLocalizedString("You have chosen to open a large number of windows. This can take a long time", comment: ""), - suppressPropertyName: suppressShowInFinder, + suppressPropertyName: CommonPrefs.Name.confirmShowInFinder.rawValue, yesText: nil, noText: nil ) diff --git a/Sources/SharedKit/Utilities/Image/IconUtils.swift b/Sources/SharedKit/Utilities/Image/IconUtils.swift index 1e3c843..5629ada 100644 --- a/Sources/SharedKit/Utilities/Image/IconUtils.swift +++ b/Sources/SharedKit/Utilities/Image/IconUtils.swift @@ -15,8 +15,15 @@ import UniformTypeIdentifiers public class IconUtils: @unchecked Sendable { static let shared = IconUtils() + // the cached image is resized to the requested size, so the size belongs to the key, + // otherwise the first caller decides the size for every other one + private struct IconKey: Hashable { + let name: String + let size: CGFloat + } + private let lock = NSLock() - private var icons = [String: NSImage]() + private var icons = [IconKey: NSImage]() private init() {} @@ -60,13 +67,9 @@ public class IconUtils: @unchecked Sendable { } public func icon(forEmptyPath size: CGFloat) -> NSImage { - guard let icon = NSImage(systemSymbolName: "square.dashed", accessibilityDescription: "Empty Path") else { - fatalError("Unable to create empty path icon") + cachedIcon(for: VDSymbol.Asset.emptyPath.rawValue, size: size) { + VDSymbol.Asset.emptyPath.image() } - - icon.size = NSSize(width: size, height: size) - - return icon } private func iconNamed(_ name: String, size: CGFloat) -> NSImage { @@ -87,7 +90,9 @@ public class IconUtils: @unchecked Sendable { lock.lock() defer { lock.unlock() } - if let icon = icons[name] { + let key = IconKey(name: name, size: size) + + if let icon = icons[key] { return icon } @@ -102,7 +107,7 @@ public class IconUtils: @unchecked Sendable { } icon.size = NSSize(width: size, height: size) - icons[name] = icon + icons[key] = icon return icon } diff --git a/Sources/SharedKit/Utilities/String/DisplayLine.swift b/Sources/SharedKit/Utilities/String/DisplayLine.swift new file mode 100644 index 0000000..97a3ebf --- /dev/null +++ b/Sources/SharedKit/Utilities/String/DisplayLine.swift @@ -0,0 +1,29 @@ +// +// DisplayLine.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +/// text of a line as it gets drawn, together with the mapping needed to translate +/// the character offsets of the source text into offsets of the drawn text +/// +/// Tab expansion changes the length of a line, so an offset in the source text does +/// not match the same offset in the drawn one. Everything else is replaced one +/// character at a time, therefore a line without tabs needs no mapping at all. +struct DisplayLine { + let text: String + + // one entry per source character, plus the past the end offset, + // empty when the drawn text matches the source text character by character + let offsets: [Int] + + func displayOffset(for sourceOffset: Int) -> Int { + if offsets.isEmpty { + return sourceOffset + } + + return sourceOffset < offsets.count ? offsets[sourceOffset] : text.count + } +} diff --git a/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift b/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift index 983d20f..e036c23 100644 --- a/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift +++ b/Sources/SharedKit/Utilities/String/VisibleWhitespaces.swift @@ -19,49 +19,73 @@ class VisibleWhitespaces: NSObject { return String(ch) } - func getString(_ component: DiffLineComponent, isWhitespacesVisible: Bool) -> String { + func getString(_ component: DiffLineComponent, isWhitespacesVisible: Bool) -> DisplayLine { if isWhitespacesVisible { return showWhitespaces(component.text + component.eol.visibleSymbol) } return Self.tabs2space(component.text, tabWidth: tabWidth) } - static func tabs2space(_ line: String, tabWidth: Int) -> String { - guard tabWidth > 0 else { - return line + static func tabs2space(_ line: String, tabWidth: Int) -> DisplayLine { + guard tabWidth > 0, line.contains("\t") else { + return DisplayLine(text: line, offsets: []) } var dest = "" + var destCount = 0 + var offsets = [Int]() + + offsets.reserveCapacity(line.count + 1) for ch in line { + offsets.append(destCount) + if ch == "\t" { - let spaces = tabWidth - (dest.count % tabWidth) + let spaces = tabWidth - (destCount % tabWidth) dest += String(repeating: " ", count: spaces) + destCount += spaces } else { dest.append(ch) + destCount += 1 } } - return dest + offsets.append(destCount) + + return DisplayLine(text: dest, offsets: offsets) } - func showWhitespaces(_ line: String) -> String { + func showWhitespaces(_ line: String) -> DisplayLine { guard tabWidth > 0 else { - return line + return DisplayLine(text: line, offsets: []) } let whitespaces = CharacterSet.whitespaces + // every character but a tab is replaced one by one, so the mapping is needed + // only when the line contains a tab + let needsOffsets = line.contains("\t") var dest = "" + var destCount = 0 + var offsets = [Int]() + + if needsOffsets { + offsets.reserveCapacity(line.count + 1) + } for ch in line { + if needsOffsets { + offsets.append(destCount) + } + guard let scalar = ch.unicodeScalars.first, whitespaces.contains(scalar) else { dest.append(ch) + destCount += 1 continue } if ch == "\t" { // subtract from spaces the character representing TAB - let spaces = (tabWidth - (dest.count % tabWidth)) - 1 + let spaces = (tabWidth - (destCount % tabWidth)) - 1 if spaces > 0 { let leftSpaces = spaces / 2 let rightSpaces = spaces - leftSpaces @@ -73,13 +97,21 @@ class VisibleWhitespaces: NSObject { if rightSpaces > 0 { dest += String(repeating: " ", count: rightSpaces) } + destCount += spaces + 1 } else { dest += Self.visibleTab + destCount += 1 } } else { dest += getVisibleCharFor(ch) + destCount += 1 } } - return dest + + if needsOffsets { + offsets.append(destCount) + } + + return DisplayLine(text: dest, offsets: offsets) } } diff --git a/Sources/SharedKit/Utilities/TextField/RSVerticallyCenteredTextFieldCell.swift b/Sources/SharedKit/Utilities/TextField/RSVerticallyCenteredTextFieldCell.swift deleted file mode 100644 index eec11dc..0000000 --- a/Sources/SharedKit/Utilities/TextField/RSVerticallyCenteredTextFieldCell.swift +++ /dev/null @@ -1,35 +0,0 @@ -// -// RSVerticallyCenteredTextFieldCell.swift -// VisualDiffer -// -// Created by Daniel Jalkut on 6/17/06. -// Copyright 2006 Red Sweater Software. All rights reserved. -// - -class RSVerticallyCenteredTextFieldCell: NSTextFieldCell { - private var isEditingOrSelecting = false - - override func drawingRect(forBounds rect: NSRect) -> NSRect { - // Get the parent's idea of where we should draw - var newRect = super.drawingRect(forBounds: rect) - - // When the text field is being - // edited or selected, we have to turn off the magic because it screws up - // the configuration of the field editor. We sneak around this by - // intercepting selectWithFrame and editWithFrame and sneaking a - // reduced, centered rect in at the last minute. - if !isEditingOrSelecting { - // Get our ideal size for current text - let textSize = cellSize(forBounds: rect) - - // Center that in the proposed rect - let heightDelta = newRect.size.height - textSize.height - if heightDelta > 0 { - newRect.size.height -= heightDelta - newRect.origin.y += (heightDelta / 2) - } - } - - return newRect - } -} diff --git a/Sources/SharedKit/Utilities/View/ActionBar/ActionBarView.swift b/Sources/SharedKit/Utilities/View/ActionBar/ActionBarView.swift index cb9c0d7..1ee7dd7 100644 --- a/Sources/SharedKit/Utilities/View/ActionBar/ActionBarView.swift +++ b/Sources/SharedKit/Utilities/View/ActionBar/ActionBarView.swift @@ -48,7 +48,6 @@ class ActionBarView: NSStackView { secondButton.widthAnchor.constraint(equalToConstant: 24), secondButton.heightAnchor.constraint(equalToConstant: 24), - popup.widthAnchor.constraint(equalToConstant: 32), popup.heightAnchor.constraint(equalToConstant: 24), ]) } diff --git a/Sources/SharedKit/Utilities/View/Console/ConsoleView.swift b/Sources/SharedKit/Utilities/View/Console/ConsoleView.swift index e04e6bc..bebc8ee 100644 --- a/Sources/SharedKit/Utilities/View/Console/ConsoleView.swift +++ b/Sources/SharedKit/Utilities/View/Console/ConsoleView.swift @@ -6,12 +6,13 @@ // Copyright (c) 2012 visualdiffer.com // +@MainActor protocol ConsoleViewDelegate: AnyObject { func hide(console: ConsoleView) } class ConsoleView: NSView, NSTextViewDelegate { - var delegate: ConsoleViewDelegate? + weak var delegate: ConsoleViewDelegate? private lazy var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() @@ -74,9 +75,9 @@ class ConsoleView: NSView, NSTextViewDelegate { 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() { diff --git a/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift b/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift index 37da868..42b2c82 100644 --- a/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift +++ b/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitView.swift @@ -6,29 +6,40 @@ // Copyright (c) 2025 visualdiffer.com // +// size used until the user resizes the collapsable pane +private let defaultCollapsablePaneSize: CGFloat = 120 + +private let userResizeKey = "NSSplitViewUserResizeKey" + class DualPaneSplitView: NSSplitView { - var isSubviewCollapsed = false + var collapsablePaneSize: CGFloat = defaultCollapsablePaneSize + + // the paired split view is not retained, the view hierarchy keeps it alive + private weak var synchronizedSplitView: DualPaneSplitView? + + // the collapsed state is owned by the app through isHidden because + // NSSplitView.isSubviewCollapsed(_:) reports true also for a pane not laid out yet + var hasSubviewCollapsed: Bool { + subviews.isEmpty || subviews.contains { $0.isHidden } + } - @objc var firstViewSize: CGFloat = 0 + // the collapsable pane is the one the delegate allows to collapse + private var collapsablePaneIndex: Int? { + subviews.firstIndex { delegate?.splitView?(self, canCollapseSubview: $0) == true } + } + + override var dividerThickness: CGFloat { + hasSubviewCollapsed ? 0.0 : super.dividerThickness + } override init(frame frameRect: NSRect) { super.init(frame: frameRect) - NotificationCenter.default.addObserver( - self, - selector: #selector(subviewResized), - name: NSSplitView.didResizeSubviewsNotification, - object: self - ) + observeUserResize() } required init?(coder: NSCoder) { super.init(coder: coder) - NotificationCenter.default.addObserver( - self, - selector: #selector(subviewResized), - name: NSSplitView.didResizeSubviewsNotification, - object: self - ) + observeUserResize() } deinit { @@ -41,80 +52,113 @@ class DualPaneSplitView: NSSplitView { @objc func subviewResized(_ notification: Notification) { - if hasSubviewCollapsed { - return - } - guard let num = notification.userInfo?["NSSplitViewUserResizeKey"] as? NSNumber else { + // only a manual drag defines the size to remember; on a window + // resize NSSplitView scales the panes proportionally and that size is not a user choice + guard let userResize = notification.userInfo?[userResizeKey] as? NSNumber, + userResize.boolValue, + !hasSubviewCollapsed, + let index = collapsablePaneIndex else { return } - let isUserResize = num.boolValue - if isUserResize { - let view = subviews[0] - firstViewSize = isVertical ? view.frame.width : view.frame.height + let currentSize = paneSize(of: subviews[index]) + if currentSize > 0 { + collapsablePaneSize = currentSize + synchronizedSplitView?.matchCollapsablePaneSize(currentSize) } } - @objc var hasSubviewCollapsed: Bool { - if subviews.isEmpty { - return true - } - for view in subviews where isSubviewCollapsed(view) { - return true - } - return false + /** + Pair the split view with another one so they collapse, expand and resize together + self is paired with other and other is paired with self, too + */ + func setSynchronized(splitView: DualPaneSplitView) { + synchronizedSplitView = splitView + splitView.synchronizedSplitView = self } - @objc - func toggleSubview(at index: Int) { + func toggleSubview() { if hasSubviewCollapsed { - expandSubview(at: index) + expandSubview() } else { - collapseSubview(at: index) + collapseSubview() } } - @objc - func collapseSubview(at index: Int) { - if hasSubviewCollapsed { + func collapseSubview() { + guard let index = collapsablePaneIndex else { return } - if index == 0 { - let collapseView = subviews[0] - collapseView.isHidden = true - setPosition(0, ofDividerAt: 0) - } else if index == 1 { - let expandView = subviews[0] - let collapseView = subviews[1] - collapseView.isHidden = true - let position = isVertical ? expandView.frame.width : expandView.frame.height - setPosition(position, ofDividerAt: 0) - } else { + + let collapseView = subviews[index] + + // returning when already collapsed also stops the pair from calling back + if collapseView.isHidden { return } + collapseView.isHidden = true adjustSubviews() + // force a layout pass so AppKit removes the collapsed divider layer + needsLayout = true + synchronizedSplitView?.collapseSubview() } - @objc - func expandSubview(at index: Int) { - if !hasSubviewCollapsed { + func expandSubview() { + guard let index = collapsablePaneIndex else { return } - if index == 0 { - let collapseView = subviews[0] - collapseView.isHidden = false - setPosition(firstViewSize, ofDividerAt: 0) - } else if index == 1 { - let collapseView = subviews[1] - collapseView.isHidden = false - setPosition(firstViewSize, ofDividerAt: 0) - } else { + + let expandView = subviews[index] + + // returning when already expanded also stops the pair from calling back + if !expandView.isHidden { return } + expandView.isHidden = false + moveDivider(collapsableIndex: index) + synchronizedSplitView?.expandSubview() + } + + // applies the size the paired split view got from the user, the comparison + // stops the pair from bouncing the change back + private func matchCollapsablePaneSize(_ size: CGFloat) { + guard !hasSubviewCollapsed, + let index = collapsablePaneIndex, + paneSize(of: subviews[index]) != size else { + return + } + + collapsablePaneSize = size + moveDivider(collapsableIndex: index) + } + + // the position is derived from the current size and kept inside the + // range allowed by the delegate, otherwise NSSplitView collapses the pane again + private func moveDivider(collapsableIndex index: Int) { + let available = paneSize(of: self) + let position = index == 0 ? collapsablePaneSize : available - collapsablePaneSize - dividerThickness + + setPosition(constrain(position: position, available: available), ofDividerAt: 0) adjustSubviews() } - override var dividerThickness: CGFloat { - hasSubviewCollapsed ? 0.0 : super.dividerThickness + private func observeUserResize() { + NotificationCenter.default.addObserver( + self, + selector: #selector(subviewResized), + name: NSSplitView.didResizeSubviewsNotification, + object: self + ) + } + + private func paneSize(of view: NSView) -> CGFloat { + isVertical ? view.frame.width : view.frame.height + } + + private func constrain(position: CGFloat, available: CGFloat) -> CGFloat { + let minPosition = delegate?.splitView?(self, constrainMinCoordinate: 0, ofSubviewAt: 0) ?? 0 + let maxPosition = delegate?.splitView?(self, constrainMaxCoordinate: available, ofSubviewAt: 0) ?? available + + return min(max(position, minPosition), maxPosition) } } diff --git a/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitViewDelegate.swift b/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitViewDelegate.swift index e7172ca..a885c27 100644 --- a/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitViewDelegate.swift +++ b/Sources/SharedKit/Utilities/View/DualPane/DualPaneSplitViewDelegate.swift @@ -7,21 +7,21 @@ // class DualPaneSplitViewDelegate: NSObject, NSSplitViewDelegate { - var minSize: CGFloat = 0 - var maxSize: CGFloat = 0 + var minFirstPaneSize: CGFloat = 0 + var minSecondPaneSize: CGFloat = 0 var collapsableSubviewIndex = 0 @objc init( collapsableSubViewIndex index: Int, - minSize: CGFloat, - maxSize: CGFloat + minFirstPaneSize: CGFloat, + minSecondPaneSize: CGFloat ) { super.init() collapsableSubviewIndex = index - self.minSize = minSize - self.maxSize = maxSize + self.minFirstPaneSize = minFirstPaneSize + self.minSecondPaneSize = minSecondPaneSize } func splitView(_: NSSplitView, shouldHideDividerAt _: Int) -> Bool { @@ -29,11 +29,11 @@ class DualPaneSplitViewDelegate: NSObject, NSSplitViewDelegate { } func splitView(_: NSSplitView, constrainMinCoordinate proposedMinimumPosition: CGFloat, ofSubviewAt _: Int) -> CGFloat { - proposedMinimumPosition + minSize + proposedMinimumPosition + minFirstPaneSize } func splitView(_: NSSplitView, constrainMaxCoordinate proposedMaximumPosition: CGFloat, ofSubviewAt _: Int) -> CGFloat { - proposedMaximumPosition - maxSize + proposedMaximumPosition - minSecondPaneSize } func splitView(_ splitView: NSSplitView, canCollapseSubview subview: NSView) -> Bool { diff --git a/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift b/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift index c61ae55..65468e7 100644 --- a/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift +++ b/Sources/SharedKit/Utilities/View/Encoding/SelectEncodingsPanel.swift @@ -7,25 +7,7 @@ // class SelectEncodingsPanel: NSWindow, NSTableViewDataSource, NSTableViewDelegate { - private lazy var titleText: NSTextField = { - let view = NSTextField(frame: .zero) - - view.isEditable = false - view.isBordered = false - view.drawsBackground = false - view.translatesAutoresizingMaskIntoConstraints = false - - let cell = TextFieldVerticalCentered() - cell.lineBreakMode = .byClipping - cell.title = NSLocalizedString("Select encodings to show on the main list", comment: "") - - view.cell = cell - - // set the font after the cell otherwise it is lost - view.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) - - return view - }() + private let titleText = NSTextField.hintWithTitle(NSLocalizedString("Select encodings to show on the main list", comment: "")) private lazy var scrollView: NSScrollView = { let view = NSScrollView(frame: .zero) diff --git a/Sources/SharedKit/Utilities/View/ProgressBar/ProgressBarView.swift b/Sources/SharedKit/Utilities/View/ProgressBar/ProgressBarView.swift index 577ed82..3d554ec 100644 --- a/Sources/SharedKit/Utilities/View/ProgressBar/ProgressBarView.swift +++ b/Sources/SharedKit/Utilities/View/ProgressBar/ProgressBarView.swift @@ -7,6 +7,9 @@ // class ProgressBarView: NSView { + private static let stopButtonWidth: CGFloat = 16 + private static let stopButtonSpacing: CGFloat = 4 + private lazy var progressIndicator: NSProgressIndicator = { let view = NSProgressIndicator(frame: .zero) @@ -51,16 +54,32 @@ class ProgressBarView: NSView { view.imagePosition = .imageOnly view.imageScaling = .scaleProportionallyDown view.keyEquivalent = KeyEquivalent.escape + view.isHidden = true return view }() + private lazy var stopButtonWidthConstraint = stopButton.widthAnchor.constraint( + equalToConstant: 0 + ) + + private lazy var stopButtonSpacingConstraint = progressIndicator.leadingAnchor.constraint( + equalTo: stopButton.trailingAnchor, + constant: 0 + ) + var waitStopMessage = "" + // a comparison that cannot be interrupted never sets a stop action, the button would + // be inert there so it takes no room either + private var isStopButtonHidden: Bool { + stopButton.target == nil + } + override var isHidden: Bool { didSet { if !isHidden { - stopButton.isEnabled = true + stopButton.isEnabled = !isStopButtonHidden messageText.stringValue = "" setProgress(position: 0, maxValue: 1) } @@ -73,9 +92,9 @@ class ProgressBarView: 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() { @@ -91,9 +110,9 @@ class ProgressBarView: NSView { stopButton.leadingAnchor.constraint(equalTo: leadingAnchor), stopButton.topAnchor.constraint(equalTo: topAnchor), stopButton.bottomAnchor.constraint(equalTo: bottomAnchor), - stopButton.widthAnchor.constraint(equalToConstant: 16), + stopButtonWidthConstraint, - progressIndicator.leadingAnchor.constraint(equalTo: stopButton.trailingAnchor, constant: 4), + stopButtonSpacingConstraint, progressIndicator.topAnchor.constraint(equalTo: topAnchor), progressIndicator.bottomAnchor.constraint(equalTo: bottomAnchor), progressIndicator.widthAnchor.constraint(equalToConstant: 250), @@ -114,8 +133,9 @@ class ProgressBarView: NSView { } func setProgress(position: Double, maxValue: Double) { - progressIndicator.doubleValue = position + // the range must be set before the value or it clamps the value progressIndicator.maxValue = maxValue + progressIndicator.doubleValue = position } func advanceProgress() { @@ -130,5 +150,20 @@ class ProgressBarView: NSView { func setStop(action: Selector, target: AnyObject) { stopButton.target = target stopButton.action = action + + stopButton.isHidden = false + stopButtonWidthConstraint.constant = Self.stopButtonWidth + stopButtonSpacingConstraint.constant = Self.stopButtonSpacing + } + + // for the callers that are about to block the main thread, the next display cycle + // happens only once that work is over + func displayMessage(_ text: String) { + updateMessage(text) + + // a window that is not on screen draws into a surface nobody sees + if window?.isVisible == true { + displayIfNeeded() + } } } diff --git a/Sources/SharedKit/Utilities/Window/WindowCancelOperation.swift b/Sources/SharedKit/Utilities/Window/WindowCancelOperation.swift index 6a3dba5..7f3bc49 100644 --- a/Sources/SharedKit/Utilities/Window/WindowCancelOperation.swift +++ b/Sources/SharedKit/Utilities/Window/WindowCancelOperation.swift @@ -7,6 +7,12 @@ // class WindowCancelOperation: NSWindow { + private static let maxContentSize = NSSize(width: 1100, height: 600) + private static let minWindowSize = NSSize(width: 480, height: 400) + private static let minContentSize = NSSize(width: 300, height: 200) + // a window opened for the first time never covers more than this fraction of the visible screen + private static let maxScreenFillRatio = 0.75 + override func cancelOperation(_ sender: Any?) { // override NSWindow.cancelOperation() instead of NSWindowController.cancelOperation() // because on NSWindowController the standard beep is played @@ -34,7 +40,7 @@ class WindowCancelOperation: NSWindow { let styleMask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable] let window = WindowCancelOperation( - contentRect: NSRect(x: 500, y: 600, width: 1100, height: 600), + contentRect: NSRect(origin: .zero, size: defaultContentSize()), styleMask: styleMask, backing: .buffered, defer: false @@ -44,10 +50,25 @@ class WindowCancelOperation: NSWindow { window.allowsToolTipsWhenApplicationIsInactive = true window.autorecalculatesKeyViewLoop = false window.setIsVisible(false) - window.minSize = NSSize(width: 480, height: 400) - window.contentMinSize = NSSize(width: 300, height: 200) + window.minSize = minWindowSize + window.contentMinSize = minContentSize window.contentView?.autoresizingMask = [.width, .height] + // a fixed origin lands on a different screen spot depending on the screen size, + // center() keeps the window above the visual center as the fixed origin used to do + window.center() + return window } + + private static func defaultContentSize() -> NSSize { + guard let visibleFrame = NSScreen.main?.visibleFrame else { + return maxContentSize + } + + return NSSize( + width: min(maxContentSize.width, visibleFrame.width * maxScreenFillRatio), + height: min(maxContentSize.height, visibleFrame.height * maxScreenFillRatio) + ) + } } diff --git a/Sources/SharedUI/Components/ComparisonActivityDisplayable.swift b/Sources/SharedUI/Components/ComparisonActivityDisplayable.swift new file mode 100644 index 0000000..3386cdd --- /dev/null +++ b/Sources/SharedUI/Components/ComparisonActivityDisplayable.swift @@ -0,0 +1,42 @@ +// +// ComparisonActivityDisplayable.swift +// VisualDiffer +// +// Created by davide ficano on 08/09/26. +// Copyright (c) 2026 visualdiffer.com +// + +// the status bar and the controls a running comparison takes over, shared by the file +// and the folder window so the two cannot drift apart again +@MainActor +protocol ComparisonActivityDisplayable: AnyObject { + var differenceCounters: DifferenceCounters { get } + var statusbarText: NSTextField { get } + var progressView: ProgressBarView { get } + var window: NSWindow? { get } + + var lockedScopeBar: ScopeBarView { get } + var lockedPathViews: [PathView] { get } +} + +extension ComparisonActivityDisplayable { + // the progress replaces the counters and the status text while a comparison runs + func setProgressHidden(_ hidden: Bool) { + differenceCounters.isHidden = !hidden + statusbarText.isHidden = !hidden + progressView.isHidden = hidden + } + + func setComparisonRunning(_ running: Bool) { + setProgressHidden(!running) + + for pathView in lockedPathViews { + pathView.isEnabled = !running + } + lockedScopeBar.setEnabledAllGroups(!running) + + // the toolbar is the only always visible validated control, it would otherwise + // keep the items it draws until AppKit decides to validate again + window?.toolbar?.validateVisibleItems() + } +} diff --git a/Sources/SharedUI/Components/ConsoleDisplayable.swift b/Sources/SharedUI/Components/ConsoleDisplayable.swift new file mode 100644 index 0000000..7126a7a --- /dev/null +++ b/Sources/SharedUI/Components/ConsoleDisplayable.swift @@ -0,0 +1,78 @@ +// +// ConsoleDisplayable.swift +// VisualDiffer +// +// Created by davide ficano on 11/09/26. +// Copyright (c) 2026 visualdiffer.com +// + +// the console pane is laid out by the splitter, so its frame is still the seed one when the +// constraints are first evaluated and it must already clear the height its own content needs, +// or that first pass is unsatisfiable and causes AppKit to log a conflict +private let consoleSeedHeight: CGFloat = 120.0 + +// the log console pane, shared by the file and the folder window so the two cannot drift apart +@MainActor +protocol ConsoleDisplayable: ConsoleViewDelegate { + var consoleView: ConsoleView { get } + var consoleSplitter: DualPaneSplitView { get } + var consoleDelegate: DualPaneSplitViewDelegate { get } + var window: NSWindow? { get } + + // the console takes the focus when it opens, hiding it hands the focus back to this view + var consoleFocusView: NSView { get } +} + +extension ConsoleDisplayable { + func createConsoleView() -> ConsoleView { + let view = ConsoleView(frame: NSRect(x: 0, y: 0, width: 1, height: consoleSeedHeight)) + view.delegate = self + + return view + } + + func createConsoleSplitter() -> DualPaneSplitView { + let view = DualPaneSplitView(frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + + return view + } + + func setupConsoleSplitter() { + consoleSplitter.delegate = consoleDelegate + consoleSplitter.collapseSubview() + } + + // MARK: - Delegate + + func hide(console _: ConsoleView) { + if consoleSplitter.hasSubviewCollapsed { + showConsoleView() + } else { + hideConsoleView() + } + } + + // MARK: - ConsoleSplitView + + func log(error: String) { + showConsoleView() + consoleView.log(error: error) + } + + func logComparisonCompleted(elapsedTimeText: String) { + consoleView.log(info: String(format: NSLocalizedString("Comparison completed in %@", comment: ""), elapsedTimeText)) + } + + func showConsoleView() { + consoleSplitter.expandSubview() + consoleView.focus() + } + + func hideConsoleView() { + consoleSplitter.collapseSubview() + // focus lost on console hide, AppKit leaves the first responder on the + // hidden text view and the window moves it to the toolbar + window?.makeFirstResponder(consoleFocusView) + } +} diff --git a/Sources/SharedUI/Components/FindOptions.swift b/Sources/SharedUI/Components/FindOptions.swift new file mode 100644 index 0000000..be634b3 --- /dev/null +++ b/Sources/SharedUI/Components/FindOptions.swift @@ -0,0 +1,73 @@ +// +// FindOptions.swift +// VisualDiffer +// +// Created by davide ficano on 22/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +enum FindMode: Int, CaseIterable { + case containingText = 1 + case matchingWord + case regularExpression + + var title: String { + switch self { + case .containingText: + NSLocalizedString("Containing Text", comment: "") + case .matchingWord: + NSLocalizedString("Matching Word", comment: "") + case .regularExpression: + NSLocalizedString("Regular Expression", comment: "") + } + } +} + +struct FindOptions { + var mode = FindMode.containingText + var matchCase = false + var matchFiles = true + var matchFolders = true + + // nil when the pattern is not a valid regular expression + func matcher(for pattern: String) -> ((String) -> Bool)? { + if mode == .containingText { + // the string search also matches canonically equivalent forms, e.g. composed and decomposed accents + let compareOptions: String.CompareOptions = matchCase ? [] : .caseInsensitive + + return { $0.range(of: pattern, options: compareOptions) != nil } + } + + // the regular expression compares code units, both sides are precomposed + // so that a decomposed file name matches a composed pattern + guard let re = regex(for: pattern.precomposedStringWithCanonicalMapping) else { + return nil + } + + return { text in + let text = text.precomposedStringWithCanonicalMapping + + return re.firstMatch( + in: text, + options: [], + range: NSRange(location: 0, length: text.utf16.count) + ) != nil + } + } + + private func regex(for pattern: String) -> NSRegularExpression? { + let expression = switch mode { + case .matchingWord: + // \b matches the word boundaries around the searched text + "\\b\(NSRegularExpression.escapedPattern(for: pattern))\\b" + // .containingText is matched without a regular expression, it never reaches here + case .containingText, .regularExpression: + pattern + } + + return try? NSRegularExpression( + pattern: expression, + options: matchCase ? [] : .caseInsensitive + ) + } +} diff --git a/Sources/SharedUI/Components/FindText.swift b/Sources/SharedUI/Components/FindText.swift index 9cd6bf1..00792ea 100644 --- a/Sources/SharedUI/Components/FindText.swift +++ b/Sources/SharedUI/Components/FindText.swift @@ -11,15 +11,25 @@ protocol FindTextDelegate: AnyObject { func find(findText: FindText, moveToMatchIndex index: Int) -> Bool func numberOfMatches(in findText: FindText) -> Int func clearMatches(in findText: FindText) + func selectAllMatches(in findText: FindText, side: DisplaySide) } -class FindText: NSView, NSSearchFieldDelegate { +class FindText: NSView, NSSearchFieldDelegate, NSMenuItemValidation { private static let viewHeight: CGFloat = 25 private var lastIndexFound = -1 var delegate: FindTextDelegate? + // files and folders can be matched separately only when searching a folders comparison + var showsFileFilters = false { + didSet { + updateSearchMenu() + } + } + + private(set) var findOptions = FindOptions() + private lazy var rewindView: WindowOSD = .init( image: VDSymbol.Asset.rewind.image(), parent: window @@ -87,6 +97,48 @@ class FindText: NSView, NSSearchFieldDelegate { return view }() + private lazy var matchCaseItem = menuItem( + NSLocalizedString("Match Case", comment: ""), + action: #selector(toggleMatchCase) + ) + + private lazy var matchFilesItem = menuItem( + NSLocalizedString("Match Files", comment: ""), + action: #selector(toggleMatchFiles) + ) + + private lazy var matchFoldersItem = menuItem( + NSLocalizedString("Match Folders", comment: ""), + action: #selector(toggleMatchFolders) + ) + + private lazy var modeItems: [NSMenuItem] = FindMode.allCases.map { mode in + let item = NSMenuItem( + title: mode.title, + action: #selector(selectFindMode), + keyEquivalent: "" + ) + + item.target = self + // the tag is the only way to know the clicked mode because the menu shown + // by the search field is a copy of this template + item.tag = mode.rawValue + + return item + } + + private lazy var searchMenu: NSMenu = { + let menu = NSMenu() + + modeItems.forEach { menu.addItem($0) } + menu.addItem(.separator()) + menu.addItem(matchCaseItem) + menu.addItem(matchFilesItem) + menu.addItem(matchFoldersItem) + + return menu + }() + var placeholder: String { get { searchField.placeholderString ?? "" } set { searchField.placeholderString = newValue } @@ -108,6 +160,8 @@ class FindText: NSView, NSSearchFieldDelegate { addSubview(arrows) addSubview(countLabel) + updateSearchMenu() + setupConstraints() } @@ -174,6 +228,8 @@ class FindText: NSView, NSSearchFieldDelegate { if delegate.find(findText: self, searchPattern: pattern) { moveToMatch(true) + } else { + updateCount() } } @@ -246,4 +302,72 @@ class FindText: NSView, NSSearchFieldDelegate { // doesn't expose searchField field but allow to set first responder searchField.becomeFirstResponder() } + + @objc + private func toggleMatchCase(_: AnyObject) { + findOptions.matchCase.toggle() + optionsChanged() + } + + @objc + private func toggleMatchFiles(_: AnyObject) { + findOptions.matchFiles.toggle() + optionsChanged() + } + + @objc + private func toggleMatchFolders(_: AnyObject) { + findOptions.matchFolders.toggle() + optionsChanged() + } + + @objc + private func selectFindMode(_ sender: NSMenuItem) { + guard let mode = FindMode(rawValue: sender.tag), + mode != findOptions.mode else { + return + } + + findOptions.mode = mode + optionsChanged() + } + + private func optionsChanged() { + // the matches found with the previous options are no longer valid + search(self) + } + + private func updateSearchMenu() { + matchFilesItem.isHidden = !showsFileFilters + matchFoldersItem.isHidden = !showsFileFilters + + // the template is copied when it is assigned, so it must be assigned again to show the changes + searchField.searchMenuTemplate = searchMenu + } + + // the shown items are copies of the template ones, their state is set while the menu is validated + func validateMenuItem(_ menuItem: NSMenuItem) -> Bool { + if menuItem.action == #selector(toggleMatchCase) { + menuItem.state = findOptions.matchCase ? .on : .off + } else if menuItem.action == #selector(selectFindMode) { + menuItem.state = menuItem.tag == findOptions.mode.rawValue ? .on : .off + } else if menuItem.action == #selector(toggleMatchFiles) { + menuItem.state = findOptions.matchFiles ? .on : .off + // the last checked file type cannot be turned off, nothing could be found anymore + return findOptions.matchFolders || !findOptions.matchFiles + } else if menuItem.action == #selector(toggleMatchFolders) { + menuItem.state = findOptions.matchFolders ? .on : .off + return findOptions.matchFiles || !findOptions.matchFolders + } + + return true + } + + private func menuItem(_ title: String, action: Selector) -> NSMenuItem { + let item = NSMenuItem(title: title, action: action, keyEquivalent: "") + + item.target = self + + return item + } } diff --git a/Sources/SharedUI/Components/NSTextField+CenterVertically.swift b/Sources/SharedUI/Components/NSTextField+CenterVertically.swift deleted file mode 100644 index 5615207..0000000 --- a/Sources/SharedUI/Components/NSTextField+CenterVertically.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// NSTextField+CenterVertically.swift -// VisualDiffer -// -// Created by davide ficano on 24/03/25. -// Copyright (c) 2025 visualdiffer.com -// - -extension NSTextField { - func centerVertically() { - let centeredCell = RSVerticallyCenteredTextFieldCell(textCell: "") - - centeredCell.controlSize = .small - centeredCell.isScrollable = true - centeredCell.lineBreakMode = .byClipping - centeredCell.alignment = .center - - cell = centeredCell - } -} diff --git a/Sources/SharedUI/Components/PathControl.swift b/Sources/SharedUI/Components/PathControl.swift index 8febe83..ae32edf 100644 --- a/Sources/SharedUI/Components/PathControl.swift +++ b/Sources/SharedUI/Components/PathControl.swift @@ -24,6 +24,10 @@ protocol PathControlDelegate: NSPathControlDelegate { @objc @MainActor optional func saveFile(_ sender: AnyObject?) + + @objc + @MainActor + optional func pathControlCanChoosePath(_ pathControl: PathControl) -> Bool } public class PathControl: NSPathControl, NSMenuItemValidation { @@ -183,6 +187,10 @@ public class PathControl: NSPathControl, NSMenuItemValidation { return } + if delegate.pathControlCanChoosePath?(self) == false { + return + } + let openPanel = NSOpenPanel() openPanel.directoryURL = safePathComponentItem?.url ?? url diff --git a/Sources/SharedUI/Components/PathView.swift b/Sources/SharedUI/Components/PathView.swift index 4e19e5f..552f9c7 100644 --- a/Sources/SharedUI/Components/PathView.swift +++ b/Sources/SharedUI/Components/PathView.swift @@ -10,7 +10,10 @@ class PathView: NSView { var isEnabled = false { didSet { for view in stackView.views { - if let control = view as? NSControl { + // the save button is driven by its .enabled binding to isDirty, forcing it + // here would leave it lit on a file that has nothing to save + if let control = view as? NSControl, + control !== saveButton { control.isEnabled = isEnabled } } @@ -231,4 +234,10 @@ class PathView: NSView { } } } + + func unbindControls() { + lockButton.unbind(.value) + pathControl.unbind(.value) + saveButton.unbind(.enabled) + } } diff --git a/Sources/SharedUI/Components/TablePanelView.swift b/Sources/SharedUI/Components/TablePanelView.swift index 6201f45..3ee263e 100644 --- a/Sources/SharedUI/Components/TablePanelView.swift +++ b/Sources/SharedUI/Components/TablePanelView.swift @@ -32,9 +32,9 @@ class TablePanelView, Bot 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() { @@ -129,4 +129,8 @@ class TablePanelView, Bot } func updateBottomBar() {} + + func unbindControls() { + pathView.unbindControls() + } } diff --git a/Sources/SharedUI/Components/TimeToleranceView.swift b/Sources/SharedUI/Components/TimeToleranceView.swift index bea2f3a..ed11184 100644 --- a/Sources/SharedUI/Components/TimeToleranceView.swift +++ b/Sources/SharedUI/Components/TimeToleranceView.swift @@ -9,9 +9,21 @@ class TimeToleranceView: NSView, NSTextFieldDelegate { private lazy var inputText: NSTextField = createInputText() private lazy var stepper: NSStepper = createStepper() + private lazy var stackView: NSStackView = createStackView() var onTextChanged: ((TimeToleranceView) -> Void)? + // the row is disabled instead of hidden, hiding it changes the height the panel requires + var isEnabled = true { + didSet { + for view in stackView.views { + if let control = view as? NSControl { + control.isEnabled = isEnabled + } + } + } + } + var tolerance = 0 { didSet { if let onTextChanged { @@ -29,7 +41,7 @@ class TimeToleranceView: NSView, NSTextFieldDelegate { } private func setupViews() { - addSubview(createStackView()) + addSubview(stackView) } @available(*, unavailable) diff --git a/Tests/AppConfig.xcconfig b/Tests/AppConfig.xcconfig index d9ae040..d1272ef 100644 --- a/Tests/AppConfig.xcconfig +++ b/Tests/AppConfig.xcconfig @@ -10,5 +10,14 @@ PRODUCT_BUNDLE_IDENTIFIER = com.visualdiffer.tests ENABLE_APP_SANDBOX = NO +// see the note in AppConfig.xcconfig: the entitlements belong to the target, not to the machine +CODE_SIGN_ENTITLEMENTS = ./Tests/NoSandbox.entitlements + +// the XCTest, Testing and libXCTestSwiftSupport libraries shipped with Xcode are built for macOS 14.0, +// so linking the test bundle at the app deployment target of 13.5 emits "built for newer version" warnings +// the test bundle is never shipped, it runs only on the developer machine and on CI, so it can require a +// newer minimum than the app, which stays on 13.5 for its users +MACOSX_DEPLOYMENT_TARGET = 14.0 + #include "Signing.local.xcconfig" #include "Versions.local.xcconfig" diff --git a/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift b/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift index f4d46bd..7c28659 100644 --- a/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift +++ b/Tests/Sources/FileCompare/DiffResult/Tests/DiffResultTests.swift @@ -709,6 +709,64 @@ final class DiffResultTests: DiffResultBaseTests { #expect(rightLine == diffRightLine.text) } } + + @Test + func insertKeepsDestinationEOL() { + let leftText = "alpha\r\nbeta\r\ngamma" + let rightText = "alpha\r\ngamma" + + let diffResult = DiffResult() + diffResult.diff(leftText: leftText, rightText: rightText) + + assert(lines: diffResult.leftSide.lines, expectedValue: [.matching, .deleted, .matching]) + assert(lines: diffResult.rightSide.lines, expectedValue: [.matching, .missing, .matching]) + + // pasted text always carries a unix line ending, the destination one must win + diffResult.insert(text: "beta\n", at: 1, side: .right) + + assert(lines: diffResult.leftSide.lines, expectedValue: [.matching, .matching, .matching]) + assert(lines: diffResult.rightSide.lines, expectedValue: [.matching, .matching, .matching]) + #expect(diffResult.rightSide.lines[1].component.eol == .pcCRLF) + } + + @Test + func insertOfCanonicallyEquivalentLineIsChanged() { + let leftText = "caf\u{00E9}\nsecond" + let rightText = "second" + + let diffResult = DiffResult() + diffResult.diff(leftText: leftText, rightText: rightText) + + assert(lines: diffResult.leftSide.lines, expectedValue: [.deleted, .matching]) + assert(lines: diffResult.rightSide.lines, expectedValue: [.missing, .matching]) + + // String considers the two spellings equal, the paste must not + diffResult.insert(text: "cafe\u{0301}\n", at: 0, side: .right) + + assert(lines: diffResult.leftSide.lines, expectedValue: [.changed, .matching]) + assert(lines: diffResult.rightSide.lines, expectedValue: [.changed, .matching]) + #expect(diffResult.leftSide.lines[0].inlineRanges == [3 ..< 4]) + #expect(diffResult.rightSide.lines[0].inlineRanges == [3 ..< 4]) + } + + @Test + func insertHonoursTheComparisonOptions() { + let leftText = "ALPHA\nsecond" + let rightText = "second" + + let diffResult = DiffResult(options: [.ignoreCharacterCase]) + diffResult.diff(leftText: leftText, rightText: rightText) + + assert(lines: diffResult.leftSide.lines, expectedValue: [.deleted, .matching]) + assert(lines: diffResult.rightSide.lines, expectedValue: [.missing, .matching]) + + diffResult.insert(text: "alpha\n", at: 0, side: .right) + + assert(lines: diffResult.leftSide.lines, expectedValue: [.matching, .matching]) + assert(lines: diffResult.rightSide.lines, expectedValue: [.matching, .matching]) + #expect(diffResult.leftSide.lines[0].hasIgnoredDifferences) + #expect(diffResult.rightSide.lines[0].hasIgnoredDifferences) + } } // swiftlint:enable file_length line_length diff --git a/Tests/Sources/FileCompare/DiffResult/Tests/InlineDiffTests.swift b/Tests/Sources/FileCompare/DiffResult/Tests/InlineDiffTests.swift new file mode 100644 index 0000000..6f6fda1 --- /dev/null +++ b/Tests/Sources/FileCompare/DiffResult/Tests/InlineDiffTests.swift @@ -0,0 +1,257 @@ +// +// InlineDiffTests.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +import Testing +@testable import VisualDiffer + +final class InlineDiffTests: DiffResultBaseTests { + private static let longLineLength = 3000 + + @Test + func identicalLines() { + let diff = inlineDiff("hello", "hello") + + #expect(diff.leftRanges.isEmpty) + #expect(diff.rightRanges.isEmpty) + } + + @Test + func emptyLines() { + let diff = inlineDiff("", "") + + #expect(diff.leftRanges.isEmpty) + #expect(diff.rightRanges.isEmpty) + } + + @Test + func emptyAgainstFilledLine() { + let diff = inlineDiff("", "abc") + + #expect(diff.leftRanges.isEmpty) + #expect(diff.rightRanges == [0 ..< 3]) + } + + @Test + func oneCharacterInTheMiddle() { + let diff = inlineDiff("abcde", "abXde") + + #expect(diff.leftRanges == [2 ..< 3]) + #expect(diff.rightRanges == [2 ..< 3]) + } + + @Test + func changeAtTheStart() { + let diff = inlineDiff("Xbcde", "abcde") + + #expect(diff.leftRanges == [0 ..< 1]) + #expect(diff.rightRanges == [0 ..< 1]) + } + + @Test + func changeAtTheEnd() { + let diff = inlineDiff("abcdX", "abcde") + + #expect(diff.leftRanges == [4 ..< 5]) + #expect(diff.rightRanges == [4 ..< 5]) + } + + @Test + func pureInsertion() { + let diff = inlineDiff("abc", "abXYc") + + #expect(diff.leftRanges.isEmpty) + #expect(diff.rightRanges == [2 ..< 4]) + } + + @Test + func pureDeletion() { + let diff = inlineDiff("abXYc", "abc") + + #expect(diff.leftRanges == [2 ..< 4]) + #expect(diff.rightRanges.isEmpty) + } + + @Test + func severalDisjointChanges() { + let diff = inlineDiff("abcXefgYijk", "abcQefgRijk") + + #expect(diff.leftRanges == [3 ..< 4, 7 ..< 8]) + #expect(diff.rightRanges == [3 ..< 4, 7 ..< 8]) + } + + @Test + func changedIdentifierKeepsCommonPrefixAndSuffix() { + let diff = inlineDiff("let value = computeTotal(a, b)", "let value = computeSum(a, b)") + + #expect(diff.leftRanges == [19 ..< 24]) + #expect(diff.rightRanges == [19 ..< 22]) + } + + @Test + func characterCaseIsHonouredOnlyWhenIgnored() { + let sensitive = inlineDiff("Hello", "hello") + let insensitive = inlineDiff("Hello", "hello", options: [.ignoreCharacterCase]) + + #expect(sensitive.leftRanges == [0 ..< 1]) + #expect(sensitive.rightRanges == [0 ..< 1]) + #expect(insensitive.leftRanges.isEmpty) + #expect(insensitive.rightRanges.isEmpty) + } + + @Test + func canonicallyEquivalentCharactersAreDifferent() { + let precomposed = "caf\u{00E9}" + let decomposed = "cafe\u{0301}" + + // Character compares the two spellings as equal and both count as one character, + // the comparison must still report the difference + #expect(precomposed == decomposed) + #expect(precomposed.count == decomposed.count) + + let diff = inlineDiff(precomposed, decomposed) + + #expect(diff.leftRanges == [3 ..< 4]) + #expect(diff.rightRanges == [3 ..< 4]) + } + + @Test + func canonicallyEquivalentCharactersDifferWhileIgnoringCase() { + let diff = inlineDiff("CAF\u{00C9}", "cafe\u{0301}", options: [.ignoreCharacterCase]) + + #expect(diff.leftRanges == [3 ..< 4]) + #expect(diff.rightRanges == [3 ..< 4]) + } + + @Test + func canonicallyEquivalentLinesAreReportedAsChanged() { + let diffResult = DiffResult() + diffResult.diff(leftText: "caf\u{00E9}\nsecond", rightText: "cafe\u{0301}\nsecond") + + #expect(diffResult.leftSide.lines[0].type == .changed) + #expect(diffResult.rightSide.lines[0].type == .changed) + #expect(diffResult.leftSide.lines[0].inlineRanges == [3 ..< 4]) + #expect(diffResult.rightSide.lines[0].inlineRanges == [3 ..< 4]) + #expect(diffResult.leftSide.lines[1].type == .matching) + } + + @Test + func longLinesDegradeToASingleRange() { + let filler = String(repeating: "a", count: Self.longLineLength) + let diff = inlineDiff(filler + "X" + filler, filler + "Y" + filler) + + #expect(diff.leftRanges == [Self.longLineLength ..< Self.longLineLength + 1]) + #expect(diff.rightRanges == [Self.longLineLength ..< Self.longLineLength + 1]) + } + + @Test + func longIdenticalLinesHaveNoRanges() { + let filler = String(repeating: "a", count: Self.longLineLength) + let diff = inlineDiff(filler, filler) + + #expect(diff.leftRanges.isEmpty) + #expect(diff.rightRanges.isEmpty) + } + + @Test + func longLineAgainstItsOwnPrefix() { + let filler = String(repeating: "a", count: Self.longLineLength) + let diff = inlineDiff(filler, filler + "X") + + #expect(diff.leftRanges.isEmpty) + #expect(diff.rightRanges == [Self.longLineLength ..< Self.longLineLength + 1]) + } + + @Test + func longLineChangedAtTheStart() { + let filler = String(repeating: "a", count: Self.longLineLength) + let diff = inlineDiff("X" + filler, "Y" + filler) + + #expect(diff.leftRanges == [0 ..< 1]) + #expect(diff.rightRanges == [0 ..< 1]) + } + + @Test + func longLineChangedAtTheEnd() { + let filler = String(repeating: "a", count: Self.longLineLength) + let diff = inlineDiff(filler + "X", filler + "Y") + + #expect(diff.leftRanges == [Self.longLineLength ..< Self.longLineLength + 1]) + #expect(diff.rightRanges == [Self.longLineLength ..< Self.longLineLength + 1]) + } + + @Test + func longLinesIgnoringCharacterCase() { + let filler = String(repeating: "a", count: Self.longLineLength) + let diff = inlineDiff(filler + "X", filler.uppercased() + "y", options: [.ignoreCharacterCase]) + + #expect(diff.leftRanges == [Self.longLineLength ..< Self.longLineLength + 1]) + #expect(diff.rightRanges == [Self.longLineLength ..< Self.longLineLength + 1]) + } + + @Test + func longLinesKeepTheExactComparison() { + let filler = String(repeating: "a", count: Self.longLineLength) + let diff = inlineDiff(filler + "caf\u{00E9}" + filler, filler + "cafe\u{0301}" + filler) + let expected = [Self.longLineLength + 3 ..< Self.longLineLength + 4] + + #expect(diff.leftRanges == expected) + #expect(diff.rightRanges == expected) + } + + @Test + func changedPairCarriesRanges() { + let diffResult = DiffResult() + diffResult.diff(leftText: "alpha\nbeta1\ngamma", rightText: "alpha\nbeta2\ngamma") + + #expect(diffResult.leftSide.lines[1].type == .changed) + #expect(diffResult.leftSide.lines[1].inlineRanges == [4 ..< 5]) + #expect(diffResult.rightSide.lines[1].inlineRanges == [4 ..< 5]) + #expect(diffResult.leftSide.lines[0].inlineRanges.isEmpty) + #expect(diffResult.leftSide.lines[2].inlineRanges.isEmpty) + } + + @Test + func replacingComponentClearsRanges() { + let line = DiffLine( + with: .changed, + number: 1, + component: DiffLineComponent(text: "abc", eol: .unix) + ) + line.inlineRanges = [0 ..< 1] + + line.component = DiffLineComponent(text: "xyz", eol: .unix) + + #expect(line.inlineRanges.isEmpty) + } + + @Test + func missingLineClearsRanges() { + let line = DiffLine( + with: .changed, + number: 1, + component: DiffLineComponent(text: "abc", eol: .unix) + ) + line.inlineRanges = [0 ..< 1] + + line.makeMissing() + + #expect(line.inlineRanges.isEmpty) + } + + private func inlineDiff( + _ leftText: String, + _ rightText: String, + options: DiffResult.Options = [] + ) -> InlineDiff { + InlineDiff.between( + DiffLineComponent(text: leftText, eol: .missing), + DiffLineComponent(text: rightText, eol: .missing), + options: options + ) + } +} diff --git a/Tests/Sources/FileCompare/DiffResult/Tests/SequenceDiffTests.swift b/Tests/Sources/FileCompare/DiffResult/Tests/SequenceDiffTests.swift new file mode 100644 index 0000000..f0e5f26 --- /dev/null +++ b/Tests/Sources/FileCompare/DiffResult/Tests/SequenceDiffTests.swift @@ -0,0 +1,128 @@ +// +// SequenceDiffTests.swift +// VisualDiffer +// +// Created by davide ficano on 12/08/26. +// Copyright (c) 2026 visualdiffer.com +// + +import Testing +@testable import VisualDiffer + +final class SequenceDiffTests: DiffResultBaseTests { + @Test + func emptySequences() { + #expect(changes([], []).isEmpty) + } + + @Test + func identicalSequences() { + #expect(changes(["a", "b", "c"], ["a", "b", "c"]).isEmpty) + } + + @Test + func insertedIntoEmptySequence() { + #expect(changes([], ["a", "b"]) == [DiffChange(line0: 0, line1: 0, deleted: 0, inserted: 2)]) + } + + @Test + func deletedWholeSequence() { + #expect(changes(["a", "b"], []) == [DiffChange(line0: 0, line1: 0, deleted: 2, inserted: 0)]) + } + + @Test + func deletedFromMiddle() { + #expect(changes(["a", "b", "c"], ["a", "c"]) == [DiffChange(line0: 1, line1: 1, deleted: 1, inserted: 0)]) + } + + @Test + func replacedElement() { + #expect(changes(["a", "b", "c"], ["a", "x", "c"]) == [DiffChange(line0: 1, line1: 1, deleted: 1, inserted: 1)]) + } + + @Test + func twoSeparateHunks() { + let result = changes(["a", "b", "c", "d", "e"], ["x", "b", "c", "y", "e"]) + + #expect(result == [ + DiffChange(line0: 0, line1: 0, deleted: 1, inserted: 1), + DiffChange(line0: 3, line1: 3, deleted: 1, inserted: 1), + ]) + } + + @Test + func blankLineDeletion() { + let result = changes(["a", "", "b", "", "c"], ["a", "b", "", "c"]) + + #expect(result == [DiffChange(line0: 1, line1: 1, deleted: 1, inserted: 0)]) + } + + @Test + func repeatedRunTrimmedAtBothEnds() { + let result = changes(["x", "a", "a", "a", "y"], ["a", "a", "a"]) + + #expect(result == [ + DiffChange(line0: 0, line1: 0, deleted: 1, inserted: 0), + DiffChange(line0: 4, line1: 3, deleted: 1, inserted: 0), + ]) + } + + @Test + func insertionNextToABlankLine() { + let result = changes(["a", "b", "", "c", "d"], ["a", "b", "", "x", "", "c", "d"]) + + #expect(result == [DiffChange(line0: 3, line1: 3, deleted: 0, inserted: 2)]) + } + + @Test + func duplicateHeavySequenceKeepsTheDiscardHeuristicHonest() { + let lineCount = 80 + let distinctCount = 4 + let base = (0 ..< lineCount).map { "line \($0 % distinctCount)" } + var mutated = base + + mutated[10] = "unique A" + mutated[40] = "unique B" + mutated.insert("unique C", at: 60) + mutated.remove(at: 20) + + #expect(changes(base, mutated) == [ + DiffChange(line0: 10, line1: 10, deleted: 1, inserted: 1), + DiffChange(line0: 20, line1: 20, deleted: 1, inserted: 0), + DiffChange(line0: 40, line1: 39, deleted: 1, inserted: 1), + DiffChange(line0: 60, line1: 59, deleted: 0, inserted: 1), + ]) + } + + @Test + func canonicallyEquivalentLinesAreDifferent() { + let precomposed = "caf\u{00E9}" + let decomposed = "cafe\u{0301}" + + // String compares the two spellings as equal, the diff engine must not, + // otherwise a real file difference would be silently hidden + #expect(precomposed == decomposed) + + #expect(changes([precomposed], [decomposed]) == [DiffChange(line0: 0, line1: 0, deleted: 1, inserted: 1)]) + } + + @Test + func ignoredDiscardsKeepFrequentElements() { + // without ignoresDiscards the frequent element would be provisionally discarded + // and the whole sequence would be reported as changed + let left = Array(String(repeating: "a", count: 100) + "X" + String(repeating: "a", count: 100)) + let right = Array(String(repeating: "a", count: 100) + "Y" + String(repeating: "a", count: 100)) + + let result = SequenceDiff.changes(left: left, right: right, ignoresDiscards: true) + + #expect(result == [DiffChange(line0: 100, line1: 100, deleted: 1, inserted: 1)]) + } + + /// compares through the exact code unit key, as the file comparison does + private func changes(_ left: [String], _ right: [String]) -> [DiffChange] { + SequenceDiff.changes( + left: left.map { SequenceDiff.TextKey($0) }, + right: right.map { SequenceDiff.TextKey($0) } + ) + } +} diff --git a/Tests/Sources/FileCompare/DiffResult/Tests/WhitespacesTests.swift b/Tests/Sources/FileCompare/DiffResult/Tests/WhitespacesTests.swift index 29e1ab4..7dabdbb 100644 --- a/Tests/Sources/FileCompare/DiffResult/Tests/WhitespacesTests.swift +++ b/Tests/Sources/FileCompare/DiffResult/Tests/WhitespacesTests.swift @@ -19,8 +19,8 @@ final class WhitespacesTests: DiffResultBaseTests { " leading spaces\n" + "another line" - let diffResult = DiffResult() - diffResult.diff(leftText: leftText, rightText: rightText, options: .ignoreLeadingWhitespaces) + let diffResult = DiffResult(options: .ignoreLeadingWhitespaces) + diffResult.diff(leftText: leftText, rightText: rightText) let leftChangeType: [DiffChangeType] = [.matching, .matching] assert(lines: diffResult.leftSide.lines, expectedValue: leftChangeType) @@ -44,8 +44,8 @@ final class WhitespacesTests: DiffResultBaseTests { "line1\n" + "another text tabs and spaces" - let diffResult = DiffResult() - diffResult.diff(leftText: leftText, rightText: rightText, options: .ignoreInternalWhitespaces) + let diffResult = DiffResult(options: .ignoreInternalWhitespaces) + diffResult.diff(leftText: leftText, rightText: rightText) let leftChangeType: [DiffChangeType] = [.changed, .matching] assert(lines: diffResult.leftSide.lines, expectedValue: leftChangeType) @@ -71,8 +71,8 @@ final class WhitespacesTests: DiffResultBaseTests { "another text tabs and spaces\n" + " last line" - let diffResult = DiffResult() - diffResult.diff(leftText: leftText, rightText: rightText, options: [.allIgnoreWhitespaces, .ignoreLineEndings]) + let diffResult = DiffResult(options: [.allIgnoreWhitespaces, .ignoreLineEndings]) + diffResult.diff(leftText: leftText, rightText: rightText) let leftChangeType: [DiffChangeType] = [.matching, .matching, .matching] assert(lines: diffResult.leftSide.lines, expectedValue: leftChangeType) @@ -98,8 +98,8 @@ final class WhitespacesTests: DiffResultBaseTests { "another text tabs and spaces\n" + " last line" - let diffResult = DiffResult() - diffResult.diff(leftText: leftText, rightText: rightText, options: [.allIgnoreWhitespaces]) + let diffResult = DiffResult(options: [.allIgnoreWhitespaces]) + diffResult.diff(leftText: leftText, rightText: rightText) let leftChangeType: [DiffChangeType] = [.matching, .changed, .matching] assert(lines: diffResult.leftSide.lines, expectedValue: leftChangeType) @@ -123,8 +123,8 @@ final class WhitespacesTests: DiffResultBaseTests { "line One\n" + "another line" - let diffResult = DiffResult() - diffResult.diff(leftText: leftText, rightText: rightText, options: .ignoreCharacterCase) + let diffResult = DiffResult(options: .ignoreCharacterCase) + diffResult.diff(leftText: leftText, rightText: rightText) let leftChangeType: [DiffChangeType] = [.matching, .matching] assert(lines: diffResult.leftSide.lines, expectedValue: leftChangeType) @@ -150,8 +150,8 @@ final class WhitespacesTests: DiffResultBaseTests { "inserted line\n" + " spaced line" - let diffResult = DiffResult() - diffResult.diff(leftText: leftText, rightText: rightText, options: .ignoreLeadingWhitespaces) + let diffResult = DiffResult(options: .ignoreLeadingWhitespaces) + diffResult.diff(leftText: leftText, rightText: rightText) let leftChangeType: [DiffChangeType] = [.matching, .missing, .missing, .matching] assert(lines: diffResult.leftSide.lines, expectedValue: leftChangeType) @@ -170,11 +170,10 @@ final class WhitespacesTests: DiffResultBaseTests { @Test func refreshSummaryAfterDeletingFilteredMatch() { - let diffResult = DiffResult() + let diffResult = DiffResult(options: .ignoreLeadingWhitespaces) diffResult.diff( leftText: " leading spaces", - rightText: "leading spaces", - options: .ignoreLeadingWhitespaces + rightText: "leading spaces" ) let filtered = DiffResult.justMatchingLines(diffResult) diff --git a/Tests/Sources/FileSystem/CaseSensitiveBaseTest.swift b/Tests/Sources/FileSystem/CaseSensitiveBaseTest.swift index d184b07..cfb0783 100644 --- a/Tests/Sources/FileSystem/CaseSensitiveBaseTest.swift +++ b/Tests/Sources/FileSystem/CaseSensitiveBaseTest.swift @@ -28,7 +28,7 @@ open class CaseSensitiveBaseTest: BaseTests { if !FileManager.default.fileExists(atPath: volumePath.osPath) { let process = Process() process.executableURL = URL(filePath: "/bin/sh") - process.arguments = ["-c", "diskutil erasevolume 'Case-sensitive HFS+' '\(volumeName)' `hdiutil attach -nomount ram://1048576`"] + process.arguments = ["-c", "diskutil erasevolume 'Case-sensitive HFS+' '\(volumeName)' $(diskutil image attach ram://1048576)"] do { try process.run() } catch { diff --git a/VisualDiffer.xctestplan b/Tests/VisualDiffer.xctestplan similarity index 100% rename from VisualDiffer.xctestplan rename to Tests/VisualDiffer.xctestplan diff --git a/Versions-Template.xcconfig b/Versions-Template.xcconfig index daf4d96..692dac5 100644 --- a/Versions-Template.xcconfig +++ b/Versions-Template.xcconfig @@ -6,7 +6,7 @@ // Copyright (c) 2022 visualdiffer.com // -// Copy this file to Versions.xcconfig and edit the values +// scripts/setup-local-env.sh copies this file to Versions.local.xcconfig, edit the values there APP_VERSION = 1.0.0 APP_BUNDLE_VERSION = $(APP_VERSION) diff --git a/VisualDiffer-Bridging-Header.h b/VisualDiffer-Bridging-Header.h index 99175c7..738ca76 100644 --- a/VisualDiffer-Bridging-Header.h +++ b/VisualDiffer-Bridging-Header.h @@ -7,5 +7,3 @@ #import "TitlePredicateEditorRowTemplate.h" #import "TOPFileSizePredicateEditorRowTemplate.h" #import "TOPTimestampPredicateEditorRowTemplate.h" -#import "UnifiedDiff.h" -#import "UDiffScriptBuilder.h" diff --git a/VisualDiffer.xcodeproj/project.pbxproj b/VisualDiffer.xcodeproj/project.pbxproj index 2d20e59..4cdb885 100644 --- a/VisualDiffer.xcodeproj/project.pbxproj +++ b/VisualDiffer.xcodeproj/project.pbxproj @@ -3,348 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 60; + objectVersion = 90; objects = { /* Begin PBXBuildFile section */ 2F7446AB0DB6BCF400F9684A /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 2F7446A70DB6BCF400F9684A /* MainMenu.xib */; }; - 551DE39F2ED422820067AB18 /* FilesWindowController+SessionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551DE39E2ED422820067AB18 /* FilesWindowController+SessionPreferences.swift */; }; - 551DE3A62ED4237B0067AB18 /* FileSessionPreferencesWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551DE3A42ED4237B0067AB18 /* FileSessionPreferencesWindow.swift */; }; - 551DE3AA2ED428B40067AB18 /* FilePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551DE3A92ED428A50067AB18 /* FilePreferences.swift */; }; - 551DE3B42ED42AD90067AB18 /* FilePreferencesComparisonPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551DE3B32ED42AD00067AB18 /* FilePreferencesComparisonPanel.swift */; }; - 551DE3B62ED42B4E0067AB18 /* FileComparisonBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551DE3B52ED42B4E0067AB18 /* FileComparisonBox.swift */; }; - 551DE3B82ED439AF0067AB18 /* PreferencesBoxDataSource+Default.swift in Sources */ = {isa = PBXBuildFile; fileRef = 551DE3B72ED4399B0067AB18 /* PreferencesBoxDataSource+Default.swift */; }; - 552430412F7673F800951F1A /* FileDifferenceNavigatorBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552430402F7673F800951F1A /* FileDifferenceNavigatorBox.swift */; }; - 552B85082F5FE29A00A9268A /* VDDocumentController+BlankDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552B85072F5FE29400A9268A /* VDDocumentController+BlankDocument.swift */; }; - 5536ADBA2EF169820019EFBF /* SessionDiff+ExtraData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5536ADB92EF169820019EFBF /* SessionDiff+ExtraData.swift */; }; - 553741792E9372DF00AB56D0 /* NSAppearance+DarkMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740E52E9372DF00AB56D0 /* NSAppearance+DarkMode.swift */; }; - 5537417A2E9372DF00AB56D0 /* QLPreviewPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740FD2E9372DF00AB56D0 /* QLPreviewPanel.swift */; }; - 5537417B2E9372DF00AB56D0 /* TrustedPathsPreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740A12E9372DF00AB56D0 /* TrustedPathsPreferencesPanel.swift */; }; - 5537417C2E9372DF00AB56D0 /* TouchPickersStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740142E9372DF00AB56D0 /* TouchPickersStackView.swift */; }; - 5537417D2E9372DF00AB56D0 /* BasePreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740862E9372DF00AB56D0 /* BasePreferences.swift */; }; - 5537417E2E9372DF00AB56D0 /* DualPaneSplitViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741532E9372DF00AB56D0 /* DualPaneSplitViewDelegate.swift */; }; - 5537417F2E9372DF00AB56D0 /* FoldersOutlineView+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FED2E9372DF00AB56D0 /* FoldersOutlineView+Menu.swift */; }; - 553741802E9372DF00AB56D0 /* AlignRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F8F2E9372DF00AB56D0 /* AlignRule.swift */; }; - 553741812E9372DF00AB56D0 /* VisualizationBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537409F2E9372DF00AB56D0 /* VisualizationBox.swift */; }; - 553741822E9372DF00AB56D0 /* CommonPrefs+FileCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F992E9372DF00AB56D0 /* CommonPrefs+FileCompare.swift */; }; - 553741832E9372DF00AB56D0 /* TablePanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741732E9372DF00AB56D0 /* TablePanelView.swift */; }; - 553741852E9372DF00AB56D0 /* NSOpenPanel+Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740FC2E9372DF00AB56D0 /* NSOpenPanel+Application.swift */; }; - 553741862E9372DF00AB56D0 /* ConfirmReplace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740012E9372DF00AB56D0 /* ConfirmReplace.swift */; }; - 553741872E9372DF00AB56D0 /* SessionDiff.ItemType+Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA62E9372DF00AB56D0 /* SessionDiff.ItemType+Paths.swift */; }; - 553741882E9372DF00AB56D0 /* FileSizeFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537412C2E9372DF00AB56D0 /* FileSizeFormatter.swift */; }; - 553741892E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = 553740DB2E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.m */; }; - 5537418A2E9372DF00AB56D0 /* DocumentWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F9F2E9372DF00AB56D0 /* DocumentWindow.swift */; }; - 5537418B2E9372DF00AB56D0 /* CommonPrefs+Name.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F9D2E9372DF00AB56D0 /* CommonPrefs+Name.swift */; }; - 5537418D2E9372DF00AB56D0 /* FilePathTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537416D2E9372DF00AB56D0 /* FilePathTextField.swift */; }; - 5537418E2E9372DF00AB56D0 /* CopyCompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740392E9372DF00AB56D0 /* CopyCompareItem.swift */; }; - 5537418F2E9372DF00AB56D0 /* ColoredFoldersManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FE62E9372DF00AB56D0 /* ColoredFoldersManager.swift */; }; - 553741902E9372DF00AB56D0 /* FilesTableView+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FB72E9372DF00AB56D0 /* FilesTableView+Menu.swift */; }; - 553741912E9372DF00AB56D0 /* NSBox+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741172E9372DF00AB56D0 /* NSBox+Helper.swift */; }; - 553741922E9372DF00AB56D0 /* FontPreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740942E9372DF00AB56D0 /* FontPreferencesPanel.swift */; }; - 553741932E9372DF00AB56D0 /* DisplayOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537404B2E9372DF00AB56D0 /* DisplayOptions.swift */; }; - 553741942E9372DF00AB56D0 /* FilesWindowController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FCE2E9372DF00AB56D0 /* FilesWindowController+Menu.swift */; }; - 553741952E9372DF00AB56D0 /* OptionSet+Toggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740F92E9372DF00AB56D0 /* OptionSet+Toggle.swift */; }; - 553741962E9372DF00AB56D0 /* URL+Finder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741102E9372DF00AB56D0 /* URL+Finder.swift */; }; - 553741982E9372DF00AB56D0 /* CommonPrefs+FolderCompare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F9A2E9372DF00AB56D0 /* CommonPrefs+FolderCompare.swift */; }; - 553741992E9372DF00AB56D0 /* FoldersWindowController+PathControlDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537402E2E9372DF00AB56D0 /* FoldersWindowController+PathControlDelegate.swift */; }; - 5537419A2E9372DF00AB56D0 /* FontBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740932E9372DF00AB56D0 /* FontBox.swift */; }; - 5537419B2E9372DF00AB56D0 /* FilesTableViewFindTextDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FB82E9372DF00AB56D0 /* FilesTableViewFindTextDelegate.swift */; }; - 5537419C2E9372DF00AB56D0 /* DeleteFileOperationExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FFD2E9372DF00AB56D0 /* DeleteFileOperationExecutor.swift */; }; - 5537419D2E9372DF00AB56D0 /* DualPaneSplitView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741522E9372DF00AB56D0 /* DualPaneSplitView.swift */; }; - 5537419E2E9372DF00AB56D0 /* StandardButtons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537414D2E9372DF00AB56D0 /* StandardButtons.swift */; }; - 5537419F2E9372DF00AB56D0 /* ConfirmationsFoldersBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537408B2E9372DF00AB56D0 /* ConfirmationsFoldersBox.swift */; }; - 553741A02E9372DF00AB56D0 /* DifferenceNavigatorBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537408E2E9372DF00AB56D0 /* DifferenceNavigatorBox.swift */; }; - 553741A12E9372DF00AB56D0 /* FolderReaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740602E9372DF00AB56D0 /* FolderReaderDelegate.swift */; }; - 553741A22E9372DF00AB56D0 /* UDiffScriptBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 553740BD2E9372DF00AB56D0 /* UDiffScriptBuilder.m */; }; - 553741A32E9372DF00AB56D0 /* String+RegularExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741072E9372DF00AB56D0 /* String+RegularExpression.swift */; }; - 553741A42E9372DF00AB56D0 /* FileSystemController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740182E9372DF00AB56D0 /* FileSystemController.swift */; }; - 553741A52E9372DF00AB56D0 /* ComparatorOptions+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740482E9372DF00AB56D0 /* ComparatorOptions+Helper.swift */; }; - 553741A62E9372DF00AB56D0 /* AppearanceBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740962E9372DF00AB56D0 /* AppearanceBox.swift */; }; - 553741A72E9372DF00AB56D0 /* FolderComparisonBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740972E9372DF00AB56D0 /* FolderComparisonBox.swift */; }; - 553741A82E9372DF00AB56D0 /* FoldersWindowController+Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740242E9372DF00AB56D0 /* FoldersWindowController+Document.swift */; }; - 553741A92E9372DF00AB56D0 /* FilterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537405D2E9372DF00AB56D0 /* FilterConfig.swift */; }; - 553741AA2E9372DF00AB56D0 /* IconUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537412F2E9372DF00AB56D0 /* IconUtils.swift */; }; - 553741AC2E9372DF00AB56D0 /* NSTextView+Style.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741202E9372DF00AB56D0 /* NSTextView+Style.swift */; }; - 553741AD2E9372DF00AB56D0 /* FoldersOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FE72E9372DF00AB56D0 /* FoldersOutlineView.swift */; }; - 553741AE2E9372DF00AB56D0 /* PreferredEditorBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740992E9372DF00AB56D0 /* PreferredEditorBox.swift */; }; - 553741AF2E9372DF00AB56D0 /* MoveCompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740412E9372DF00AB56D0 /* MoveCompareItem.swift */; }; - 553741B12E9372DF00AB56D0 /* TextFieldSelectionHolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537415C2E9372DF00AB56D0 /* TextFieldSelectionHolder.swift */; }; - 553741B22E9372DF00AB56D0 /* TouchFileOperationExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740132E9372DF00AB56D0 /* TouchFileOperationExecutor.swift */; }; - 553741B32E9372DF00AB56D0 /* NSTextFieldCell+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741452E9372DF00AB56D0 /* NSTextFieldCell+Helper.swift */; }; - 553741B42E9372DF00AB56D0 /* NSApplication.ModalResponse+ReplaceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740032E9372DF00AB56D0 /* NSApplication.ModalResponse+ReplaceFile.swift */; }; - 553741B52E9372DF00AB56D0 /* DiffOpenerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA42E9372DF00AB56D0 /* DiffOpenerDelegate.swift */; }; - 553741B72E9372DF00AB56D0 /* PreferencesCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740832E9372DF00AB56D0 /* PreferencesCheckbox.swift */; }; - 553741B82E9372DF00AB56D0 /* FileManager+Attributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740F12E9372DF00AB56D0 /* FileManager+Attributes.swift */; }; - 553741B92E9372DF00AB56D0 /* MainThreadFolderReaderDelegateBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537401C2E9372DF00AB56D0 /* MainThreadFolderReaderDelegateBridge.swift */; }; - 553741BA2E9372DF00AB56D0 /* VisibleWhitespaces.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537413F2E9372DF00AB56D0 /* VisibleWhitespaces.swift */; }; - 553741BB2E9372DF00AB56D0 /* DisplayPositionable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F912E9372DF00AB56D0 /* DisplayPositionable.swift */; }; - 553741BC2E9372DF00AB56D0 /* AlignRuleWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740AA2E9372DF00AB56D0 /* AlignRuleWindow.swift */; }; - 553741BD2E9372DF00AB56D0 /* DeleteCompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537403A2E9372DF00AB56D0 /* DeleteCompareItem.swift */; }; - 553741BE2E9372DF00AB56D0 /* DiffChangeType+Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FDD2E9372DF00AB56D0 /* DiffChangeType+Color.swift */; }; - 553741BF2E9372DF00AB56D0 /* DescriptionOutlineNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741412E9372DF00AB56D0 /* DescriptionOutlineNode.swift */; }; - 553741C02E9372DF00AB56D0 /* FilesTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FB62E9372DF00AB56D0 /* FilesTableView.swift */; }; - 553741C12E9372DF00AB56D0 /* String+Occcurrences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741052E9372DF00AB56D0 /* String+Occcurrences.swift */; }; - 553741C22E9372DF00AB56D0 /* FoldersWindowController+QLPreviewPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537402F2E9372DF00AB56D0 /* FoldersWindowController+QLPreviewPanel.swift */; }; - 553741C32E9372DF00AB56D0 /* FoldersOutlineView+DifferenceNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537405A2E9372DF00AB56D0 /* FoldersOutlineView+DifferenceNavigator.swift */; }; - 553741C42E9372DF00AB56D0 /* JumpToLineWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FE32E9372DF00AB56D0 /* JumpToLineWindow.swift */; }; - 553741C52E9372DF00AB56D0 /* EncodingError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741282E9372DF00AB56D0 /* EncodingError.swift */; }; - 553741C62E9372DF00AB56D0 /* FileExtraOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537404E2E9372DF00AB56D0 /* FileExtraOptions.swift */; }; - 553741C72E9372DF00AB56D0 /* Array+MoveIndexes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740E72E9372DF00AB56D0 /* Array+MoveIndexes.swift */; }; - 553741C82E9372DF00AB56D0 /* BigFileFileOperationManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 553740C82E9372DF00AB56D0 /* BigFileFileOperationManager.m */; }; - 553741C92E9372DF00AB56D0 /* FoldersWindowController+BaseFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740202E9372DF00AB56D0 /* FoldersWindowController+BaseFolder.swift */; }; - 553741CB2E9372DF00AB56D0 /* FoldersWindowController+UICreation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740322E9372DF00AB56D0 /* FoldersWindowController+UICreation.swift */; }; - 553741CC2E9372DF00AB56D0 /* FilesWindowController+UICreation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD82E9372DF00AB56D0 /* FilesWindowController+UICreation.swift */; }; - 553741CD2E9372DF00AB56D0 /* NSTableView+Row.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537410A2E9372DF00AB56D0 /* NSTableView+Row.swift */; }; - 553741CE2E9372DF00AB56D0 /* FindText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537416E2E9372DF00AB56D0 /* FindText.swift */; }; - 553741CF2E9372DF00AB56D0 /* FoldersTraversalBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740902E9372DF00AB56D0 /* FoldersTraversalBox.swift */; }; - 553741D02E9372DF00AB56D0 /* FileError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741342E9372DF00AB56D0 /* FileError.swift */; }; - 553741D12E9372DF00AB56D0 /* OpenEditorError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537413B2E9372DF00AB56D0 /* OpenEditorError.swift */; }; - 553741D22E9372DF00AB56D0 /* GeneralPreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740982E9372DF00AB56D0 /* GeneralPreferencesPanel.swift */; }; - 553741D32E9372DF00AB56D0 /* CompareItem+DifferenceNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537405B2E9372DF00AB56D0 /* CompareItem+DifferenceNavigator.swift */; }; - 553741D42E9372DF00AB56D0 /* PreferencesBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740812E9372DF00AB56D0 /* PreferencesBox.swift */; }; - 553741D52E9372DF00AB56D0 /* NSAlert+ReplaceFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740022E9372DF00AB56D0 /* NSAlert+ReplaceFile.swift */; }; - 553741D72E9372DF00AB56D0 /* URL+SymLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741152E9372DF00AB56D0 /* URL+SymLink.swift */; }; - 553741D82E9372DF00AB56D0 /* RecentDocumentPopupMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA22E9372DF00AB56D0 /* RecentDocumentPopupMenu.swift */; }; - 553741D92E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineViewContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740282E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineViewContextMenu.swift */; }; - 553741DA2E9372DF00AB56D0 /* FileAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537403B2E9372DF00AB56D0 /* FileAttributes.swift */; }; - 553741DB2E9372DF00AB56D0 /* SyncFileController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537400C2E9372DF00AB56D0 /* SyncFileController.swift */; }; - 553741DC2E9372DF00AB56D0 /* ConfirmationsFilesBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537408A2E9372DF00AB56D0 /* ConfirmationsFilesBox.swift */; }; - 553741DD2E9372DF00AB56D0 /* CompareItem+Accessors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740502E9372DF00AB56D0 /* CompareItem+Accessors.swift */; }; - 553741DE2E9372DF00AB56D0 /* FilesWindowController+JumpLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FCC2E9372DF00AB56D0 /* FilesWindowController+JumpLine.swift */; }; - 553741DF2E9372DF00AB56D0 /* CommonPrefs+ConsoleLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740362E9372DF00AB56D0 /* CommonPrefs+ConsoleLog.swift */; }; - 553741E02E9372DF00AB56D0 /* NSToolbar+Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537410C2E9372DF00AB56D0 /* NSToolbar+Create.swift */; }; - 553741E12E9372DF00AB56D0 /* String+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741032E9372DF00AB56D0 /* String+Helper.swift */; }; - 553741E32E9372DF00AB56D0 /* LineNumberTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC02E9372DF00AB56D0 /* LineNumberTableRowView.swift */; }; - 553741E42E9372DF00AB56D0 /* FolderReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537405E2E9372DF00AB56D0 /* FolderReader.swift */; }; - 553741E52E9372DF00AB56D0 /* DateFormatter+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740F32E9372DF00AB56D0 /* DateFormatter+Helper.swift */; }; - 553741E62E9372DF00AB56D0 /* FoldersWindowController+Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537402B2E9372DF00AB56D0 /* FoldersWindowController+Navigation.swift */; }; - 553741E72E9372DF00AB56D0 /* HistoryEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FAB2E9372DF00AB56D0 /* HistoryEntity.swift */; }; - 553741E82E9372DF00AB56D0 /* CompareItem+Clone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740512E9372DF00AB56D0 /* CompareItem+Clone.swift */; }; - 553741E92E9372DF00AB56D0 /* URL+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741122E9372DF00AB56D0 /* URL+Path.swift */; }; - 553741EA2E9372DF00AB56D0 /* PathChooser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA12E9372DF00AB56D0 /* PathChooser.swift */; }; - 553741EB2E9372DF00AB56D0 /* VDDocumentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA92E9372DF00AB56D0 /* VDDocumentController.swift */; }; - 553741EC2E9372DF00AB56D0 /* CopyFileOperationExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FFB2E9372DF00AB56D0 /* CopyFileOperationExecutor.swift */; }; - 553741ED2E9372DF00AB56D0 /* FoldersOutlineView+Columns.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FE92E9372DF00AB56D0 /* FoldersOutlineView+Columns.swift */; }; - 553741EE2E9372DF00AB56D0 /* NSTextField+CenterVertically.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537416F2E9372DF00AB56D0 /* NSTextField+CenterVertically.swift */; }; - 553741EF2E9372DF00AB56D0 /* BufferedInputStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741322E9372DF00AB56D0 /* BufferedInputStream.swift */; }; - 553741F02E9372DF00AB56D0 /* ComparisonStandardUserDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740802E9372DF00AB56D0 /* ComparisonStandardUserDataSource.swift */; }; - 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 */; }; - 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 */; }; - 553741F82E9372DF00AB56D0 /* KeyboardPreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537409C2E9372DF00AB56D0 /* KeyboardPreferencesPanel.swift */; }; - 553741F92E9372DF00AB56D0 /* CompareItem+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740402E9372DF00AB56D0 /* CompareItem+Metadata.swift */; }; - 553741FA2E9372DF00AB56D0 /* TextFieldVerticalCentered.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC32E9372DF00AB56D0 /* TextFieldVerticalCentered.swift */; }; - 553741FB2E9372DF00AB56D0 /* KeyEquivalent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F932E9372DF00AB56D0 /* KeyEquivalent.swift */; }; - 553741FC2E9372DF00AB56D0 /* CompareItem+VisibleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740572E9372DF00AB56D0 /* CompareItem+VisibleItem.swift */; }; - 553741FD2E9372DF00AB56D0 /* FoldersWindowController+SessionPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740312E9372DF00AB56D0 /* FoldersWindowController+SessionPreferences.swift */; }; - 553741FE2E9372DF00AB56D0 /* CompareItemTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FF62E9372DF00AB56D0 /* CompareItemTableCellView.swift */; }; - 553741FF2E9372DF00AB56D0 /* ProgressBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741582E9372DF00AB56D0 /* ProgressBarView.swift */; }; - 553742002E9372DF00AB56D0 /* FolderManagerError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537403F2E9372DF00AB56D0 /* FolderManagerError.swift */; }; - 553742012E9372DF00AB56D0 /* FolderSelectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537401E2E9372DF00AB56D0 /* FolderSelectionInfo.swift */; }; - 553742022E9372DF00AB56D0 /* SynchroScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537415A2E9372DF00AB56D0 /* SynchroScrollView.swift */; }; - 553742032E9372DF00AB56D0 /* FilesWindowController+Save.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD52E9372DF00AB56D0 /* FilesWindowController+Save.swift */; }; - 553742042E9372DF00AB56D0 /* FoldersWindowController+ConsoleViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740222E9372DF00AB56D0 /* FoldersWindowController+ConsoleViewDelegate.swift */; }; - 553742052E9372DF00AB56D0 /* PreferencesBoxDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740822E9372DF00AB56D0 /* PreferencesBoxDataSource.swift */; }; - 553742062E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = 553740D92E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.m */; }; - 553742072E9372DF00AB56D0 /* FolderReader+Detached.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537401B2E9372DF00AB56D0 /* FolderReader+Detached.swift */; }; - 553742082E9372DF00AB56D0 /* ViewLinkable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F962E9372DF00AB56D0 /* ViewLinkable.swift */; }; - 553742092E9372DF00AB56D0 /* SessionPreferencesFiltersBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740B42E9372DF00AB56D0 /* SessionPreferencesFiltersBox.swift */; }; - 5537420A2E9372DF00AB56D0 /* FilesWindowController+Common.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC72E9372DF00AB56D0 /* FilesWindowController+Common.swift */; }; - 5537420B2E9372DF00AB56D0 /* FilesWindowController+NSToolbarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD12E9372DF00AB56D0 /* FilesWindowController+NSToolbarDelegate.swift */; }; - 5537420C2E9372DF00AB56D0 /* FilesWindowController+TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD72E9372DF00AB56D0 /* FilesWindowController+TableView.swift */; }; - 5537420D2E9372DF00AB56D0 /* NSProgressIndicator+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537411C2E9372DF00AB56D0 /* NSProgressIndicator+Helper.swift */; }; - 5537420E2E9372DF00AB56D0 /* ComparatorOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740462E9372DF00AB56D0 /* ComparatorOptions.swift */; }; - 5537420F2E9372DF00AB56D0 /* PathTimestamps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740422E9372DF00AB56D0 /* PathTimestamps.swift */; }; - 553742102E9372DF00AB56D0 /* FilesWindowController+Clipboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC62E9372DF00AB56D0 /* FilesWindowController+Clipboard.swift */; }; - 553742112E9372DF00AB56D0 /* RSVerticallyCenteredTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741462E9372DF00AB56D0 /* RSVerticallyCenteredTextFieldCell.swift */; }; - 553742122E9372DF00AB56D0 /* CompareItem+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740532E9372DF00AB56D0 /* CompareItem+Description.swift */; }; - 553742132E9372DF00AB56D0 /* SessionPreferencesWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740A62E9372DF00AB56D0 /* SessionPreferencesWindow.swift */; }; - 553742142E9372DF00AB56D0 /* UserDefinedRulesBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740AF2E9372DF00AB56D0 /* UserDefinedRulesBox.swift */; }; - 553742162E9372DF00AB56D0 /* ProgressIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740092E9372DF00AB56D0 /* ProgressIndicatorController.swift */; }; - 553742172E9372DF00AB56D0 /* FolderPanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FF52E9372DF00AB56D0 /* FolderPanelView.swift */; }; - 553742182E9372DF00AB56D0 /* ComparatorOptions+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740472E9372DF00AB56D0 /* ComparatorOptions+Description.swift */; }; - 553742192E9372DF00AB56D0 /* FoldersOutlineView+Clipboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FE82E9372DF00AB56D0 /* FoldersOutlineView+Clipboard.swift */; }; - 5537421A2E9372DF00AB56D0 /* DatePickersStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740112E9372DF00AB56D0 /* DatePickersStackView.swift */; }; - 5537421B2E9372DF00AB56D0 /* UnifiedDiff.m in Sources */ = {isa = PBXBuildFile; fileRef = 553740BF2E9372DF00AB56D0 /* UnifiedDiff.m */; }; - 5537421C2E9372DF00AB56D0 /* SyncOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537400F2E9372DF00AB56D0 /* SyncOutlineView.swift */; }; - 5537421D2E9372DF00AB56D0 /* CompareItem+Comparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740522E9372DF00AB56D0 /* CompareItem+Comparison.swift */; }; - 5537421E2E9372DF00AB56D0 /* NSError+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740EF2E9372DF00AB56D0 /* NSError+Format.swift */; }; - 5537421F2E9372DF00AB56D0 /* RefreshInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740612E9372DF00AB56D0 /* RefreshInfo.swift */; }; - 553742202E9372DF00AB56D0 /* FileDropView+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741562E9372DF00AB56D0 /* FileDropView+Helper.swift */; }; - 553742212E9372DF00AB56D0 /* SyncFileOperationExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537400D2E9372DF00AB56D0 /* SyncFileOperationExecutor.swift */; }; - 553742222E9372DF00AB56D0 /* CompareItem+FilterConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740542E9372DF00AB56D0 /* CompareItem+FilterConfig.swift */; }; - 553742232E9372DF00AB56D0 /* NSStackView+PreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740A32E9372DF00AB56D0 /* NSStackView+PreferencesPanel.swift */; }; - 553742242E9372DF00AB56D0 /* CustomValidationToolbarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741482E9372DF00AB56D0 /* CustomValidationToolbarItem.swift */; }; - 553742252E9372DF00AB56D0 /* VisibleItem+Sort.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740732E9372DF00AB56D0 /* VisibleItem+Sort.swift */; }; - 553742262E9372DF00AB56D0 /* LineNumberTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FBF2E9372DF00AB56D0 /* LineNumberTableCellView.swift */; }; - 553742272E9372DF00AB56D0 /* FilesWindowController+Slider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD62E9372DF00AB56D0 /* FilesWindowController+Slider.swift */; }; - 553742282E9372DF00AB56D0 /* VDDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA82E9372DF00AB56D0 /* VDDocument.swift */; }; - 553742292E9372DF00AB56D0 /* FilesWindowController+Read.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD42E9372DF00AB56D0 /* FilesWindowController+Read.swift */; }; - 5537422A2E9372DF00AB56D0 /* PowerAssertion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741352E9372DF00AB56D0 /* PowerAssertion.swift */; }; - 5537422C2E9372DF00AB56D0 /* FilesWindowController+UISetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD92E9372DF00AB56D0 /* FilesWindowController+UISetup.swift */; }; - 5537422D2E9372DF00AB56D0 /* CompareUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741332E9372DF00AB56D0 /* CompareUtil.swift */; }; - 5537422E2E9372DF00AB56D0 /* DisplayFiltersScopeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FF42E9372DF00AB56D0 /* DisplayFiltersScopeBar.swift */; }; - 5537422F2E9372DF00AB56D0 /* HistorySessionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FAC2E9372DF00AB56D0 /* HistorySessionManager.swift */; }; - 553742302E9372DF00AB56D0 /* FilePanelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FBB2E9372DF00AB56D0 /* FilePanelView.swift */; }; - 553742312E9372DF00AB56D0 /* FilesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC52E9372DF00AB56D0 /* FilesWindowController.swift */; }; - 553742322E9372DF00AB56D0 /* FilesWindowController+MenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FCF2E9372DF00AB56D0 /* FilesWindowController+MenuDelegate.swift */; }; - 553742332E9372DF00AB56D0 /* DifferenceCounters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537416B2E9372DF00AB56D0 /* DifferenceCounters.swift */; }; - 553742342E9372DF00AB56D0 /* ReplaceFileAttributeKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740042E9372DF00AB56D0 /* ReplaceFileAttributeKey.swift */; }; - 553742352E9372DF00AB56D0 /* FoldersWindowController+Select.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740302E9372DF00AB56D0 /* FoldersWindowController+Select.swift */; }; - 553742362E9372DF00AB56D0 /* FoldersWindowController+Comparison.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740212E9372DF00AB56D0 /* FoldersWindowController+Comparison.swift */; }; - 553742372E9372DF00AB56D0 /* PathView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741712E9372DF00AB56D0 /* PathView.swift */; }; - 553742382E9372DF00AB56D0 /* DiffLine+Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FDB2E9372DF00AB56D0 /* DiffLine+Color.swift */; }; - 553742392E9372DF00AB56D0 /* FileDropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741552E9372DF00AB56D0 /* FileDropView.swift */; }; - 5537423A2E9372DF00AB56D0 /* PreferredEditorPopupCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537413D2E9372DF00AB56D0 /* PreferredEditorPopupCell.swift */; }; - 5537423B2E9372DF00AB56D0 /* FilePanelView+File.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FBC2E9372DF00AB56D0 /* FilePanelView+File.swift */; }; - 5537423C2E9372DF00AB56D0 /* KeyboardDocumentBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537409B2E9372DF00AB56D0 /* KeyboardDocumentBox.swift */; }; - 5537423D2E9372DF00AB56D0 /* FoldersOutlineView+VisibleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FEE2E9372DF00AB56D0 /* FoldersOutlineView+VisibleItem.swift */; }; - 5537423E2E9372DF00AB56D0 /* FileNameCaseBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740AE2E9372DF00AB56D0 /* FileNameCaseBox.swift */; }; - 5537423F2E9372DF00AB56D0 /* URL+Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741112E9372DF00AB56D0 /* URL+Metadata.swift */; }; - 553742402E9372DF00AB56D0 /* FoldersWindowController+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740292E9372DF00AB56D0 /* FoldersWindowController+Menu.swift */; }; - 553742412E9372DF00AB56D0 /* OperationSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740192E9372DF00AB56D0 /* OperationSummaryView.swift */; }; - 553742422E9372DF00AB56D0 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F8D2E9372DF00AB56D0 /* AppDelegate.swift */; }; - 553742432E9372DF00AB56D0 /* ExpressionBox+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740AD2E9372DF00AB56D0 /* ExpressionBox+Menu.swift */; }; - 553742442E9372DF00AB56D0 /* FilesWindowController+PathControlDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD32E9372DF00AB56D0 /* FilesWindowController+PathControlDelegate.swift */; }; - 553742452E9372DF00AB56D0 /* VisibleItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537406F2E9372DF00AB56D0 /* VisibleItem.swift */; }; - 553742462E9372DF00AB56D0 /* DiffChangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FDE2E9372DF00AB56D0 /* DiffChangeType.swift */; }; - 553742472E9372DF00AB56D0 /* NotificationCenter+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F942E9372DF00AB56D0 /* NotificationCenter+Helper.swift */; }; - 553742482E9372DF00AB56D0 /* NSImage+Tint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740F52E9372DF00AB56D0 /* NSImage+Tint.swift */; }; - 553742492E9372DF00AB56D0 /* NSButton+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741192E9372DF00AB56D0 /* NSButton+Helper.swift */; }; - 5537424A2E9372DF00AB56D0 /* FoldersWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537401F2E9372DF00AB56D0 /* FoldersWindowController.swift */; }; - 5537424B2E9372DF00AB56D0 /* VisibleItem+Find.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740702E9372DF00AB56D0 /* VisibleItem+Find.swift */; }; - 5537424C2E9372DF00AB56D0 /* String+Highlights.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741042E9372DF00AB56D0 /* String+Highlights.swift */; }; - 5537424D2E9372DF00AB56D0 /* SyncItemsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537400E2E9372DF00AB56D0 /* SyncItemsInfo.swift */; }; - 5537424E2E9372DF00AB56D0 /* FileOperationManager+FileOperationManagerAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537403D2E9372DF00AB56D0 /* FileOperationManager+FileOperationManagerAction.swift */; }; - 5537424F2E9372DF00AB56D0 /* NoodleCustomImageRep.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741302E9372DF00AB56D0 /* NoodleCustomImageRep.swift */; }; - 553742502E9372DF00AB56D0 /* FoldersWindowController+FileSystemControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740262E9372DF00AB56D0 /* FoldersWindowController+FileSystemControllerDelegate.swift */; }; - 553742512E9372DF00AB56D0 /* FilesWindowController+NSWindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD22E9372DF00AB56D0 /* FilesWindowController+NSWindowDelegate.swift */; }; - 553742522E9372DF00AB56D0 /* FilesWindowController+Navigate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FD02E9372DF00AB56D0 /* FilesWindowController+Navigate.swift */; }; - 553742532E9372DF00AB56D0 /* DiffCountersItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741692E9372DF00AB56D0 /* DiffCountersItem.swift */; }; - 553742542E9372DF00AB56D0 /* SaveFileAccessoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC22E9372DF00AB56D0 /* SaveFileAccessoryView.swift */; }; - 553742552E9372DF00AB56D0 /* FileThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FBE2E9372DF00AB56D0 /* FileThumbnailView.swift */; }; - 553742562E9372DF00AB56D0 /* FilesTableView+EditorData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FE12E9372DF00AB56D0 /* FilesTableView+EditorData.swift */; }; - 553742572E9372DF00AB56D0 /* ComparatorPopUpButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741652E9372DF00AB56D0 /* ComparatorPopUpButtonCell.swift */; }; - 553742582E9372DF00AB56D0 /* IntegerFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537412D2E9372DF00AB56D0 /* IntegerFormatter.swift */; }; - 553742592E9372DF00AB56D0 /* SessionDiff+ResolvePath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FB22E9372DF00AB56D0 /* SessionDiff+ResolvePath.swift */; }; - 5537425A2E9372DF00AB56D0 /* CompareItem+NSTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FF12E9372DF00AB56D0 /* CompareItem+NSTableCellView.swift */; }; - 5537425B2E9372DF00AB56D0 /* DocumentError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA52E9372DF00AB56D0 /* DocumentError.swift */; }; - 5537425C2E9372DF00AB56D0 /* FolderSelectionInfo+FileSystemActionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537406A2E9372DF00AB56D0 /* FolderSelectionInfo+FileSystemActionValidator.swift */; }; - 5537425D2E9372DF00AB56D0 /* FoldersWindowController+Exclude.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740252E9372DF00AB56D0 /* FoldersWindowController+Exclude.swift */; }; - 5537425E2E9372DF00AB56D0 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740872E9372DF00AB56D0 /* Preferences.swift */; }; - 5537425F2E9372DF00AB56D0 /* OutlineViewItemDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740342E9372DF00AB56D0 /* OutlineViewItemDelegate.swift */; }; - 553742622E9372DF00AB56D0 /* TableViewContextMenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741432E9372DF00AB56D0 /* TableViewContextMenuDelegate.swift */; }; - 553742632E9372DF00AB56D0 /* NSPredicate+Objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 553740D52E9372DF00AB56D0 /* NSPredicate+Objc.m */; }; - 553742642E9372DF00AB56D0 /* FileSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740172E9372DF00AB56D0 /* FileSummaryView.swift */; }; - 553742652E9372DF00AB56D0 /* CompareItemTableRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FF72E9372DF00AB56D0 /* CompareItemTableRowView.swift */; }; - 553742662E9372DF00AB56D0 /* FolderSelectionInfo+FolderActionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537406C2E9372DF00AB56D0 /* FolderSelectionInfo+FolderActionValidator.swift */; }; - 553742672E9372DF00AB56D0 /* CommonPrefs+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F9C2E9372DF00AB56D0 /* CommonPrefs+Helper.swift */; }; - 553742682E9372DF00AB56D0 /* FilePathTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741672E9372DF00AB56D0 /* FilePathTableCellView.swift */; }; - 553742692E9372DF00AB56D0 /* AttributedMenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741372E9372DF00AB56D0 /* AttributedMenuItem.swift */; }; - 5537426A2E9372DF00AB56D0 /* URL+StructuredContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741142E9372DF00AB56D0 /* URL+StructuredContent.swift */; }; - 5537426B2E9372DF00AB56D0 /* SessionPreferencesComparisonPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740B12E9372DF00AB56D0 /* SessionPreferencesComparisonPanel.swift */; }; - 5537426C2E9372DF00AB56D0 /* FolderCompareInfoWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740762E9372DF00AB56D0 /* FolderCompareInfoWindow.swift */; }; - 5537426D2E9372DF00AB56D0 /* NSToolbarItem+Create.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537410D2E9372DF00AB56D0 /* NSToolbarItem+Create.swift */; }; - 5537426E2E9372DF00AB56D0 /* MoveFileOperationExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FFF2E9372DF00AB56D0 /* MoveFileOperationExecutor.swift */; }; - 5537426F2E9372DF00AB56D0 /* URL+FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537410F2E9372DF00AB56D0 /* URL+FileManager.swift */; }; - 553742712E9372DF00AB56D0 /* ConfirmationsDocumentsBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740892E9372DF00AB56D0 /* ConfirmationsDocumentsBox.swift */; }; - 553742722E9372DF00AB56D0 /* ExpressionBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740AC2E9372DF00AB56D0 /* ExpressionBox.swift */; }; - 553742732E9372DF00AB56D0 /* CompareItem+LeafPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FF02E9372DF00AB56D0 /* CompareItem+LeafPath.swift */; }; - 553742742E9372DF00AB56D0 /* SessionDiff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FAE2E9372DF00AB56D0 /* SessionDiff.swift */; }; - 553742762E9372DF00AB56D0 /* TimeToleranceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741742E9372DF00AB56D0 /* TimeToleranceView.swift */; }; - 553742782E9372DF00AB56D0 /* FoldersOutlineView+Enumerate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FEA2E9372DF00AB56D0 /* FoldersOutlineView+Enumerate.swift */; }; - 553742792E9372DF00AB56D0 /* HistoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537407A2E9372DF00AB56D0 /* HistoryController.swift */; }; - 5537427A2E9372DF00AB56D0 /* ConsoleToolbarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537414F2E9372DF00AB56D0 /* ConsoleToolbarView.swift */; }; - 5537427B2E9372DF00AB56D0 /* HistorySearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537407D2E9372DF00AB56D0 /* HistorySearchField.swift */; }; - 5537427C2E9372DF00AB56D0 /* URL+ResourceFork.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741132E9372DF00AB56D0 /* URL+ResourceFork.swift */; }; - 5537427D2E9372DF00AB56D0 /* LinkButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537414C2E9372DF00AB56D0 /* LinkButton.swift */; }; - 5537427E2E9372DF00AB56D0 /* ReplaceInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537400A2E9372DF00AB56D0 /* ReplaceInfoView.swift */; }; - 5537427F2E9372DF00AB56D0 /* NSAlert+DirtyFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC12E9372DF00AB56D0 /* NSAlert+DirtyFiles.swift */; }; - 553742802E9372DF00AB56D0 /* CommonPrefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F982E9372DF00AB56D0 /* CommonPrefs.swift */; }; - 553742812E9372DF00AB56D0 /* FilesWindowController+LinesDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FCD2E9372DF00AB56D0 /* FilesWindowController+LinesDetail.swift */; }; - 553742822E9372DF00AB56D0 /* FileOperationManager+Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537403E2E9372DF00AB56D0 /* FileOperationManager+Util.swift */; }; - 553742832E9372DF00AB56D0 /* ConsoleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741502E9372DF00AB56D0 /* ConsoleView.swift */; }; - 553742842E9372DF00AB56D0 /* CommonPrefs+DifferenceNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740592E9372DF00AB56D0 /* CommonPrefs+DifferenceNavigator.swift */; }; - 553742852E9372DF00AB56D0 /* FileOperationExecutor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740162E9372DF00AB56D0 /* FileOperationExecutor.swift */; }; - 553742862E9372DF00AB56D0 /* AlignmentPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740A92E9372DF00AB56D0 /* AlignmentPanel.swift */; }; - 553742872E9372DF00AB56D0 /* FoldersOutlineView+ExternalApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FEB2E9372DF00AB56D0 /* FoldersOutlineView+ExternalApp.swift */; }; - 553742882E9372DF00AB56D0 /* PreferencesPanelDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740B92E9372DF00AB56D0 /* PreferencesPanelDataSource.swift */; }; - 553742892E9372DF00AB56D0 /* DiffCountersTextFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537416A2E9372DF00AB56D0 /* DiffCountersTextFieldCell.swift */; }; - 5537428A2E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740272E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineView.swift */; }; - 5537428B2E9372DF00AB56D0 /* TableViewCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741422E9372DF00AB56D0 /* TableViewCommon.swift */; }; - 5537428C2E9372DF00AB56D0 /* CompareItem+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740562E9372DF00AB56D0 /* CompareItem+Path.swift */; }; - 5537428D2E9372DF00AB56D0 /* FolderPreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537408F2E9372DF00AB56D0 /* FolderPreferencesPanel.swift */; }; - 5537428E2E9372DF00AB56D0 /* FileOperationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537403C2E9372DF00AB56D0 /* FileOperationManager.swift */; }; - 5537428F2E9372DF00AB56D0 /* StandardUserPreferencesBoxDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740842E9372DF00AB56D0 /* StandardUserPreferencesBoxDataSource.swift */; }; - 553742902E9372DF00AB56D0 /* SecureBookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741262E9372DF00AB56D0 /* SecureBookmark.swift */; }; - 553742912E9372DF00AB56D0 /* FilesScopeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FBD2E9372DF00AB56D0 /* FilesScopeBar.swift */; }; - 553742922E9372DF00AB56D0 /* OpenEditorAttribute.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537413A2E9372DF00AB56D0 /* OpenEditorAttribute.swift */; }; - 553742932E9372DF00AB56D0 /* WindowCancelOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537415F2E9372DF00AB56D0 /* WindowCancelOperation.swift */; }; - 553742942E9372DF00AB56D0 /* VisibleItem+QLPreviewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740722E9372DF00AB56D0 /* VisibleItem+QLPreviewItem.swift */; }; - 553742952E9372DF00AB56D0 /* FilesWindowController+FilesTableViewContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FCB2E9372DF00AB56D0 /* FilesWindowController+FilesTableViewContextMenu.swift */; }; - 553742962E9372DF00AB56D0 /* FolderSelectionInfo+CompareActionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740692E9372DF00AB56D0 /* FolderSelectionInfo+CompareActionValidator.swift */; }; - 553742982E9372DF00AB56D0 /* DiffCountersItem+DiffResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FE02E9372DF00AB56D0 /* DiffCountersItem+DiffResult.swift */; }; - 553742992E9372DF00AB56D0 /* FileOperationManagerDelegateImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740082E9372DF00AB56D0 /* FileOperationManagerDelegateImpl.swift */; }; - 5537429A2E9372DF00AB56D0 /* SessionDiff+AlignRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FAF2E9372DF00AB56D0 /* SessionDiff+AlignRule.swift */; }; - 5537429B2E9372DF00AB56D0 /* FoldersWindowController+MenuDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537402A2E9372DF00AB56D0 /* FoldersWindowController+MenuDelegate.swift */; }; - 5537429D2E9372DF00AB56D0 /* FolderSelectionInfo+FilterActionValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537406B2E9372DF00AB56D0 /* FolderSelectionInfo+FilterActionValidator.swift */; }; - 5537429E2E9372DF00AB56D0 /* OpenDiffCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740E22E9372DF00AB56D0 /* OpenDiffCommand.swift */; }; - 5537429F2E9372DF00AB56D0 /* RenameCompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740432E9372DF00AB56D0 /* RenameCompareItem.swift */; }; - 553742A02E9372DF00AB56D0 /* DisplayOptions+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537404C2E9372DF00AB56D0 /* DisplayOptions+Helper.swift */; }; - 553742A12E9372DF00AB56D0 /* SessionPreferencesWindow+Data.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740A72E9372DF00AB56D0 /* SessionPreferencesWindow+Data.swift */; }; - 553742A22E9372DF00AB56D0 /* FolderReader+Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537405F2E9372DF00AB56D0 /* FolderReader+Log.swift */; }; - 553742A32E9372DF00AB56D0 /* NSArrayController+Paths.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA02E9372DF00AB56D0 /* NSArrayController+Paths.swift */; }; - 553742A42E9372DF00AB56D0 /* NSPredicateEditor+Fix.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741012E9372DF00AB56D0 /* NSPredicateEditor+Fix.swift */; }; - 553742A52E9372DF00AB56D0 /* FoldersWindowController+NSToolbarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537402C2E9372DF00AB56D0 /* FoldersWindowController+NSToolbarDelegate.swift */; }; - 553742A62E9372DF00AB56D0 /* NSAlert+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740FB2E9372DF00AB56D0 /* NSAlert+Helper.swift */; }; - 553742A72E9372DF00AB56D0 /* FoldersWindowController+NSWindowDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537402D2E9372DF00AB56D0 /* FoldersWindowController+NSWindowDelegate.swift */; }; - 553742A82E9372DF00AB56D0 /* FilesWindowController+FilesScopeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FCA2E9372DF00AB56D0 /* FilesWindowController+FilesScopeBar.swift */; }; - 553742A92E9372DF00AB56D0 /* NSManagedObjectContext+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740EB2E9372DF00AB56D0 /* NSManagedObjectContext+Helpers.swift */; }; - 553742AA2E9372DF00AB56D0 /* String+Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741062E9372DF00AB56D0 /* String+Path.swift */; }; - 553742AB2E9372DF00AB56D0 /* TimeInterval+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740ED2E9372DF00AB56D0 /* TimeInterval+Helper.swift */; }; - 553742AC2E9372DF00AB56D0 /* ColorScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F902E9372DF00AB56D0 /* ColorScheme.swift */; }; - 553742AD2E9372DF00AB56D0 /* FilesWindowController+Document.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FC82E9372DF00AB56D0 /* FilesWindowController+Document.swift */; }; - 553742AE2E9372DF00AB56D0 /* PathControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741702E9372DF00AB56D0 /* PathControl.swift */; }; - 553742AF2E9372DF00AB56D0 /* TouchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740122E9372DF00AB56D0 /* TouchController.swift */; }; - 553742B02E9372DF00AB56D0 /* CompletionIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740062E9372DF00AB56D0 /* CompletionIndicator.swift */; }; - 553742B12E9372DF00AB56D0 /* FoldersWindowController+UISetup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740332E9372DF00AB56D0 /* FoldersWindowController+UISetup.swift */; }; - 553742B22E9372DF00AB56D0 /* NSTableView+Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741092E9372DF00AB56D0 /* NSTableView+Font.swift */; }; - 553742B32E9372DF00AB56D0 /* ActionBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537414A2E9372DF00AB56D0 /* ActionBarView.swift */; }; - 553742B42E9372DF00AB56D0 /* TouchCompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740442E9372DF00AB56D0 /* TouchCompareItem.swift */; }; - 553742B52E9372DF00AB56D0 /* HistoryFetchedResultsControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537407C2E9372DF00AB56D0 /* HistoryFetchedResultsControllerDelegate.swift */; }; - 553742B62E9372DF00AB56D0 /* CommonPrefs+Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373F9B2E9372DF00AB56D0 /* CommonPrefs+Font.swift */; }; - 553742B72E9372DF00AB56D0 /* NSWorkspace+Finder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741232E9372DF00AB56D0 /* NSWorkspace+Finder.swift */; }; - 553742B82E9372DF00AB56D0 /* FoldersOutlineViewFindTextDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FEF2E9372DF00AB56D0 /* FoldersOutlineViewFindTextDelegate.swift */; }; - 553742B92E9372DF00AB56D0 /* AlignTestResultBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740AB2E9372DF00AB56D0 /* AlignTestResultBox.swift */; }; - 553742BA2E9372DF00AB56D0 /* VisibleItem+Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740712E9372DF00AB56D0 /* VisibleItem+Log.swift */; }; - 553742BB2E9372DF00AB56D0 /* HistoryTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537407E2E9372DF00AB56D0 /* HistoryTableView.swift */; }; - 553742BC2E9372DF00AB56D0 /* SessionDiff+NSSortDescriptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FB12E9372DF00AB56D0 /* SessionDiff+NSSortDescriptor.swift */; }; - 553742BD2E9372DF00AB56D0 /* NSPopUpButton+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537411A2E9372DF00AB56D0 /* NSPopUpButton+Helper.swift */; }; - 553742BE2E9372DF00AB56D0 /* NSColor+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740E92E9372DF00AB56D0 /* NSColor+Hex.swift */; }; - 553742BF2E9372DF00AB56D0 /* FileInfoBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FBA2E9372DF00AB56D0 /* FileInfoBar.swift */; }; - 553742C02E9372DF00AB56D0 /* TitlePredicateEditorRowTemplate.m in Sources */ = {isa = PBXBuildFile; fileRef = 553740D72E9372DF00AB56D0 /* TitlePredicateEditorRowTemplate.m */; }; - 553742C12E9372DF00AB56D0 /* ErrorsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740072E9372DF00AB56D0 /* ErrorsView.swift */; }; - 553742C22E9372DF00AB56D0 /* IconUtils+CompareItemIconUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FF22E9372DF00AB56D0 /* IconUtils+CompareItemIconUtils.swift */; }; - 553742C32E9372DF00AB56D0 /* SessionTypeError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FA72E9372DF00AB56D0 /* SessionTypeError.swift */; }; - 553742C42E9372DF00AB56D0 /* FoldersOutlineView+FileCount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FEC2E9372DF00AB56D0 /* FoldersOutlineView+FileCount.swift */; }; - 553742C52E9372DF00AB56D0 /* SessionPreferencesFiltersPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740B52E9372DF00AB56D0 /* SessionPreferencesFiltersPanel.swift */; }; - 553742C62E9372DF00AB56D0 /* FoldersWindowController+DisplayFiltersScopeBarDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740232E9372DF00AB56D0 /* FoldersWindowController+DisplayFiltersScopeBarDelegate.swift */; }; - 553742C72E9372DF00AB56D0 /* DiffCountersItem+CompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740372E9372DF00AB56D0 /* DiffCountersItem+CompareItem.swift */; }; - 553742C82E9372DF00AB56D0 /* DisplayFiltersPopUpButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741662E9372DF00AB56D0 /* DisplayFiltersPopUpButtonCell.swift */; }; - 553742C92E9372DF00AB56D0 /* NSEvent+VirtualKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537412A2E9372DF00AB56D0 /* NSEvent+VirtualKeys.swift */; }; - 553742CA2E9372DF00AB56D0 /* DescriptionOutlineNode+FolderCompareInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740752E9372DF00AB56D0 /* DescriptionOutlineNode+FolderCompareInfo.swift */; }; - 553742CB2E9372DF00AB56D0 /* WindowOSD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553741602E9372DF00AB56D0 /* WindowOSD.swift */; }; - 553742CC2E9372DF00AB56D0 /* NSPasteboard+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740FF2E9372DF00AB56D0 /* NSPasteboard+Helper.swift */; }; - 553742CD2E9372DF00AB56D0 /* ConfirmationsPreferencesPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537408C2E9372DF00AB56D0 /* ConfirmationsPreferencesPanel.swift */; }; - 553742CE2E9372DF00AB56D0 /* CompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537404F2E9372DF00AB56D0 /* CompareItem.swift */; }; - 553742CF2E9372DF00AB56D0 /* NSTextField+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537411E2E9372DF00AB56D0 /* NSTextField+Helper.swift */; }; - 553742D02E9372DF00AB56D0 /* FolderViewBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740912E9372DF00AB56D0 /* FolderViewBox.swift */; }; - 553742D12E9372DF00AB56D0 /* SessionDiff+Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55373FB32E9372DF00AB56D0 /* SessionDiff+Types.swift */; }; - 553742D22E9372DF00AB56D0 /* FiltersPredicateEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 553740B32E9372DF00AB56D0 /* FiltersPredicateEditor.swift */; }; - 553742D32E9372DF00AB56D0 /* HistoryEntityTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5537407B2E9372DF00AB56D0 /* HistoryEntityTableCellView.swift */; }; 5537434E2E937B7F00AB56D0 /* VDDefaults.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5537434C2E937B7F00AB56D0 /* VDDefaults.plist */; }; 5537434F2E937B7F00AB56D0 /* colors.json in Resources */ = {isa = PBXBuildFile; fileRef = 5537434A2E937B7F00AB56D0 /* colors.json */; }; 553743502E937B7F00AB56D0 /* colorsDark.json in Resources */ = {isa = PBXBuildFile; fileRef = 5537434B2E937B7F00AB56D0 /* colorsDark.json */; }; @@ -355,114 +18,23 @@ 5541CE482FFCD8DA0044A569 /* mask-back-white.png in Resources */ = {isa = PBXBuildFile; fileRef = 5541CE402FFCD8DA0044A569 /* mask-back-white.png */; }; 5541CE492FFCD8DA0044A569 /* mask-back.png in Resources */ = {isa = PBXBuildFile; fileRef = 5541CE3F2FFCD8DA0044A569 /* mask-back.png */; }; 5541CE4B2FFCD9E70044A569 /* vd.iconset in Resources */ = {isa = PBXBuildFile; fileRef = 5541CE4A2FFCD9E70044A569 /* vd.iconset */; }; - 55450B172ED4790B0048619F /* TextPreferencesPanel+PreferencesBoxDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55450B162ED4790B0048619F /* TextPreferencesPanel+PreferencesBoxDataSource.swift */; }; - 55450B192ED47B700048619F /* FileSessionPreferencesWindow+PreferencesBoxDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55450B182ED47B680048619F /* FileSessionPreferencesWindow+PreferencesBoxDataSource.swift */; }; - 554750C62F5C0A1100C1747A /* CommonAncestorPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554750C52F5C0A0A00C1747A /* CommonAncestorPathTests.swift */; }; - 5548ABA82F7FE2BB0064B964 /* LineDetailTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5548ABA72F7FE2BB0064B964 /* LineDetailTextView.swift */; }; - 554B4A452E9B7B8F0023AAF0 /* EndOfLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A3A2E9B7B8F0023AAF0 /* EndOfLine.swift */; }; - 554B4A462E9B7B8F0023AAF0 /* DiffSide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A402E9B7B8F0023AAF0 /* DiffSide.swift */; }; - 554B4A472E9B7B8F0023AAF0 /* DiffSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A412E9B7B8F0023AAF0 /* DiffSection.swift */; }; - 554B4A482E9B7B8F0023AAF0 /* DiffResult+Dump.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A3D2E9B7B8F0023AAF0 /* DiffResult+Dump.swift */; }; - 554B4A4A2E9B7B8F0023AAF0 /* DiffResult+Section.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A3F2E9B7B8F0023AAF0 /* DiffResult+Section.swift */; }; - 554B4A4B2E9B7B8F0023AAF0 /* DiffResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A3B2E9B7B8F0023AAF0 /* DiffResult.swift */; }; - 554B4A4D2E9B7B8F0023AAF0 /* DiffResult+Lines.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A3E2E9B7B8F0023AAF0 /* DiffResult+Lines.swift */; }; - 554B4A4E2E9B7B8F0023AAF0 /* DiffLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 554B4A432E9B7B8F0023AAF0 /* DiffLine.swift */; }; 5550B5931A21CD1F009BC4A4 /* aliasbadge@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5550B5921A21CD1F009BC4A4 /* aliasbadge@2x.png */; }; 5550B5951A21D2F3009BC4A4 /* empty@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 5550B5941A21D2F3009BC4A4 /* empty@2x.png */; }; 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 */; }; 557A47F22FF7DE3800D0F6B4 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 557A47F12FF7DE3800D0F6B4 /* Localizable.xcstrings */; }; - 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 */; }; - 557C18242E7E8C0D00381A3A /* Versions.local.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 557C18222E7E8C0D00381A3A /* Versions.local.xcconfig */; }; - 557C18252E7E8C0D00381A3A /* AppConfig.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 557C18202E7E8C0D00381A3A /* AppConfig.xcconfig */; }; - 55858304300DD8B100EDC56C /* DiffResult.Options+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55858303300DD8B100EDC56C /* DiffResult.Options+Description.swift */; }; - 55858306300DE12E00EDC56C /* ScopeBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55858305300DE12E00EDC56C /* ScopeBarView.swift */; }; - 558DEAC62EA263DE00F05A10 /* CFStringEncoding+StringEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DEAC42EA263DE00F05A10 /* CFStringEncoding+StringEncoding.swift */; }; - 558DEACB2EA263F500F05A10 /* EncodingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DEAC72EA263F500F05A10 /* EncodingManager.swift */; }; - 558DEACC2EA263F500F05A10 /* SelectEncodingsPanel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DEAC92EA263F500F05A10 /* SelectEncodingsPanel.swift */; }; - 558DEACD2EA263F500F05A10 /* EncodingPopUpButtonCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DEAC82EA263F500F05A10 /* EncodingPopUpButtonCell.swift */; }; - 558DEAD02EA2642100F05A10 /* NSWindow+Editing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DEACE2EA2642100F05A10 /* NSWindow+Editing.swift */; }; - 558DEAD22EA26C5B00F05A10 /* Logger+App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 558DEAD12EA26C5800F05A10 /* Logger+App.swift */; }; + 557EBD89305AC79D00A9BBD2 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 557EBD88305AC79D00A9BBD2 /* Cocoa.framework */; }; 55916A102E8A814200ED6629 /* MyDocument.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 55916A092E8A814200ED6629 /* MyDocument.xcdatamodeld */; }; 55916A112E8A814200ED6629 /* History.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 55916A082E8A814200ED6629 /* History.xcdatamodeld */; }; - 559186412E9B821B00A30620 /* DiffSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559186402E9B821B00A30620 /* DiffSummary.swift */; }; - 5595901D2EABA7FE00B5AB0B /* CompareSummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5595901C2EABA7FE00B5AB0B /* CompareSummary.swift */; }; - 5595901F2EABA8A100B5AB0B /* CompareChangeType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5595901E2EABA8A100B5AB0B /* CompareChangeType.swift */; }; - 559590212EABAFC200B5AB0B /* SessionDiff+ItemComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559590202EABAFC200B5AB0B /* SessionDiff+ItemComparator.swift */; }; - 559590262EABB06100B5AB0B /* ItemComparator+AlignRegularExpression.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559590242EABB06100B5AB0B /* ItemComparator+AlignRegularExpression.swift */; }; - 559590272EABB06100B5AB0B /* ItemComparator+Align.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559590232EABB06100B5AB0B /* ItemComparator+Align.swift */; }; - 559590282EABB06100B5AB0B /* ItemComparator+Compare.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559590252EABB06100B5AB0B /* ItemComparator+Compare.swift */; }; - 559590292EABB06100B5AB0B /* ItemComparator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 559590222EABB06100B5AB0B /* ItemComparator.swift */; }; - 5595902B2EABB15F00B5AB0B /* MockItemComparatorDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5595902A2EABB15F00B5AB0B /* MockItemComparatorDelegate.swift */; }; 559614B114B0471E0075ED34 /* aliasbadge.png in Resources */ = {isa = PBXBuildFile; fileRef = 559614B014B0471E0075ED34 /* aliasbadge.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 */; }; - 55B1CE322E9934F800426FEC /* DiffResultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B1CE2C2E9934F800426FEC /* DiffResultTests.swift */; }; - 55B93A452FAC42950024AEC0 /* DiffProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B93A442FAC42910024AEC0 /* DiffProcessor.swift */; }; - 55B9B73E2F5C26F800DB8326 /* BaseTests+CompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55B9B73D2F5C26F800DB8326 /* BaseTests+CompareItem.swift */; }; - 55BC333E2EDAC69C004035A0 /* RowHeightCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC333D2EDAC69C004035A0 /* RowHeightCalculator.swift */; }; - 55BC33402EDAC96F004035A0 /* TiledImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC333F2EDAC96F004035A0 /* TiledImageView.swift */; }; - 55BC33432EDACB09004035A0 /* FilesWindowController+RowHeightDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC33412EDACB09004035A0 /* FilesWindowController+RowHeightDataSource.swift */; }; - 55BC33442EDACB09004035A0 /* FilesWindowController+SplitViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BC33422EDACB09004035A0 /* FilesWindowController+SplitViewDelegate.swift */; }; - 55BCFE472F627DDF00E704D8 /* SessionPopupButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55BCFE462F627DDF00E704D8 /* SessionPopupButton.swift */; }; 55C165622EE444050082A24C /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 55C165612EE444050082A24C /* Sparkle */; }; - 55C1CBFD2E8BB6AD004FE322 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1CBF42E8BB6AD004FE322 /* main.swift */; }; 55C1CC012E8BB755004FE322 /* visdiff in Copy Visdiff to Helpers */ = {isa = PBXBuildFile; fileRef = 55C1CBE22E8BB6A2004FE322 /* visdiff */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 55C1CC072E8BBD85004FE322 /* DocumentWaiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55C1CC062E8BBAD1004FE322 /* DocumentWaiter.swift */; }; 55C84A712DA93ADB00BA39DA /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55C84A702DA93ADB00BA39DA /* UserNotifications.framework */; }; - 55CE5BC62FFCBC8500B0F3F5 /* VDSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55CE5BC52FFCBC8500B0F3F5 /* VDSymbol.swift */; }; - 55D352E72EFD0C5B00C944D5 /* FileSystemTestHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D352E62EFD0C4D00C944D5 /* FileSystemTestHelper.swift */; }; - 55D71FD62EE584B7007E0FA0 /* AppUpdater.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D71FD52EE584B7007E0FA0 /* AppUpdater.swift */; }; - 55D8812E2F3DA332008DE497 /* CopyFinderMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55D8812D2F3DA329008DE497 /* CopyFinderMetadata.swift */; }; - 55DF3DD22E924BB10044CC0C /* BaseTests+FileSystemOperations.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC52E924BB10044CC0C /* BaseTests+FileSystemOperations.swift */; }; - 55DF3DD32E924BB10044CC0C /* FiltersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DBF2E924BB10044CC0C /* FiltersTests.swift */; }; - 55DF3DD42E924BB10044CC0C /* LeafPathTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC82E924BB10044CC0C /* LeafPathTests.swift */; }; - 55DF3DD52E924BB10044CC0C /* DeleteFileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DBC2E924BB10044CC0C /* DeleteFileTests.swift */; }; - 55DF3DD62E924BB10044CC0C /* TagsCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB32E924BB10044CC0C /* TagsCopyTests.swift */; }; - 55DF3DD82E924BB10044CC0C /* CopyFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DBB2E924BB10044CC0C /* CopyFilesTests.swift */; }; - 55DF3DD92E924BB10044CC0C /* StringUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DCE2E924BB10044CC0C /* StringUtils.swift */; }; - 55DF3DDB2E924BB10044CC0C /* BufferedInputStreamTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB92E924BB10044CC0C /* BufferedInputStreamTests.swift */; }; - 55DF3DDC2E924BB10044CC0C /* TagsMoveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB62E924BB10044CC0C /* TagsMoveTests.swift */; }; - 55DF3DDD2E924BB10044CC0C /* CaseSensitiveBaseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC62E924BB10044CC0C /* CaseSensitiveBaseTest.swift */; }; - 55DF3DDE2E924BB10044CC0C /* BaseTests+AssertCompareItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DAA2E924BB10044CC0C /* BaseTests+AssertCompareItem.swift */; }; - 55DF3DDF2E924BB10044CC0C /* AlignmentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB82E924BB10044CC0C /* AlignmentTests.swift */; }; - 55DF3DE02E924BB10044CC0C /* RefreshInfoTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DCB2E924BB10044CC0C /* RefreshInfoTests.swift */; }; - 55DF3DE12E924BB10044CC0C /* EmptyFolderColorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DBE2E924BB10044CC0C /* EmptyFolderColorTests.swift */; }; - 55DF3DE22E924BB10044CC0C /* BaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DA92E924BB10044CC0C /* BaseTests.swift */; }; - 55DF3DE32E924BB10044CC0C /* FoldersWindowControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DCA2E924BB10044CC0C /* FoldersWindowControllerTests.swift */; }; - 55DF3DE42E924BB10044CC0C /* TagsDeleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB52E924BB10044CC0C /* TagsDeleteTests.swift */; }; - 55DF3DE52E924BB10044CC0C /* ComparatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DBA2E924BB10044CC0C /* ComparatorTests.swift */; }; - 55DF3DE62E924BB10044CC0C /* TouchFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC22E924BB10044CC0C /* TouchFilesTests.swift */; }; - 55DF3DE72E924BB10044CC0C /* DisplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DBD2E924BB10044CC0C /* DisplayTests.swift */; }; - 55DF3DE82E924BB10044CC0C /* BaseTests+AssertFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC42E924BB10044CC0C /* BaseTests+AssertFileSystem.swift */; }; - 55DF3DE92E924BB10044CC0C /* LabelsCreateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB12E924BB10044CC0C /* LabelsCreateTests.swift */; }; - 55DF3DEA2E924BB10044CC0C /* MockFileOperationManagerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DAC2E924BB10044CC0C /* MockFileOperationManagerDelegate.swift */; }; - 55DF3DEB2E924BB10044CC0C /* LabelsMoveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB22E924BB10044CC0C /* LabelsMoveTests.swift */; }; - 55DF3DEC2E924BB10044CC0C /* MoveFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC02E924BB10044CC0C /* MoveFilesTests.swift */; }; - 55DF3DED2E924BB10044CC0C /* TagsCreateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB42E924BB10044CC0C /* TagsCreateTests.swift */; }; - 55DF3DEE2E924BB10044CC0C /* LabelsCopyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DB02E924BB10044CC0C /* LabelsCopyTests.swift */; }; - 55DF3DEF2E924BB10044CC0C /* MockFolderReaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DAD2E924BB10044CC0C /* MockFolderReaderDelegate.swift */; }; - 55DF3DF02E924BB10044CC0C /* RenameFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55DF3DC12E924BB10044CC0C /* RenameFilesTests.swift */; }; 55E1029A12C9E17300432C61 /* empty.png in Resources */ = {isa = PBXBuildFile; fileRef = 55E1029912C9E17300432C61 /* empty.png */; }; - 55E381C12EEFFD0500CB2F00 /* SymbolicLinkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E381C02EEFFCF500CB2F00 /* SymbolicLinkTests.swift */; }; 55E3A5661826552A002A890B /* lockedbadge.png in Resources */ = {isa = PBXBuildFile; fileRef = 55E3A5641826552A002A890B /* lockedbadge.png */; }; 55E3A5671826552A002A890B /* lockedbadge@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 55E3A5651826552A002A890B /* lockedbadge@2x.png */; }; - 55EC0A4B2F3868E4008201E1 /* CopyFilesTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55EC0A4A2F3868E4008201E1 /* CopyFilesTag.swift */; }; - 55F42EE12F76798A009B211C /* CommonPrefs+FileDifferenceNavigator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F42EDF2F76798A009B211C /* CommonPrefs+FileDifferenceNavigator.swift */; }; - 55F4A0032FEE100000A1B2C3 /* ExternalFileOperationContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F4A0012FEE100000A1B2C3 /* ExternalFileOperationContext.swift */; }; - 55F9BAF22FA7259B004EA543 /* FoldersWindowController+Observers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F9BAF12FA7259B004EA543 /* FoldersWindowController+Observers.swift */; }; - 55F9E79A2EDD5DBD001218C7 /* DiffResult.Options.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F9E7992EDD5DAD001218C7 /* DiffResult.Options.swift */; }; - 55F9E79C2EDD6221001218C7 /* WhitespacesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55F9E79B2EDD6221001218C7 /* WhitespacesTests.swift */; }; - 55FE894A2F054B260003864A /* FlagSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55FE89492F054B260003864A /* FlagSet.swift */; }; - 775DFF38067A968500C5B868 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A7FEA54F5311CA2CBB /* Cocoa.framework */; }; 8D15AC2C0486D014006FF6A4 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 2A37F4B9FDCFA73011CA2CEA /* Credits.rtf */; }; /* End PBXBuildFile section */ @@ -486,387 +58,24 @@ /* Begin PBXCopyFilesBuildPhase section */ 5504B6FF255FCBBA00CDE04A /* Copy Visdiff to Helpers */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; dstPath = Contents/Helpers; - dstSubfolderSpec = 1; + dstSubfolder = Wrapper; files = ( 55C1CC012E8BB755004FE322 /* visdiff in Copy Visdiff to Helpers */, ); name = "Copy Visdiff to Helpers"; - runOnlyForDeploymentPostprocessing = 0; }; 55943C852EE43E3800DE513B /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 12; dstPath = ""; - dstSubfolderSpec = 10; + dstSubfolder = Frameworks; files = ( ); name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - 55C1CBE02E8BB6A2004FE322 /* CopyFiles */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = /usr/share/man/man1/; - dstSubfolderSpec = 0; - files = ( - ); - runOnlyForDeploymentPostprocessing = 1; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 1058C7A7FEA54F5311CA2CBB /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = /System/Library/Frameworks/Cocoa.framework; sourceTree = ""; }; - 2A37F4C4FDCFA73011CA2CEA /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = /System/Library/Frameworks/AppKit.framework; sourceTree = ""; }; - 2A37F4C5FDCFA73011CA2CEA /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = /System/Library/Frameworks/Foundation.framework; sourceTree = ""; }; - 32DBCF750370BD2300C91783 /* VisualDiffer_Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VisualDiffer_Prefix.pch; sourceTree = ""; }; - 550A2CC113FF973B00981A9B /* ApplicationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ApplicationServices.framework; path = System/Library/Frameworks/ApplicationServices.framework; sourceTree = SDKROOT; }; - 551DE39E2ED422820067AB18 /* FilesWindowController+SessionPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+SessionPreferences.swift"; sourceTree = ""; }; - 551DE3A42ED4237B0067AB18 /* FileSessionPreferencesWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSessionPreferencesWindow.swift; sourceTree = ""; }; - 551DE3A92ED428A50067AB18 /* FilePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreferences.swift; sourceTree = ""; }; - 551DE3B32ED42AD00067AB18 /* FilePreferencesComparisonPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePreferencesComparisonPanel.swift; sourceTree = ""; }; - 551DE3B52ED42B4E0067AB18 /* FileComparisonBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileComparisonBox.swift; sourceTree = ""; }; - 551DE3B72ED4399B0067AB18 /* PreferencesBoxDataSource+Default.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PreferencesBoxDataSource+Default.swift"; sourceTree = ""; }; - 552430402F7673F800951F1A /* FileDifferenceNavigatorBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDifferenceNavigatorBox.swift; sourceTree = ""; }; - 552B85072F5FE29400A9268A /* VDDocumentController+BlankDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VDDocumentController+BlankDocument.swift"; sourceTree = ""; }; - 553033AC2E5DBABF0016F101 /* VisualDiffer.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = VisualDiffer.xctestplan; sourceTree = SOURCE_ROOT; }; - 5536ADB92EF169820019EFBF /* SessionDiff+ExtraData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionDiff+ExtraData.swift"; sourceTree = ""; }; - 55373F8D2E9372DF00AB56D0 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 55373F8F2E9372DF00AB56D0 /* AlignRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignRule.swift; sourceTree = ""; }; - 55373F902E9372DF00AB56D0 /* ColorScheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorScheme.swift; sourceTree = ""; }; - 55373F912E9372DF00AB56D0 /* DisplayPositionable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayPositionable.swift; sourceTree = ""; }; - 55373F932E9372DF00AB56D0 /* KeyEquivalent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyEquivalent.swift; sourceTree = ""; }; - 55373F942E9372DF00AB56D0 /* NotificationCenter+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NotificationCenter+Helper.swift"; sourceTree = ""; }; - 55373F962E9372DF00AB56D0 /* ViewLinkable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewLinkable.swift; sourceTree = ""; }; - 55373F982E9372DF00AB56D0 /* CommonPrefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonPrefs.swift; sourceTree = ""; }; - 55373F992E9372DF00AB56D0 /* CommonPrefs+FileCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+FileCompare.swift"; sourceTree = ""; }; - 55373F9A2E9372DF00AB56D0 /* CommonPrefs+FolderCompare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+FolderCompare.swift"; sourceTree = ""; }; - 55373F9B2E9372DF00AB56D0 /* CommonPrefs+Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+Font.swift"; sourceTree = ""; }; - 55373F9C2E9372DF00AB56D0 /* CommonPrefs+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+Helper.swift"; sourceTree = ""; }; - 55373F9D2E9372DF00AB56D0 /* CommonPrefs+Name.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+Name.swift"; sourceTree = ""; }; - 55373F9F2E9372DF00AB56D0 /* DocumentWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentWindow.swift; sourceTree = ""; }; - 55373FA02E9372DF00AB56D0 /* NSArrayController+Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSArrayController+Paths.swift"; sourceTree = ""; }; - 55373FA12E9372DF00AB56D0 /* PathChooser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathChooser.swift; sourceTree = ""; }; - 55373FA22E9372DF00AB56D0 /* RecentDocumentPopupMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentDocumentPopupMenu.swift; sourceTree = ""; }; - 55373FA42E9372DF00AB56D0 /* DiffOpenerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffOpenerDelegate.swift; sourceTree = ""; }; - 55373FA52E9372DF00AB56D0 /* DocumentError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentError.swift; sourceTree = ""; }; - 55373FA62E9372DF00AB56D0 /* SessionDiff.ItemType+Paths.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionDiff.ItemType+Paths.swift"; sourceTree = ""; }; - 55373FA72E9372DF00AB56D0 /* SessionTypeError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionTypeError.swift; sourceTree = ""; }; - 55373FA82E9372DF00AB56D0 /* VDDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDDocument.swift; sourceTree = ""; }; - 55373FA92E9372DF00AB56D0 /* VDDocumentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDDocumentController.swift; sourceTree = ""; }; - 55373FAB2E9372DF00AB56D0 /* HistoryEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEntity.swift; sourceTree = ""; }; - 55373FAC2E9372DF00AB56D0 /* HistorySessionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySessionManager.swift; sourceTree = ""; }; - 55373FAE2E9372DF00AB56D0 /* SessionDiff.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionDiff.swift; sourceTree = ""; }; - 55373FAF2E9372DF00AB56D0 /* SessionDiff+AlignRule.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionDiff+AlignRule.swift"; sourceTree = ""; }; - 55373FB12E9372DF00AB56D0 /* SessionDiff+NSSortDescriptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionDiff+NSSortDescriptor.swift"; sourceTree = ""; }; - 55373FB22E9372DF00AB56D0 /* SessionDiff+ResolvePath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionDiff+ResolvePath.swift"; sourceTree = ""; }; - 55373FB32E9372DF00AB56D0 /* SessionDiff+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionDiff+Types.swift"; sourceTree = ""; }; - 55373FB62E9372DF00AB56D0 /* FilesTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesTableView.swift; sourceTree = ""; }; - 55373FB72E9372DF00AB56D0 /* FilesTableView+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesTableView+Menu.swift"; sourceTree = ""; }; - 55373FB82E9372DF00AB56D0 /* FilesTableViewFindTextDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesTableViewFindTextDelegate.swift; sourceTree = ""; }; - 55373FBA2E9372DF00AB56D0 /* FileInfoBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileInfoBar.swift; sourceTree = ""; }; - 55373FBB2E9372DF00AB56D0 /* FilePanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePanelView.swift; sourceTree = ""; }; - 55373FBC2E9372DF00AB56D0 /* FilePanelView+File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilePanelView+File.swift"; sourceTree = ""; }; - 55373FBD2E9372DF00AB56D0 /* FilesScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesScopeBar.swift; sourceTree = ""; }; - 55373FBE2E9372DF00AB56D0 /* FileThumbnailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileThumbnailView.swift; sourceTree = ""; }; - 55373FBF2E9372DF00AB56D0 /* LineNumberTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineNumberTableCellView.swift; sourceTree = ""; }; - 55373FC02E9372DF00AB56D0 /* LineNumberTableRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineNumberTableRowView.swift; sourceTree = ""; }; - 55373FC12E9372DF00AB56D0 /* NSAlert+DirtyFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAlert+DirtyFiles.swift"; sourceTree = ""; }; - 55373FC22E9372DF00AB56D0 /* SaveFileAccessoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveFileAccessoryView.swift; sourceTree = ""; }; - 55373FC32E9372DF00AB56D0 /* TextFieldVerticalCentered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldVerticalCentered.swift; sourceTree = ""; }; - 55373FC52E9372DF00AB56D0 /* FilesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesWindowController.swift; sourceTree = ""; }; - 55373FC62E9372DF00AB56D0 /* FilesWindowController+Clipboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Clipboard.swift"; sourceTree = ""; }; - 55373FC72E9372DF00AB56D0 /* FilesWindowController+Common.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Common.swift"; sourceTree = ""; }; - 55373FC82E9372DF00AB56D0 /* FilesWindowController+Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Document.swift"; sourceTree = ""; }; - 55373FC92E9372DF00AB56D0 /* FilesWindowController+FileInfoBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+FileInfoBarDelegate.swift"; sourceTree = ""; }; - 55373FCA2E9372DF00AB56D0 /* FilesWindowController+FilesScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+FilesScopeBar.swift"; sourceTree = ""; }; - 55373FCB2E9372DF00AB56D0 /* FilesWindowController+FilesTableViewContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+FilesTableViewContextMenu.swift"; sourceTree = ""; }; - 55373FCC2E9372DF00AB56D0 /* FilesWindowController+JumpLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+JumpLine.swift"; sourceTree = ""; }; - 55373FCD2E9372DF00AB56D0 /* FilesWindowController+LinesDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+LinesDetail.swift"; sourceTree = ""; }; - 55373FCE2E9372DF00AB56D0 /* FilesWindowController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Menu.swift"; sourceTree = ""; }; - 55373FCF2E9372DF00AB56D0 /* FilesWindowController+MenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+MenuDelegate.swift"; sourceTree = ""; }; - 55373FD02E9372DF00AB56D0 /* FilesWindowController+Navigate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Navigate.swift"; sourceTree = ""; }; - 55373FD12E9372DF00AB56D0 /* FilesWindowController+NSToolbarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+NSToolbarDelegate.swift"; sourceTree = ""; }; - 55373FD22E9372DF00AB56D0 /* FilesWindowController+NSWindowDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+NSWindowDelegate.swift"; sourceTree = ""; }; - 55373FD32E9372DF00AB56D0 /* FilesWindowController+PathControlDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+PathControlDelegate.swift"; sourceTree = ""; }; - 55373FD42E9372DF00AB56D0 /* FilesWindowController+Read.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Read.swift"; sourceTree = ""; }; - 55373FD52E9372DF00AB56D0 /* FilesWindowController+Save.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Save.swift"; sourceTree = ""; }; - 55373FD62E9372DF00AB56D0 /* FilesWindowController+Slider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+Slider.swift"; sourceTree = ""; }; - 55373FD72E9372DF00AB56D0 /* FilesWindowController+TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+TableView.swift"; sourceTree = ""; }; - 55373FD82E9372DF00AB56D0 /* FilesWindowController+UICreation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+UICreation.swift"; sourceTree = ""; }; - 55373FD92E9372DF00AB56D0 /* FilesWindowController+UISetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+UISetup.swift"; sourceTree = ""; }; - 55373FDB2E9372DF00AB56D0 /* DiffLine+Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffLine+Color.swift"; sourceTree = ""; }; - 55373FDD2E9372DF00AB56D0 /* DiffChangeType+Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffChangeType+Color.swift"; sourceTree = ""; }; - 55373FDE2E9372DF00AB56D0 /* DiffChangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffChangeType.swift; sourceTree = ""; }; - 55373FE02E9372DF00AB56D0 /* DiffCountersItem+DiffResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffCountersItem+DiffResult.swift"; sourceTree = ""; }; - 55373FE12E9372DF00AB56D0 /* FilesTableView+EditorData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesTableView+EditorData.swift"; sourceTree = ""; }; - 55373FE32E9372DF00AB56D0 /* JumpToLineWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JumpToLineWindow.swift; sourceTree = ""; }; - 55373FE62E9372DF00AB56D0 /* ColoredFoldersManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColoredFoldersManager.swift; sourceTree = ""; }; - 55373FE72E9372DF00AB56D0 /* FoldersOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoldersOutlineView.swift; sourceTree = ""; }; - 55373FE82E9372DF00AB56D0 /* FoldersOutlineView+Clipboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+Clipboard.swift"; sourceTree = ""; }; - 55373FE92E9372DF00AB56D0 /* FoldersOutlineView+Columns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+Columns.swift"; sourceTree = ""; }; - 55373FEA2E9372DF00AB56D0 /* FoldersOutlineView+Enumerate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+Enumerate.swift"; sourceTree = ""; }; - 55373FEB2E9372DF00AB56D0 /* FoldersOutlineView+ExternalApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+ExternalApp.swift"; sourceTree = ""; }; - 55373FEC2E9372DF00AB56D0 /* FoldersOutlineView+FileCount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+FileCount.swift"; sourceTree = ""; }; - 55373FED2E9372DF00AB56D0 /* FoldersOutlineView+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+Menu.swift"; sourceTree = ""; }; - 55373FEE2E9372DF00AB56D0 /* FoldersOutlineView+VisibleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+VisibleItem.swift"; sourceTree = ""; }; - 55373FEF2E9372DF00AB56D0 /* FoldersOutlineViewFindTextDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoldersOutlineViewFindTextDelegate.swift; sourceTree = ""; }; - 55373FF02E9372DF00AB56D0 /* CompareItem+LeafPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+LeafPath.swift"; sourceTree = ""; }; - 55373FF12E9372DF00AB56D0 /* CompareItem+NSTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+NSTableCellView.swift"; sourceTree = ""; }; - 55373FF22E9372DF00AB56D0 /* IconUtils+CompareItemIconUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "IconUtils+CompareItemIconUtils.swift"; sourceTree = ""; }; - 55373FF42E9372DF00AB56D0 /* DisplayFiltersScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayFiltersScopeBar.swift; sourceTree = ""; }; - 55373FF52E9372DF00AB56D0 /* FolderPanelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPanelView.swift; sourceTree = ""; }; - 55373FF62E9372DF00AB56D0 /* CompareItemTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareItemTableCellView.swift; sourceTree = ""; }; - 55373FF72E9372DF00AB56D0 /* CompareItemTableRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareItemTableRowView.swift; sourceTree = ""; }; - 55373FFB2E9372DF00AB56D0 /* CopyFileOperationExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyFileOperationExecutor.swift; sourceTree = ""; }; - 55373FFD2E9372DF00AB56D0 /* DeleteFileOperationExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFileOperationExecutor.swift; sourceTree = ""; }; - 55373FFF2E9372DF00AB56D0 /* MoveFileOperationExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveFileOperationExecutor.swift; sourceTree = ""; }; - 553740012E9372DF00AB56D0 /* ConfirmReplace.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmReplace.swift; sourceTree = ""; }; - 553740022E9372DF00AB56D0 /* NSAlert+ReplaceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAlert+ReplaceFile.swift"; sourceTree = ""; }; - 553740032E9372DF00AB56D0 /* NSApplication.ModalResponse+ReplaceFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSApplication.ModalResponse+ReplaceFile.swift"; sourceTree = ""; }; - 553740042E9372DF00AB56D0 /* ReplaceFileAttributeKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceFileAttributeKey.swift; sourceTree = ""; }; - 553740062E9372DF00AB56D0 /* CompletionIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompletionIndicator.swift; sourceTree = ""; }; - 553740072E9372DF00AB56D0 /* ErrorsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorsView.swift; sourceTree = ""; }; - 553740082E9372DF00AB56D0 /* FileOperationManagerDelegateImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperationManagerDelegateImpl.swift; sourceTree = ""; }; - 553740092E9372DF00AB56D0 /* ProgressIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressIndicatorController.swift; sourceTree = ""; }; - 5537400A2E9372DF00AB56D0 /* ReplaceInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceInfoView.swift; sourceTree = ""; }; - 5537400C2E9372DF00AB56D0 /* SyncFileController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncFileController.swift; sourceTree = ""; }; - 5537400D2E9372DF00AB56D0 /* SyncFileOperationExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncFileOperationExecutor.swift; sourceTree = ""; }; - 5537400E2E9372DF00AB56D0 /* SyncItemsInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncItemsInfo.swift; sourceTree = ""; }; - 5537400F2E9372DF00AB56D0 /* SyncOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncOutlineView.swift; sourceTree = ""; }; - 553740112E9372DF00AB56D0 /* DatePickersStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatePickersStackView.swift; sourceTree = ""; }; - 553740122E9372DF00AB56D0 /* TouchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchController.swift; sourceTree = ""; }; - 553740132E9372DF00AB56D0 /* TouchFileOperationExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchFileOperationExecutor.swift; sourceTree = ""; }; - 553740142E9372DF00AB56D0 /* TouchPickersStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchPickersStackView.swift; sourceTree = ""; }; - 553740162E9372DF00AB56D0 /* FileOperationExecutor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperationExecutor.swift; sourceTree = ""; }; - 553740172E9372DF00AB56D0 /* FileSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSummaryView.swift; sourceTree = ""; }; - 553740182E9372DF00AB56D0 /* FileSystemController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemController.swift; sourceTree = ""; }; - 553740192E9372DF00AB56D0 /* OperationSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationSummaryView.swift; sourceTree = ""; }; - 5537401B2E9372DF00AB56D0 /* FolderReader+Detached.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FolderReader+Detached.swift"; sourceTree = ""; }; - 5537401C2E9372DF00AB56D0 /* MainThreadFolderReaderDelegateBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainThreadFolderReaderDelegateBridge.swift; sourceTree = ""; }; - 5537401E2E9372DF00AB56D0 /* FolderSelectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderSelectionInfo.swift; sourceTree = ""; }; - 5537401F2E9372DF00AB56D0 /* FoldersWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoldersWindowController.swift; sourceTree = ""; }; - 553740202E9372DF00AB56D0 /* FoldersWindowController+BaseFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+BaseFolder.swift"; sourceTree = ""; }; - 553740212E9372DF00AB56D0 /* FoldersWindowController+Comparison.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+Comparison.swift"; sourceTree = ""; }; - 553740222E9372DF00AB56D0 /* FoldersWindowController+ConsoleViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+ConsoleViewDelegate.swift"; sourceTree = ""; }; - 553740232E9372DF00AB56D0 /* FoldersWindowController+DisplayFiltersScopeBarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+DisplayFiltersScopeBarDelegate.swift"; sourceTree = ""; }; - 553740242E9372DF00AB56D0 /* FoldersWindowController+Document.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+Document.swift"; sourceTree = ""; }; - 553740252E9372DF00AB56D0 /* FoldersWindowController+Exclude.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+Exclude.swift"; sourceTree = ""; }; - 553740262E9372DF00AB56D0 /* FoldersWindowController+FileSystemControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+FileSystemControllerDelegate.swift"; sourceTree = ""; }; - 553740272E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+FoldersOutlineView.swift"; sourceTree = ""; }; - 553740282E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineViewContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+FoldersOutlineViewContextMenu.swift"; sourceTree = ""; }; - 553740292E9372DF00AB56D0 /* FoldersWindowController+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+Menu.swift"; sourceTree = ""; }; - 5537402A2E9372DF00AB56D0 /* FoldersWindowController+MenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+MenuDelegate.swift"; sourceTree = ""; }; - 5537402B2E9372DF00AB56D0 /* FoldersWindowController+Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+Navigation.swift"; sourceTree = ""; }; - 5537402C2E9372DF00AB56D0 /* FoldersWindowController+NSToolbarDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+NSToolbarDelegate.swift"; sourceTree = ""; }; - 5537402D2E9372DF00AB56D0 /* FoldersWindowController+NSWindowDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+NSWindowDelegate.swift"; sourceTree = ""; }; - 5537402E2E9372DF00AB56D0 /* FoldersWindowController+PathControlDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+PathControlDelegate.swift"; sourceTree = ""; }; - 5537402F2E9372DF00AB56D0 /* FoldersWindowController+QLPreviewPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+QLPreviewPanel.swift"; sourceTree = ""; }; - 553740302E9372DF00AB56D0 /* FoldersWindowController+Select.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+Select.swift"; sourceTree = ""; }; - 553740312E9372DF00AB56D0 /* FoldersWindowController+SessionPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+SessionPreferences.swift"; sourceTree = ""; }; - 553740322E9372DF00AB56D0 /* FoldersWindowController+UICreation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+UICreation.swift"; sourceTree = ""; }; - 553740332E9372DF00AB56D0 /* FoldersWindowController+UISetup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+UISetup.swift"; sourceTree = ""; }; - 553740342E9372DF00AB56D0 /* OutlineViewItemDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutlineViewItemDelegate.swift; sourceTree = ""; }; - 553740362E9372DF00AB56D0 /* CommonPrefs+ConsoleLog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+ConsoleLog.swift"; sourceTree = ""; }; - 553740372E9372DF00AB56D0 /* DiffCountersItem+CompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffCountersItem+CompareItem.swift"; sourceTree = ""; }; - 553740392E9372DF00AB56D0 /* CopyCompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyCompareItem.swift; sourceTree = ""; }; - 5537403A2E9372DF00AB56D0 /* DeleteCompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteCompareItem.swift; sourceTree = ""; }; - 5537403B2E9372DF00AB56D0 /* FileAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileAttributes.swift; sourceTree = ""; }; - 5537403C2E9372DF00AB56D0 /* FileOperationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileOperationManager.swift; sourceTree = ""; }; - 5537403D2E9372DF00AB56D0 /* FileOperationManager+FileOperationManagerAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileOperationManager+FileOperationManagerAction.swift"; sourceTree = ""; }; - 5537403E2E9372DF00AB56D0 /* FileOperationManager+Util.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileOperationManager+Util.swift"; sourceTree = ""; }; - 5537403F2E9372DF00AB56D0 /* FolderManagerError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderManagerError.swift; sourceTree = ""; }; - 553740402E9372DF00AB56D0 /* CompareItem+Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+Metadata.swift"; sourceTree = ""; }; - 553740412E9372DF00AB56D0 /* MoveCompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveCompareItem.swift; sourceTree = ""; }; - 553740422E9372DF00AB56D0 /* PathTimestamps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathTimestamps.swift; sourceTree = ""; }; - 553740432E9372DF00AB56D0 /* RenameCompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameCompareItem.swift; sourceTree = ""; }; - 553740442E9372DF00AB56D0 /* TouchCompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchCompareItem.swift; sourceTree = ""; }; - 553740462E9372DF00AB56D0 /* ComparatorOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparatorOptions.swift; sourceTree = ""; }; - 553740472E9372DF00AB56D0 /* ComparatorOptions+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComparatorOptions+Description.swift"; sourceTree = ""; }; - 553740482E9372DF00AB56D0 /* ComparatorOptions+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ComparatorOptions+Helper.swift"; sourceTree = ""; }; - 5537404B2E9372DF00AB56D0 /* DisplayOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayOptions.swift; sourceTree = ""; }; - 5537404C2E9372DF00AB56D0 /* DisplayOptions+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DisplayOptions+Helper.swift"; sourceTree = ""; }; - 5537404E2E9372DF00AB56D0 /* FileExtraOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileExtraOptions.swift; sourceTree = ""; }; - 5537404F2E9372DF00AB56D0 /* CompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareItem.swift; sourceTree = ""; }; - 553740502E9372DF00AB56D0 /* CompareItem+Accessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+Accessors.swift"; sourceTree = ""; }; - 553740512E9372DF00AB56D0 /* CompareItem+Clone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+Clone.swift"; sourceTree = ""; }; - 553740522E9372DF00AB56D0 /* CompareItem+Comparison.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+Comparison.swift"; sourceTree = ""; }; - 553740532E9372DF00AB56D0 /* CompareItem+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+Description.swift"; sourceTree = ""; }; - 553740542E9372DF00AB56D0 /* CompareItem+FilterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+FilterConfig.swift"; sourceTree = ""; }; - 553740562E9372DF00AB56D0 /* CompareItem+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+Path.swift"; sourceTree = ""; }; - 553740572E9372DF00AB56D0 /* CompareItem+VisibleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+VisibleItem.swift"; sourceTree = ""; }; - 553740592E9372DF00AB56D0 /* CommonPrefs+DifferenceNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+DifferenceNavigator.swift"; sourceTree = ""; }; - 5537405A2E9372DF00AB56D0 /* FoldersOutlineView+DifferenceNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersOutlineView+DifferenceNavigator.swift"; sourceTree = ""; }; - 5537405B2E9372DF00AB56D0 /* CompareItem+DifferenceNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CompareItem+DifferenceNavigator.swift"; sourceTree = ""; }; - 5537405D2E9372DF00AB56D0 /* FilterConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterConfig.swift; sourceTree = ""; }; - 5537405E2E9372DF00AB56D0 /* FolderReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderReader.swift; sourceTree = ""; }; - 5537405F2E9372DF00AB56D0 /* FolderReader+Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FolderReader+Log.swift"; sourceTree = ""; }; - 553740602E9372DF00AB56D0 /* FolderReaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderReaderDelegate.swift; sourceTree = ""; }; - 553740612E9372DF00AB56D0 /* RefreshInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInfo.swift; sourceTree = ""; }; - 553740692E9372DF00AB56D0 /* FolderSelectionInfo+CompareActionValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FolderSelectionInfo+CompareActionValidator.swift"; sourceTree = ""; }; - 5537406A2E9372DF00AB56D0 /* FolderSelectionInfo+FileSystemActionValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FolderSelectionInfo+FileSystemActionValidator.swift"; sourceTree = ""; }; - 5537406B2E9372DF00AB56D0 /* FolderSelectionInfo+FilterActionValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FolderSelectionInfo+FilterActionValidator.swift"; sourceTree = ""; }; - 5537406C2E9372DF00AB56D0 /* FolderSelectionInfo+FolderActionValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FolderSelectionInfo+FolderActionValidator.swift"; sourceTree = ""; }; - 5537406D2E9372DF00AB56D0 /* FolderSelectionInfo+ViewerActionValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FolderSelectionInfo+ViewerActionValidator.swift"; sourceTree = ""; }; - 5537406F2E9372DF00AB56D0 /* VisibleItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleItem.swift; sourceTree = ""; }; - 553740702E9372DF00AB56D0 /* VisibleItem+Find.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleItem+Find.swift"; sourceTree = ""; }; - 553740712E9372DF00AB56D0 /* VisibleItem+Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleItem+Log.swift"; sourceTree = ""; }; - 553740722E9372DF00AB56D0 /* VisibleItem+QLPreviewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleItem+QLPreviewItem.swift"; sourceTree = ""; }; - 553740732E9372DF00AB56D0 /* VisibleItem+Sort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "VisibleItem+Sort.swift"; sourceTree = ""; }; - 553740752E9372DF00AB56D0 /* DescriptionOutlineNode+FolderCompareInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DescriptionOutlineNode+FolderCompareInfo.swift"; sourceTree = ""; }; - 553740762E9372DF00AB56D0 /* FolderCompareInfoWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderCompareInfoWindow.swift; sourceTree = ""; }; - 5537407A2E9372DF00AB56D0 /* HistoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryController.swift; sourceTree = ""; }; - 5537407B2E9372DF00AB56D0 /* HistoryEntityTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryEntityTableCellView.swift; sourceTree = ""; }; - 5537407C2E9372DF00AB56D0 /* HistoryFetchedResultsControllerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFetchedResultsControllerDelegate.swift; sourceTree = ""; }; - 5537407D2E9372DF00AB56D0 /* HistorySearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistorySearchField.swift; sourceTree = ""; }; - 5537407E2E9372DF00AB56D0 /* HistoryTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryTableView.swift; sourceTree = ""; }; - 553740802E9372DF00AB56D0 /* ComparisonStandardUserDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparisonStandardUserDataSource.swift; sourceTree = ""; }; - 553740812E9372DF00AB56D0 /* PreferencesBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesBox.swift; sourceTree = ""; }; - 553740822E9372DF00AB56D0 /* PreferencesBoxDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesBoxDataSource.swift; sourceTree = ""; }; - 553740832E9372DF00AB56D0 /* PreferencesCheckbox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesCheckbox.swift; sourceTree = ""; }; - 553740842E9372DF00AB56D0 /* StandardUserPreferencesBoxDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardUserPreferencesBoxDataSource.swift; sourceTree = ""; }; - 553740862E9372DF00AB56D0 /* BasePreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasePreferences.swift; sourceTree = ""; }; - 553740872E9372DF00AB56D0 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = ""; }; - 553740892E9372DF00AB56D0 /* ConfirmationsDocumentsBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationsDocumentsBox.swift; sourceTree = ""; }; - 5537408A2E9372DF00AB56D0 /* ConfirmationsFilesBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationsFilesBox.swift; sourceTree = ""; }; - 5537408B2E9372DF00AB56D0 /* ConfirmationsFoldersBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationsFoldersBox.swift; sourceTree = ""; }; - 5537408C2E9372DF00AB56D0 /* ConfirmationsPreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmationsPreferencesPanel.swift; sourceTree = ""; }; - 5537408E2E9372DF00AB56D0 /* DifferenceNavigatorBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DifferenceNavigatorBox.swift; sourceTree = ""; }; - 5537408F2E9372DF00AB56D0 /* FolderPreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPreferencesPanel.swift; sourceTree = ""; }; - 553740902E9372DF00AB56D0 /* FoldersTraversalBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoldersTraversalBox.swift; sourceTree = ""; }; - 553740912E9372DF00AB56D0 /* FolderViewBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderViewBox.swift; sourceTree = ""; }; - 553740932E9372DF00AB56D0 /* FontBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontBox.swift; sourceTree = ""; }; - 553740942E9372DF00AB56D0 /* FontPreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPreferencesPanel.swift; sourceTree = ""; }; - 553740962E9372DF00AB56D0 /* AppearanceBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceBox.swift; sourceTree = ""; }; - 553740972E9372DF00AB56D0 /* FolderComparisonBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderComparisonBox.swift; sourceTree = ""; }; - 553740982E9372DF00AB56D0 /* GeneralPreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesPanel.swift; sourceTree = ""; }; - 553740992E9372DF00AB56D0 /* PreferredEditorBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredEditorBox.swift; sourceTree = ""; }; - 5537409B2E9372DF00AB56D0 /* KeyboardDocumentBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardDocumentBox.swift; sourceTree = ""; }; - 5537409C2E9372DF00AB56D0 /* KeyboardPreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPreferencesPanel.swift; sourceTree = ""; }; - 5537409E2E9372DF00AB56D0 /* TextPreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextPreferencesPanel.swift; sourceTree = ""; }; - 5537409F2E9372DF00AB56D0 /* VisualizationBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualizationBox.swift; sourceTree = ""; }; - 553740A12E9372DF00AB56D0 /* TrustedPathsPreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedPathsPreferencesPanel.swift; sourceTree = ""; }; - 553740A32E9372DF00AB56D0 /* NSStackView+PreferencesPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSStackView+PreferencesPanel.swift"; sourceTree = ""; }; - 553740A62E9372DF00AB56D0 /* SessionPreferencesWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPreferencesWindow.swift; sourceTree = ""; }; - 553740A72E9372DF00AB56D0 /* SessionPreferencesWindow+Data.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionPreferencesWindow+Data.swift"; sourceTree = ""; }; - 553740A92E9372DF00AB56D0 /* AlignmentPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignmentPanel.swift; sourceTree = ""; }; - 553740AA2E9372DF00AB56D0 /* AlignRuleWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignRuleWindow.swift; sourceTree = ""; }; - 553740AB2E9372DF00AB56D0 /* AlignTestResultBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignTestResultBox.swift; sourceTree = ""; }; - 553740AC2E9372DF00AB56D0 /* ExpressionBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExpressionBox.swift; sourceTree = ""; }; - 553740AD2E9372DF00AB56D0 /* ExpressionBox+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ExpressionBox+Menu.swift"; sourceTree = ""; }; - 553740AE2E9372DF00AB56D0 /* FileNameCaseBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileNameCaseBox.swift; sourceTree = ""; }; - 553740AF2E9372DF00AB56D0 /* UserDefinedRulesBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefinedRulesBox.swift; sourceTree = ""; }; - 553740B12E9372DF00AB56D0 /* SessionPreferencesComparisonPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPreferencesComparisonPanel.swift; sourceTree = ""; }; - 553740B32E9372DF00AB56D0 /* FiltersPredicateEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersPredicateEditor.swift; sourceTree = ""; }; - 553740B42E9372DF00AB56D0 /* SessionPreferencesFiltersBox.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPreferencesFiltersBox.swift; sourceTree = ""; }; - 553740B52E9372DF00AB56D0 /* SessionPreferencesFiltersPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPreferencesFiltersPanel.swift; sourceTree = ""; }; - 553740B92E9372DF00AB56D0 /* PreferencesPanelDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesPanelDataSource.swift; sourceTree = ""; }; - 553740BC2E9372DF00AB56D0 /* UDiffScriptBuilder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UDiffScriptBuilder.h; sourceTree = ""; }; - 553740BD2E9372DF00AB56D0 /* UDiffScriptBuilder.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UDiffScriptBuilder.m; sourceTree = ""; }; - 553740BE2E9372DF00AB56D0 /* UnifiedDiff.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = UnifiedDiff.h; sourceTree = ""; }; - 553740BF2E9372DF00AB56D0 /* UnifiedDiff.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = UnifiedDiff.m; sourceTree = ""; }; - 553740C72E9372DF00AB56D0 /* BigFileFileOperationManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BigFileFileOperationManager.h; sourceTree = ""; }; - 553740C82E9372DF00AB56D0 /* BigFileFileOperationManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BigFileFileOperationManager.m; sourceTree = ""; }; - 553740D42E9372DF00AB56D0 /* NSPredicate+Objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSPredicate+Objc.h"; sourceTree = ""; }; - 553740D52E9372DF00AB56D0 /* NSPredicate+Objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSPredicate+Objc.m"; sourceTree = ""; }; - 553740D62E9372DF00AB56D0 /* TitlePredicateEditorRowTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TitlePredicateEditorRowTemplate.h; sourceTree = ""; }; - 553740D72E9372DF00AB56D0 /* TitlePredicateEditorRowTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TitlePredicateEditorRowTemplate.m; sourceTree = ""; }; - 553740D82E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPFileSizePredicateEditorRowTemplate.h; sourceTree = ""; }; - 553740D92E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPFileSizePredicateEditorRowTemplate.m; sourceTree = ""; }; - 553740DA2E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = TOPTimestampPredicateEditorRowTemplate.h; sourceTree = ""; }; - 553740DB2E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TOPTimestampPredicateEditorRowTemplate.m; sourceTree = ""; }; - 553740E22E9372DF00AB56D0 /* OpenDiffCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenDiffCommand.swift; sourceTree = ""; }; - 553740E52E9372DF00AB56D0 /* NSAppearance+DarkMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAppearance+DarkMode.swift"; sourceTree = ""; }; - 553740E72E9372DF00AB56D0 /* Array+MoveIndexes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+MoveIndexes.swift"; sourceTree = ""; }; - 553740E92E9372DF00AB56D0 /* NSColor+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSColor+Hex.swift"; sourceTree = ""; }; - 553740EB2E9372DF00AB56D0 /* NSManagedObjectContext+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+Helpers.swift"; sourceTree = ""; }; - 553740ED2E9372DF00AB56D0 /* TimeInterval+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Helper.swift"; sourceTree = ""; }; - 553740EF2E9372DF00AB56D0 /* NSError+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSError+Format.swift"; sourceTree = ""; }; - 553740F12E9372DF00AB56D0 /* FileManager+Attributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileManager+Attributes.swift"; sourceTree = ""; }; - 553740F32E9372DF00AB56D0 /* DateFormatter+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Helper.swift"; sourceTree = ""; }; - 553740F52E9372DF00AB56D0 /* NSImage+Tint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSImage+Tint.swift"; sourceTree = ""; }; - 553740F72E9372DF00AB56D0 /* NSMenu+File.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSMenu+File.swift"; sourceTree = ""; }; - 553740F92E9372DF00AB56D0 /* OptionSet+Toggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OptionSet+Toggle.swift"; sourceTree = ""; }; - 553740FB2E9372DF00AB56D0 /* NSAlert+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAlert+Helper.swift"; sourceTree = ""; }; - 553740FC2E9372DF00AB56D0 /* NSOpenPanel+Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSOpenPanel+Application.swift"; sourceTree = ""; }; - 553740FD2E9372DF00AB56D0 /* QLPreviewPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QLPreviewPanel.swift; sourceTree = ""; }; - 553740FF2E9372DF00AB56D0 /* NSPasteboard+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPasteboard+Helper.swift"; sourceTree = ""; }; - 553741012E9372DF00AB56D0 /* NSPredicateEditor+Fix.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPredicateEditor+Fix.swift"; sourceTree = ""; }; - 553741032E9372DF00AB56D0 /* String+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Helper.swift"; sourceTree = ""; }; - 553741042E9372DF00AB56D0 /* String+Highlights.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Highlights.swift"; sourceTree = ""; }; - 553741052E9372DF00AB56D0 /* String+Occcurrences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Occcurrences.swift"; sourceTree = ""; }; - 553741062E9372DF00AB56D0 /* String+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Path.swift"; sourceTree = ""; }; - 553741072E9372DF00AB56D0 /* String+RegularExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+RegularExpression.swift"; sourceTree = ""; }; - 553741092E9372DF00AB56D0 /* NSTableView+Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Font.swift"; sourceTree = ""; }; - 5537410A2E9372DF00AB56D0 /* NSTableView+Row.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTableView+Row.swift"; sourceTree = ""; }; - 5537410C2E9372DF00AB56D0 /* NSToolbar+Create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSToolbar+Create.swift"; sourceTree = ""; }; - 5537410D2E9372DF00AB56D0 /* NSToolbarItem+Create.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSToolbarItem+Create.swift"; sourceTree = ""; }; - 5537410F2E9372DF00AB56D0 /* URL+FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+FileManager.swift"; sourceTree = ""; }; - 553741102E9372DF00AB56D0 /* URL+Finder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Finder.swift"; sourceTree = ""; }; - 553741112E9372DF00AB56D0 /* URL+Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Metadata.swift"; sourceTree = ""; }; - 553741122E9372DF00AB56D0 /* URL+Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Path.swift"; sourceTree = ""; }; - 553741132E9372DF00AB56D0 /* URL+ResourceFork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+ResourceFork.swift"; sourceTree = ""; }; - 553741142E9372DF00AB56D0 /* URL+StructuredContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+StructuredContent.swift"; sourceTree = ""; }; - 553741152E9372DF00AB56D0 /* URL+SymLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+SymLink.swift"; sourceTree = ""; }; - 553741172E9372DF00AB56D0 /* NSBox+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSBox+Helper.swift"; sourceTree = ""; }; - 553741192E9372DF00AB56D0 /* NSButton+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSButton+Helper.swift"; sourceTree = ""; }; - 5537411A2E9372DF00AB56D0 /* NSPopUpButton+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSPopUpButton+Helper.swift"; sourceTree = ""; }; - 5537411C2E9372DF00AB56D0 /* NSProgressIndicator+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSProgressIndicator+Helper.swift"; sourceTree = ""; }; - 5537411E2E9372DF00AB56D0 /* NSTextField+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextField+Helper.swift"; sourceTree = ""; }; - 553741202E9372DF00AB56D0 /* NSTextView+Style.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextView+Style.swift"; sourceTree = ""; }; - 553741232E9372DF00AB56D0 /* NSWorkspace+Finder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWorkspace+Finder.swift"; sourceTree = ""; }; - 553741262E9372DF00AB56D0 /* SecureBookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureBookmark.swift; sourceTree = ""; }; - 553741282E9372DF00AB56D0 /* EncodingError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodingError.swift; sourceTree = ""; }; - 5537412A2E9372DF00AB56D0 /* NSEvent+VirtualKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSEvent+VirtualKeys.swift"; sourceTree = ""; }; - 5537412C2E9372DF00AB56D0 /* FileSizeFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSizeFormatter.swift; sourceTree = ""; }; - 5537412D2E9372DF00AB56D0 /* IntegerFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntegerFormatter.swift; sourceTree = ""; }; - 5537412F2E9372DF00AB56D0 /* IconUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconUtils.swift; sourceTree = ""; }; - 553741302E9372DF00AB56D0 /* NoodleCustomImageRep.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoodleCustomImageRep.swift; sourceTree = ""; }; - 553741322E9372DF00AB56D0 /* BufferedInputStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferedInputStream.swift; sourceTree = ""; }; - 553741332E9372DF00AB56D0 /* CompareUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareUtil.swift; sourceTree = ""; }; - 553741342E9372DF00AB56D0 /* FileError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileError.swift; sourceTree = ""; }; - 553741352E9372DF00AB56D0 /* PowerAssertion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PowerAssertion.swift; sourceTree = ""; }; - 553741372E9372DF00AB56D0 /* AttributedMenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttributedMenuItem.swift; sourceTree = ""; }; - 553741392E9372DF00AB56D0 /* OpenEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenEditor.swift; sourceTree = ""; }; - 5537413A2E9372DF00AB56D0 /* OpenEditorAttribute.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenEditorAttribute.swift; sourceTree = ""; }; - 5537413B2E9372DF00AB56D0 /* OpenEditorError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenEditorError.swift; sourceTree = ""; }; - 5537413D2E9372DF00AB56D0 /* PreferredEditorPopupCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredEditorPopupCell.swift; sourceTree = ""; }; - 5537413F2E9372DF00AB56D0 /* VisibleWhitespaces.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleWhitespaces.swift; sourceTree = ""; }; - 553741412E9372DF00AB56D0 /* DescriptionOutlineNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DescriptionOutlineNode.swift; sourceTree = ""; }; - 553741422E9372DF00AB56D0 /* TableViewCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewCommon.swift; sourceTree = ""; }; - 553741432E9372DF00AB56D0 /* TableViewContextMenuDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewContextMenuDelegate.swift; sourceTree = ""; }; - 553741452E9372DF00AB56D0 /* NSTextFieldCell+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSTextFieldCell+Helper.swift"; sourceTree = ""; }; - 553741462E9372DF00AB56D0 /* RSVerticallyCenteredTextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSVerticallyCenteredTextFieldCell.swift; sourceTree = ""; }; - 553741482E9372DF00AB56D0 /* CustomValidationToolbarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomValidationToolbarItem.swift; sourceTree = ""; }; - 5537414A2E9372DF00AB56D0 /* ActionBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionBarView.swift; sourceTree = ""; }; - 5537414C2E9372DF00AB56D0 /* LinkButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkButton.swift; sourceTree = ""; }; - 5537414D2E9372DF00AB56D0 /* StandardButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardButtons.swift; sourceTree = ""; }; - 5537414F2E9372DF00AB56D0 /* ConsoleToolbarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleToolbarView.swift; sourceTree = ""; }; - 553741502E9372DF00AB56D0 /* ConsoleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConsoleView.swift; sourceTree = ""; }; - 553741522E9372DF00AB56D0 /* DualPaneSplitView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DualPaneSplitView.swift; sourceTree = ""; }; - 553741532E9372DF00AB56D0 /* DualPaneSplitViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DualPaneSplitViewDelegate.swift; sourceTree = ""; }; - 553741552E9372DF00AB56D0 /* FileDropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDropView.swift; sourceTree = ""; }; - 553741562E9372DF00AB56D0 /* FileDropView+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileDropView+Helper.swift"; sourceTree = ""; }; - 553741582E9372DF00AB56D0 /* ProgressBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressBarView.swift; sourceTree = ""; }; - 5537415A2E9372DF00AB56D0 /* SynchroScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SynchroScrollView.swift; sourceTree = ""; }; - 5537415C2E9372DF00AB56D0 /* TextFieldSelectionHolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldSelectionHolder.swift; sourceTree = ""; }; - 5537415F2E9372DF00AB56D0 /* WindowCancelOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowCancelOperation.swift; sourceTree = ""; }; - 553741602E9372DF00AB56D0 /* WindowOSD.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WindowOSD.swift; sourceTree = ""; }; - 553741642E9372DF00AB56D0 /* AlignPopupButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignPopupButtonCell.swift; sourceTree = ""; }; - 553741652E9372DF00AB56D0 /* ComparatorPopUpButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparatorPopUpButtonCell.swift; sourceTree = ""; }; - 553741662E9372DF00AB56D0 /* DisplayFiltersPopUpButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayFiltersPopUpButtonCell.swift; sourceTree = ""; }; - 553741672E9372DF00AB56D0 /* FilePathTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePathTableCellView.swift; sourceTree = ""; }; - 553741692E9372DF00AB56D0 /* DiffCountersItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffCountersItem.swift; sourceTree = ""; }; - 5537416A2E9372DF00AB56D0 /* DiffCountersTextFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffCountersTextFieldCell.swift; sourceTree = ""; }; - 5537416B2E9372DF00AB56D0 /* DifferenceCounters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DifferenceCounters.swift; sourceTree = ""; }; - 5537416D2E9372DF00AB56D0 /* FilePathTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilePathTextField.swift; sourceTree = ""; }; - 5537416E2E9372DF00AB56D0 /* FindText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindText.swift; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; 5537434B2E937B7F00AB56D0 /* colorsDark.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = colorsDark.json; path = AppDefaults/colorsDark.json; sourceTree = ""; }; 5537434C2E937B7F00AB56D0 /* VDDefaults.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = VDDefaults.plist; path = AppDefaults/VDDefaults.plist; sourceTree = ""; }; @@ -878,18 +87,6 @@ 5541CE422FFCD8DA0044A569 /* mask-full.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mask-full.png"; sourceTree = ""; }; 5541CE432FFCD8DA0044A569 /* mask-middle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mask-middle.png"; sourceTree = ""; }; 5541CE4A2FFCD9E70044A569 /* vd.iconset */ = {isa = PBXFileReference; lastKnownFileType = folder.iconset; name = vd.iconset; path = images/vd.iconset; sourceTree = ""; }; - 55450B162ED4790B0048619F /* TextPreferencesPanel+PreferencesBoxDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TextPreferencesPanel+PreferencesBoxDataSource.swift"; sourceTree = ""; }; - 55450B182ED47B680048619F /* FileSessionPreferencesWindow+PreferencesBoxDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FileSessionPreferencesWindow+PreferencesBoxDataSource.swift"; sourceTree = ""; }; - 554750C52F5C0A0A00C1747A /* CommonAncestorPathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommonAncestorPathTests.swift; sourceTree = ""; }; - 5548ABA72F7FE2BB0064B964 /* LineDetailTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineDetailTextView.swift; sourceTree = ""; }; - 554B4A3A2E9B7B8F0023AAF0 /* EndOfLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndOfLine.swift; sourceTree = ""; }; - 554B4A3B2E9B7B8F0023AAF0 /* DiffResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffResult.swift; sourceTree = ""; }; - 554B4A3D2E9B7B8F0023AAF0 /* DiffResult+Dump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffResult+Dump.swift"; sourceTree = ""; }; - 554B4A3E2E9B7B8F0023AAF0 /* DiffResult+Lines.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffResult+Lines.swift"; sourceTree = ""; }; - 554B4A3F2E9B7B8F0023AAF0 /* DiffResult+Section.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffResult+Section.swift"; sourceTree = ""; }; - 554B4A402E9B7B8F0023AAF0 /* DiffSide.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSide.swift; sourceTree = ""; }; - 554B4A412E9B7B8F0023AAF0 /* DiffSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSection.swift; sourceTree = ""; }; - 554B4A432E9B7B8F0023AAF0 /* DiffLine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffLine.swift; sourceTree = ""; }; 5550B5921A21CD1F009BC4A4 /* aliasbadge@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "aliasbadge@2x.png"; sourceTree = ""; }; 5550B5941A21D2F3009BC4A4 /* empty@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "empty@2x.png"; sourceTree = ""; }; 555CAD9B1C2710D000371E4D /* Warnings.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -897,1637 +94,169 @@ 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 = ""; }; 557A47F12FF7DE3800D0F6B4 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 557A47F32FF7DE3800D0F6B4 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/MainMenu.xcstrings; 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 = ""; }; 557C181C2E7E8BDD00381A3A /* Versions.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Versions.local.xcconfig; sourceTree = ""; }; - 557C18202E7E8C0D00381A3A /* AppConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppConfig.xcconfig; sourceTree = ""; }; - 557C18212E7E8C0D00381A3A /* Signing.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.local.xcconfig; sourceTree = ""; }; - 557C18222E7E8C0D00381A3A /* Versions.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Versions.local.xcconfig; sourceTree = ""; }; - 5580A6E32E5D7B19008D3D3A /* NoSandbox.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = NoSandbox.entitlements; sourceTree = ""; }; - 55858303300DD8B100EDC56C /* DiffResult.Options+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DiffResult.Options+Description.swift"; sourceTree = ""; }; - 55858305300DE12E00EDC56C /* ScopeBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScopeBarView.swift; sourceTree = ""; }; - 558DEAC42EA263DE00F05A10 /* CFStringEncoding+StringEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CFStringEncoding+StringEncoding.swift"; sourceTree = ""; }; - 558DEAC72EA263F500F05A10 /* EncodingManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodingManager.swift; sourceTree = ""; }; - 558DEAC82EA263F500F05A10 /* EncodingPopUpButtonCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodingPopUpButtonCell.swift; sourceTree = ""; }; - 558DEAC92EA263F500F05A10 /* SelectEncodingsPanel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectEncodingsPanel.swift; sourceTree = ""; }; - 558DEACE2EA2642100F05A10 /* NSWindow+Editing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSWindow+Editing.swift"; sourceTree = ""; }; - 558DEAD12EA26C5800F05A10 /* Logger+App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Logger+App.swift"; sourceTree = ""; }; + 557EBD88305AC79D00A9BBD2 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 55916A0B2E8A814200ED6629 /* History.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = History.xcdatamodel; sourceTree = ""; }; 55916A0C2E8A814200ED6629 /* MyDocument1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MyDocument1.0.xcdatamodel; sourceTree = ""; }; 55916A0D2E8A814200ED6629 /* MyDocument1.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MyDocument1.1.xcdatamodel; sourceTree = ""; }; 55916A0E2E8A814200ED6629 /* MyDocument1.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MyDocument1.2.xcdatamodel; sourceTree = ""; }; 55916A0F2E8A814200ED6629 /* MyDocument1.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = MyDocument1.3.xcdatamodel; sourceTree = ""; }; - 559186402E9B821B00A30620 /* DiffSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffSummary.swift; sourceTree = ""; }; 55944CC72E5C8D9F00FB1F8C /* VisualDifferTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VisualDifferTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 5595901C2EABA7FE00B5AB0B /* CompareSummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareSummary.swift; sourceTree = ""; }; - 5595901E2EABA8A100B5AB0B /* CompareChangeType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompareChangeType.swift; sourceTree = ""; }; - 559590202EABAFC200B5AB0B /* SessionDiff+ItemComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionDiff+ItemComparator.swift"; sourceTree = ""; }; - 559590222EABB06100B5AB0B /* ItemComparator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemComparator.swift; sourceTree = ""; }; - 559590232EABB06100B5AB0B /* ItemComparator+Align.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemComparator+Align.swift"; sourceTree = ""; }; - 559590242EABB06100B5AB0B /* ItemComparator+AlignRegularExpression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemComparator+AlignRegularExpression.swift"; sourceTree = ""; }; - 559590252EABB06100B5AB0B /* ItemComparator+Compare.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ItemComparator+Compare.swift"; sourceTree = ""; }; - 5595902A2EABB15F00B5AB0B /* MockItemComparatorDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockItemComparatorDelegate.swift; sourceTree = ""; }; 559614B014B0471E0075ED34 /* aliasbadge.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = aliasbadge.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 = ""; }; - 55B1CE2C2E9934F800426FEC /* DiffResultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffResultTests.swift; sourceTree = ""; }; - 55B1CE2E2E9934F800426FEC /* DiffResultBaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffResultBaseTests.swift; sourceTree = ""; }; 55B212092490AB3B001717DD /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; - 55B93A442FAC42910024AEC0 /* DiffProcessor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffProcessor.swift; sourceTree = ""; }; - 55B9B73D2F5C26F800DB8326 /* BaseTests+CompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTests+CompareItem.swift"; sourceTree = ""; }; - 55BC333D2EDAC69C004035A0 /* RowHeightCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RowHeightCalculator.swift; sourceTree = ""; }; - 55BC333F2EDAC96F004035A0 /* TiledImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TiledImageView.swift; sourceTree = ""; }; - 55BC33412EDACB09004035A0 /* FilesWindowController+RowHeightDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+RowHeightDataSource.swift"; sourceTree = ""; }; - 55BC33422EDACB09004035A0 /* FilesWindowController+SplitViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FilesWindowController+SplitViewDelegate.swift"; sourceTree = ""; }; - 55BCFE462F627DDF00E704D8 /* SessionPopupButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionPopupButton.swift; sourceTree = ""; }; 55C1CBE22E8BB6A2004FE322 /* visdiff */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = visdiff; sourceTree = BUILT_PRODUCTS_DIR; }; - 55C1CBF32E8BB6AD004FE322 /* AppConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppConfig.xcconfig; sourceTree = ""; }; - 55C1CBF42E8BB6AD004FE322 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; - 55C1CBF52E8BB6AD004FE322 /* Signing.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Signing.local.xcconfig; sourceTree = ""; }; - 55C1CBF82E8BB6AD004FE322 /* Versions.local.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Versions.local.xcconfig; sourceTree = ""; }; - 55C1CBF92E8BB6AD004FE322 /* visdiff.1 */ = {isa = PBXFileReference; lastKnownFileType = text.man; path = visdiff.1; sourceTree = ""; }; - 55C1CBFA2E8BB6AD004FE322 /* visdiff.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = visdiff.entitlements; sourceTree = ""; }; - 55C1CBFB2E8BB6AD004FE322 /* visdiff-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "visdiff-Info.plist"; sourceTree = ""; }; - 55C1CC062E8BBAD1004FE322 /* DocumentWaiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentWaiter.swift; sourceTree = ""; }; 55C84A702DA93ADB00BA39DA /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; - 55CE5BC52FFCBC8500B0F3F5 /* VDSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VDSymbol.swift; sourceTree = ""; }; - 55D26E84140E2EAD00646F50 /* AppKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AppKit.framework; path = System/Library/Frameworks/AppKit.framework; sourceTree = SDKROOT; }; - 55D352E62EFD0C4D00C944D5 /* FileSystemTestHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSystemTestHelper.swift; sourceTree = ""; }; - 55D71FD52EE584B7007E0FA0 /* AppUpdater.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdater.swift; sourceTree = ""; }; 55D71FD72EE584FF007E0FA0 /* AppConfig-Sparkle.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "AppConfig-Sparkle.xcconfig"; sourceTree = ""; }; 55D71FD82EE584FF007E0FA0 /* VisualDiffer-Sparkle.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "VisualDiffer-Sparkle.entitlements"; sourceTree = ""; }; 55D71FD92EE584FF007E0FA0 /* VisualDiffer-Sparkle-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "VisualDiffer-Sparkle-Info.plist"; sourceTree = ""; }; - 55D8812D2F3DA329008DE497 /* CopyFinderMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyFinderMetadata.swift; sourceTree = ""; }; - 55DF3DA92E924BB10044CC0C /* BaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseTests.swift; sourceTree = ""; }; - 55DF3DAA2E924BB10044CC0C /* BaseTests+AssertCompareItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTests+AssertCompareItem.swift"; sourceTree = ""; }; - 55DF3DAC2E924BB10044CC0C /* MockFileOperationManagerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFileOperationManagerDelegate.swift; sourceTree = ""; }; - 55DF3DAD2E924BB10044CC0C /* MockFolderReaderDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockFolderReaderDelegate.swift; sourceTree = ""; }; - 55DF3DB02E924BB10044CC0C /* LabelsCopyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsCopyTests.swift; sourceTree = ""; }; - 55DF3DB12E924BB10044CC0C /* LabelsCreateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsCreateTests.swift; sourceTree = ""; }; - 55DF3DB22E924BB10044CC0C /* LabelsMoveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelsMoveTests.swift; sourceTree = ""; }; - 55DF3DB32E924BB10044CC0C /* TagsCopyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsCopyTests.swift; sourceTree = ""; }; - 55DF3DB42E924BB10044CC0C /* TagsCreateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsCreateTests.swift; sourceTree = ""; }; - 55DF3DB52E924BB10044CC0C /* TagsDeleteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsDeleteTests.swift; sourceTree = ""; }; - 55DF3DB62E924BB10044CC0C /* TagsMoveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsMoveTests.swift; sourceTree = ""; }; - 55DF3DB82E924BB10044CC0C /* AlignmentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlignmentTests.swift; sourceTree = ""; }; - 55DF3DB92E924BB10044CC0C /* BufferedInputStreamTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BufferedInputStreamTests.swift; sourceTree = ""; }; - 55DF3DBA2E924BB10044CC0C /* ComparatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparatorTests.swift; sourceTree = ""; }; - 55DF3DBB2E924BB10044CC0C /* CopyFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyFilesTests.swift; sourceTree = ""; }; - 55DF3DBC2E924BB10044CC0C /* DeleteFileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteFileTests.swift; sourceTree = ""; }; - 55DF3DBD2E924BB10044CC0C /* DisplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayTests.swift; sourceTree = ""; }; - 55DF3DBE2E924BB10044CC0C /* EmptyFolderColorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyFolderColorTests.swift; sourceTree = ""; }; - 55DF3DBF2E924BB10044CC0C /* FiltersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersTests.swift; sourceTree = ""; }; - 55DF3DC02E924BB10044CC0C /* MoveFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoveFilesTests.swift; sourceTree = ""; }; - 55DF3DC12E924BB10044CC0C /* RenameFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenameFilesTests.swift; sourceTree = ""; }; - 55DF3DC22E924BB10044CC0C /* TouchFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchFilesTests.swift; sourceTree = ""; }; - 55DF3DC42E924BB10044CC0C /* BaseTests+AssertFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTests+AssertFileSystem.swift"; sourceTree = ""; }; - 55DF3DC52E924BB10044CC0C /* BaseTests+FileSystemOperations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseTests+FileSystemOperations.swift"; sourceTree = ""; }; - 55DF3DC62E924BB10044CC0C /* CaseSensitiveBaseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CaseSensitiveBaseTest.swift; sourceTree = ""; }; - 55DF3DC82E924BB10044CC0C /* LeafPathTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LeafPathTests.swift; sourceTree = ""; }; - 55DF3DCA2E924BB10044CC0C /* FoldersWindowControllerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoldersWindowControllerTests.swift; sourceTree = ""; }; - 55DF3DCB2E924BB10044CC0C /* RefreshInfoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInfoTests.swift; sourceTree = ""; }; - 55DF3DCE2E924BB10044CC0C /* StringUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringUtils.swift; sourceTree = ""; }; 55E1029912C9E17300432C61 /* empty.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = empty.png; sourceTree = ""; }; - 55E381C02EEFFCF500CB2F00 /* SymbolicLinkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SymbolicLinkTests.swift; sourceTree = ""; }; 55E3A5641826552A002A890B /* lockedbadge.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = lockedbadge.png; sourceTree = ""; }; 55E3A5651826552A002A890B /* lockedbadge@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "lockedbadge@2x.png"; sourceTree = ""; }; 55E3EF3524C2D06800F7A95A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 55E78B57164FCE7800877721 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = Library/Frameworks/Cocoa.framework; sourceTree = DEVELOPER_DIR; }; - 55EC0A4A2F3868E4008201E1 /* CopyFilesTag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyFilesTag.swift; sourceTree = ""; }; 55ECD8671476B34600D8BEB4 /* VisualDiffer.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = VisualDiffer.entitlements; sourceTree = ""; }; 55F013FC27A90C46003EA2D4 /* AppConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppConfig.xcconfig; sourceTree = ""; }; - 55F42EDF2F76798A009B211C /* CommonPrefs+FileDifferenceNavigator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommonPrefs+FileDifferenceNavigator.swift"; sourceTree = ""; }; - 55F4A0012FEE100000A1B2C3 /* ExternalFileOperationContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalFileOperationContext.swift; sourceTree = ""; }; - 55F9BAF12FA7259B004EA543 /* FoldersWindowController+Observers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FoldersWindowController+Observers.swift"; sourceTree = ""; }; - 55F9E7992EDD5DAD001218C7 /* DiffResult.Options.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiffResult.Options.swift; sourceTree = ""; }; - 55F9E79B2EDD6221001218C7 /* WhitespacesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WhitespacesTests.swift; sourceTree = ""; }; - 55FC15F22557CD000075E30D /* ScriptingBridge.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ScriptingBridge.framework; path = System/Library/Frameworks/ScriptingBridge.framework; sourceTree = SDKROOT; }; - 55FE89492F054B260003864A /* FlagSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagSet.swift; sourceTree = ""; }; - 7788DA0506752A1600599AAD /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; 8D15AC360486D014006FF6A4 /* VisualDiffer-Info.plist */ = {isa = PBXFileReference; explicitFileType = text.plist.xml; fileEncoding = 4; path = "VisualDiffer-Info.plist"; sourceTree = ""; }; 8D15AC370486D014006FF6A4 /* VisualDiffer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VisualDiffer.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ -/* Begin PBXFrameworksBuildPhase section */ - 55944CC42E5C8D9F00FB1F8C /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 55C1CBDF2E8BB6A2004FE322 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 8D15AC330486D014006FF6A4 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 55C84A712DA93ADB00BA39DA /* UserNotifications.framework in Frameworks */, - 55A027D4158242310004583F /* Quartz.framework in Frameworks */, - 775DFF38067A968500C5B868 /* Cocoa.framework in Frameworks */, - 55C165622EE444050082A24C /* Sparkle in Frameworks */, - 55740BF512D710EC004BF6CC /* IOKit.framework in Frameworks */, - 55740BF912D71103004BF6CC /* Security.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 55C9DA8A305A697C00D4FD1E /* Exceptions for "Tests" folder in "VisualDifferTests" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + AppConfig.xcconfig, + Signing.local.xcconfig, + Versions.local.xcconfig, + VisualDiffer.xctestplan, + ); + target = 55944CC62E5C8D9F00FB1F8C /* VisualDifferTests */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ -/* Begin PBXGroup section */ - 1058C7A6FEA54F5311CA2CBB /* Linked Frameworks */ = { - isa = PBXGroup; - children = ( - 1058C7A7FEA54F5311CA2CBB /* Cocoa.framework */, - ); - name = "Linked Frameworks"; - sourceTree = ""; - }; - 1058C7A8FEA54F5311CA2CBB /* Other Frameworks */ = { - isa = PBXGroup; - children = ( - 2A37F4C5FDCFA73011CA2CEA /* Foundation.framework */, - 7788DA0506752A1600599AAD /* CoreData.framework */, - 2A37F4C4FDCFA73011CA2CEA /* AppKit.framework */, - ); - name = "Other Frameworks"; - sourceTree = ""; - }; - 19C28FB0FE9D524F11CA2CBB /* Products */ = { - isa = PBXGroup; - children = ( - 8D15AC370486D014006FF6A4 /* VisualDiffer.app */, - 55944CC72E5C8D9F00FB1F8C /* VisualDifferTests.xctest */, - 55C1CBE22E8BB6A2004FE322 /* visdiff */, - ); - name = Products; - sourceTree = ""; - }; - 2A37F4AAFDCFA73011CA2CEA /* QuickDirtyDiff */ = { - isa = PBXGroup; - children = ( - 55D71FD72EE584FF007E0FA0 /* AppConfig-Sparkle.xcconfig */, - 55D71FD82EE584FF007E0FA0 /* VisualDiffer-Sparkle.entitlements */, - 55D71FD92EE584FF007E0FA0 /* VisualDiffer-Sparkle-Info.plist */, - 55F013FC27A90C46003EA2D4 /* AppConfig.xcconfig */, - 557C181B2E7E8BDD00381A3A /* Signing.local.xcconfig */, - 557C181C2E7E8BDD00381A3A /* Versions.local.xcconfig */, - 555CAD9B1C2710D000371E4D /* Warnings.xcconfig */, - 553741772E9372DF00AB56D0 /* Sources */, - 2A37F4C3FDCFA73011CA2CEA /* Frameworks */, - 55916A0A2E8A814200ED6629 /* Models */, - 2A37F4AFFDCFA73011CA2CEA /* Other Sources */, - 19C28FB0FE9D524F11CA2CBB /* Products */, - 2A37F4B8FDCFA73011CA2CEA /* Resources */, - 55944CDA2E5C8E5B00FB1F8C /* Tests */, - 55C1CBFC2E8BB6AD004FE322 /* visdiff */, - 55656A852DD34C5100DD5835 /* VisualDiffer-Bridging-Header.h */, - ); - name = QuickDirtyDiff; - sourceTree = ""; - }; - 2A37F4AFFDCFA73011CA2CEA /* Other Sources */ = { - isa = PBXGroup; - children = ( - 32DBCF750370BD2300C91783 /* VisualDiffer_Prefix.pch */, - ); - name = "Other Sources"; - sourceTree = ""; - }; - 2A37F4B8FDCFA73011CA2CEA /* Resources */ = { - isa = PBXGroup; - children = ( - 5537434A2E937B7F00AB56D0 /* colors.json */, - 5537434B2E937B7F00AB56D0 /* colorsDark.json */, - 2A37F4B9FDCFA73011CA2CEA /* Credits.rtf */, - 55157D5C12898F8800826FF8 /* images */, - 557A47F12FF7DE3800D0F6B4 /* Localizable.xcstrings */, - 2F7446A70DB6BCF400F9684A /* MainMenu.xib */, - 5541CE4A2FFCD9E70044A569 /* vd.iconset */, - 5537434C2E937B7F00AB56D0 /* VDDefaults.plist */, - 55ECD8671476B34600D8BEB4 /* VisualDiffer.entitlements */, - 5537434D2E937B7F00AB56D0 /* VisualDiffer.sdef */, - 8D15AC360486D014006FF6A4 /* VisualDiffer-Info.plist */, - ); - name = Resources; - sourceTree = ""; - }; - 2A37F4C3FDCFA73011CA2CEA /* Frameworks */ = { - isa = PBXGroup; - children = ( - 55C84A702DA93ADB00BA39DA /* UserNotifications.framework */, - 55FC15F22557CD000075E30D /* ScriptingBridge.framework */, - 55D26E84140E2EAD00646F50 /* AppKit.framework */, - 550A2CC113FF973B00981A9B /* ApplicationServices.framework */, - 55A67B5113FF95B5006DD5BB /* Foundation.framework */, - 55740BF412D710EC004BF6CC /* IOKit.framework */, - 1058C7A6FEA54F5311CA2CBB /* Linked Frameworks */, - 55E78B57164FCE7800877721 /* Cocoa.framework */, - 1058C7A8FEA54F5311CA2CBB /* Other Frameworks */, - 55A027D3158242310004583F /* Quartz.framework */, - 55740BF812D71103004BF6CC /* Security.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 55157D5C12898F8800826FF8 /* images */ = { - isa = PBXGroup; - children = ( - 5541CE442FFCD8DA0044A569 /* folder */, - 557315B02551307800CF4372 /* Images.xcassets */, - 559614B014B0471E0075ED34 /* aliasbadge.png */, - 5550B5921A21CD1F009BC4A4 /* aliasbadge@2x.png */, - 55E1029912C9E17300432C61 /* empty.png */, - 5550B5941A21D2F3009BC4A4 /* empty@2x.png */, - 55E3A5641826552A002A890B /* lockedbadge.png */, - 55E3A5651826552A002A890B /* lockedbadge@2x.png */, - ); - path = images; - sourceTree = ""; - }; - 551DE3A52ED4237B0067AB18 /* SessionPreferences */ = { - isa = PBXGroup; - children = ( - 551DE3A92ED428A50067AB18 /* FilePreferences.swift */, - 551DE3A42ED4237B0067AB18 /* FileSessionPreferencesWindow.swift */, - 55450B182ED47B680048619F /* FileSessionPreferencesWindow+PreferencesBoxDataSource.swift */, - 551DE3B12ED42AB20067AB18 /* Panels */, - ); - path = SessionPreferences; - sourceTree = ""; - }; - 551DE3B02ED42AB20067AB18 /* Comparison */ = { - isa = PBXGroup; - children = ( - 551DE3B32ED42AD00067AB18 /* FilePreferencesComparisonPanel.swift */, - ); - path = Comparison; - sourceTree = ""; - }; - 551DE3B12ED42AB20067AB18 /* Panels */ = { - isa = PBXGroup; - children = ( - 551DE3B02ED42AB20067AB18 /* Comparison */, - ); - path = Panels; - sourceTree = ""; - }; - 55373F8E2E9372DF00AB56D0 /* App */ = { - isa = PBXGroup; - children = ( - 55373F8D2E9372DF00AB56D0 /* AppDelegate.swift */, - 55D71FD52EE584B7007E0FA0 /* AppUpdater.swift */, - 558DEAD12EA26C5800F05A10 /* Logger+App.swift */, - ); - path = App; - sourceTree = ""; - }; - 55373F972E9372DF00AB56D0 /* Common */ = { - isa = PBXGroup; - children = ( - 55373F8F2E9372DF00AB56D0 /* AlignRule.swift */, - 55373F902E9372DF00AB56D0 /* ColorScheme.swift */, - 55373F912E9372DF00AB56D0 /* DisplayPositionable.swift */, - 55373F932E9372DF00AB56D0 /* KeyEquivalent.swift */, - 55373F942E9372DF00AB56D0 /* NotificationCenter+Helper.swift */, - 55CE5BC52FFCBC8500B0F3F5 /* VDSymbol.swift */, - 55373F962E9372DF00AB56D0 /* ViewLinkable.swift */, - ); - path = Common; - sourceTree = ""; - }; - 55373F9E2E9372DF00AB56D0 /* CommonPrefs */ = { - isa = PBXGroup; - children = ( - 55373F982E9372DF00AB56D0 /* CommonPrefs.swift */, - 55373F992E9372DF00AB56D0 /* CommonPrefs+FileCompare.swift */, - 55373F9A2E9372DF00AB56D0 /* CommonPrefs+FolderCompare.swift */, - 55373F9B2E9372DF00AB56D0 /* CommonPrefs+Font.swift */, - 55373F9C2E9372DF00AB56D0 /* CommonPrefs+Helper.swift */, - 55373F9D2E9372DF00AB56D0 /* CommonPrefs+Name.swift */, - ); - path = CommonPrefs; - sourceTree = ""; - }; - 55373FA32E9372DF00AB56D0 /* Window */ = { - isa = PBXGroup; - children = ( - 55373F9F2E9372DF00AB56D0 /* DocumentWindow.swift */, - 55373FA02E9372DF00AB56D0 /* NSArrayController+Paths.swift */, - 55373FA12E9372DF00AB56D0 /* PathChooser.swift */, - 55373FA22E9372DF00AB56D0 /* RecentDocumentPopupMenu.swift */, - 55BCFE462F627DDF00E704D8 /* SessionPopupButton.swift */, - ); - path = Window; - sourceTree = ""; - }; - 55373FAA2E9372DF00AB56D0 /* Document */ = { - isa = PBXGroup; - children = ( - 55373FA42E9372DF00AB56D0 /* DiffOpenerDelegate.swift */, - 55373FA52E9372DF00AB56D0 /* DocumentError.swift */, - 55373FA62E9372DF00AB56D0 /* SessionDiff.ItemType+Paths.swift */, - 55373FA72E9372DF00AB56D0 /* SessionTypeError.swift */, - 55373FA82E9372DF00AB56D0 /* VDDocument.swift */, - 55373FA92E9372DF00AB56D0 /* VDDocumentController.swift */, - 552B85072F5FE29400A9268A /* VDDocumentController+BlankDocument.swift */, - 55373FA32E9372DF00AB56D0 /* Window */, - ); - path = Document; - sourceTree = ""; - }; - 55373FAD2E9372DF00AB56D0 /* History */ = { - isa = PBXGroup; - children = ( - 55373FAB2E9372DF00AB56D0 /* HistoryEntity.swift */, - 55373FAC2E9372DF00AB56D0 /* HistorySessionManager.swift */, - ); - path = History; - sourceTree = ""; - }; - 55373FB42E9372DF00AB56D0 /* SessionDiff */ = { - isa = PBXGroup; - children = ( - 55373FAE2E9372DF00AB56D0 /* SessionDiff.swift */, - 55373FAF2E9372DF00AB56D0 /* SessionDiff+AlignRule.swift */, - 5536ADB92EF169820019EFBF /* SessionDiff+ExtraData.swift */, - 559590202EABAFC200B5AB0B /* SessionDiff+ItemComparator.swift */, - 55373FB12E9372DF00AB56D0 /* SessionDiff+NSSortDescriptor.swift */, - 55373FB22E9372DF00AB56D0 /* SessionDiff+ResolvePath.swift */, - 55373FB32E9372DF00AB56D0 /* SessionDiff+Types.swift */, - ); - path = SessionDiff; - sourceTree = ""; - }; - 55373FB52E9372DF00AB56D0 /* Core */ = { - isa = PBXGroup; - children = ( - 55373F972E9372DF00AB56D0 /* Common */, - 55373F9E2E9372DF00AB56D0 /* CommonPrefs */, - 55373FAA2E9372DF00AB56D0 /* Document */, - 55373FAD2E9372DF00AB56D0 /* History */, - 55373FB42E9372DF00AB56D0 /* SessionDiff */, - ); - path = Core; - sourceTree = ""; - }; - 55373FB92E9372DF00AB56D0 /* FilesTableView */ = { - isa = PBXGroup; - children = ( - 55373FB62E9372DF00AB56D0 /* FilesTableView.swift */, - 55373FB72E9372DF00AB56D0 /* FilesTableView+Menu.swift */, - 55373FB82E9372DF00AB56D0 /* FilesTableViewFindTextDelegate.swift */, - ); - path = FilesTableView; - sourceTree = ""; - }; - 55373FC42E9372DF00AB56D0 /* Components */ = { - isa = PBXGroup; - children = ( - 55373FBA2E9372DF00AB56D0 /* FileInfoBar.swift */, - 55373FBB2E9372DF00AB56D0 /* FilePanelView.swift */, - 55373FBC2E9372DF00AB56D0 /* FilePanelView+File.swift */, - 55373FBD2E9372DF00AB56D0 /* FilesScopeBar.swift */, - 55373FB92E9372DF00AB56D0 /* FilesTableView */, - 55373FBE2E9372DF00AB56D0 /* FileThumbnailView.swift */, - 5548ABA72F7FE2BB0064B964 /* LineDetailTextView.swift */, - 55373FBF2E9372DF00AB56D0 /* LineNumberTableCellView.swift */, - 55373FC02E9372DF00AB56D0 /* LineNumberTableRowView.swift */, - 55373FC12E9372DF00AB56D0 /* NSAlert+DirtyFiles.swift */, - 55BC333D2EDAC69C004035A0 /* RowHeightCalculator.swift */, - 55373FC22E9372DF00AB56D0 /* SaveFileAccessoryView.swift */, - 55373FC32E9372DF00AB56D0 /* TextFieldVerticalCentered.swift */, - ); - path = Components; - sourceTree = ""; - }; - 55373FDA2E9372DF00AB56D0 /* Controller */ = { - isa = PBXGroup; - children = ( - 55373FC52E9372DF00AB56D0 /* FilesWindowController.swift */, - 55373FC62E9372DF00AB56D0 /* FilesWindowController+Clipboard.swift */, - 55373FC72E9372DF00AB56D0 /* FilesWindowController+Common.swift */, - 55373FC82E9372DF00AB56D0 /* FilesWindowController+Document.swift */, - 55373FC92E9372DF00AB56D0 /* FilesWindowController+FileInfoBarDelegate.swift */, - 55373FCA2E9372DF00AB56D0 /* FilesWindowController+FilesScopeBar.swift */, - 55373FCB2E9372DF00AB56D0 /* FilesWindowController+FilesTableViewContextMenu.swift */, - 55373FCC2E9372DF00AB56D0 /* FilesWindowController+JumpLine.swift */, - 55373FCD2E9372DF00AB56D0 /* FilesWindowController+LinesDetail.swift */, - 55373FCE2E9372DF00AB56D0 /* FilesWindowController+Menu.swift */, - 55373FCF2E9372DF00AB56D0 /* FilesWindowController+MenuDelegate.swift */, - 55373FD02E9372DF00AB56D0 /* FilesWindowController+Navigate.swift */, - 55373FD12E9372DF00AB56D0 /* FilesWindowController+NSToolbarDelegate.swift */, - 55373FD22E9372DF00AB56D0 /* FilesWindowController+NSWindowDelegate.swift */, - 55373FD32E9372DF00AB56D0 /* FilesWindowController+PathControlDelegate.swift */, - 55373FD42E9372DF00AB56D0 /* FilesWindowController+Read.swift */, - 55BC33412EDACB09004035A0 /* FilesWindowController+RowHeightDataSource.swift */, - 55373FD52E9372DF00AB56D0 /* FilesWindowController+Save.swift */, - 551DE39E2ED422820067AB18 /* FilesWindowController+SessionPreferences.swift */, - 55373FD62E9372DF00AB56D0 /* FilesWindowController+Slider.swift */, - 55BC33422EDACB09004035A0 /* FilesWindowController+SplitViewDelegate.swift */, - 55373FD72E9372DF00AB56D0 /* FilesWindowController+TableView.swift */, - 55373FD82E9372DF00AB56D0 /* FilesWindowController+UICreation.swift */, - 55373FD92E9372DF00AB56D0 /* FilesWindowController+UISetup.swift */, - ); - path = Controller; - sourceTree = ""; - }; - 55373FDF2E9372DF00AB56D0 /* DiffResult */ = { - isa = PBXGroup; - children = ( - 55373FDB2E9372DF00AB56D0 /* DiffLine+Color.swift */, - 55373FDD2E9372DF00AB56D0 /* DiffChangeType+Color.swift */, - ); - path = DiffResult; - sourceTree = ""; - }; - 55373FE22E9372DF00AB56D0 /* Extensions */ = { - isa = PBXGroup; - children = ( - 55373FDF2E9372DF00AB56D0 /* DiffResult */, - 55373FE02E9372DF00AB56D0 /* DiffCountersItem+DiffResult.swift */, - 55373FE12E9372DF00AB56D0 /* FilesTableView+EditorData.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 55373FE42E9372DF00AB56D0 /* Windows */ = { - isa = PBXGroup; - children = ( - 551DE3A52ED4237B0067AB18 /* SessionPreferences */, - 55373FE32E9372DF00AB56D0 /* JumpToLineWindow.swift */, - ); - path = Windows; - sourceTree = ""; - }; - 55373FE52E9372DF00AB56D0 /* FilesCompare */ = { - isa = PBXGroup; - children = ( - 55373FC42E9372DF00AB56D0 /* Components */, - 55373FDA2E9372DF00AB56D0 /* Controller */, - 554B4A442E9B7B8F0023AAF0 /* DiffResult */, - 55373FE22E9372DF00AB56D0 /* Extensions */, - 55F42EE02F76798A009B211C /* Navigator */, - 55373FE42E9372DF00AB56D0 /* Windows */, - ); - path = FilesCompare; - sourceTree = ""; - }; - 55373FF32E9372DF00AB56D0 /* FoldersOutlineView */ = { - isa = PBXGroup; - children = ( - 55373FE62E9372DF00AB56D0 /* ColoredFoldersManager.swift */, - 55373FE72E9372DF00AB56D0 /* FoldersOutlineView.swift */, - 55373FE82E9372DF00AB56D0 /* FoldersOutlineView+Clipboard.swift */, - 55373FE92E9372DF00AB56D0 /* FoldersOutlineView+Columns.swift */, - 55373FEA2E9372DF00AB56D0 /* FoldersOutlineView+Enumerate.swift */, - 55373FEB2E9372DF00AB56D0 /* FoldersOutlineView+ExternalApp.swift */, - 55373FEC2E9372DF00AB56D0 /* FoldersOutlineView+FileCount.swift */, - 55373FED2E9372DF00AB56D0 /* FoldersOutlineView+Menu.swift */, - 55373FEE2E9372DF00AB56D0 /* FoldersOutlineView+VisibleItem.swift */, - 55373FEF2E9372DF00AB56D0 /* FoldersOutlineViewFindTextDelegate.swift */, - 55373FF02E9372DF00AB56D0 /* CompareItem+LeafPath.swift */, - 55373FF12E9372DF00AB56D0 /* CompareItem+NSTableCellView.swift */, - 55373FF22E9372DF00AB56D0 /* IconUtils+CompareItemIconUtils.swift */, - ); - path = FoldersOutlineView; - sourceTree = ""; - }; - 55373FF82E9372DF00AB56D0 /* Components */ = { - isa = PBXGroup; - children = ( - 55373FF32E9372DF00AB56D0 /* FoldersOutlineView */, - 55373FF42E9372DF00AB56D0 /* DisplayFiltersScopeBar.swift */, - 55373FF52E9372DF00AB56D0 /* FolderPanelView.swift */, - 55373FF62E9372DF00AB56D0 /* CompareItemTableCellView.swift */, - 55373FF72E9372DF00AB56D0 /* CompareItemTableRowView.swift */, - ); - path = Components; - sourceTree = ""; - }; - 55373FFC2E9372DF00AB56D0 /* Copy */ = { - isa = PBXGroup; - children = ( - 55373FFB2E9372DF00AB56D0 /* CopyFileOperationExecutor.swift */, - ); - path = Copy; - sourceTree = ""; - }; - 55373FFE2E9372DF00AB56D0 /* Delete */ = { - isa = PBXGroup; - children = ( - 55373FFD2E9372DF00AB56D0 /* DeleteFileOperationExecutor.swift */, - ); - path = Delete; - sourceTree = ""; - }; - 553740002E9372DF00AB56D0 /* Move */ = { - isa = PBXGroup; - children = ( - 55373FFF2E9372DF00AB56D0 /* MoveFileOperationExecutor.swift */, - ); - path = Move; - sourceTree = ""; - }; - 553740052E9372DF00AB56D0 /* ReplaceFile */ = { - isa = PBXGroup; - children = ( - 553740012E9372DF00AB56D0 /* ConfirmReplace.swift */, - 553740022E9372DF00AB56D0 /* NSAlert+ReplaceFile.swift */, - 553740032E9372DF00AB56D0 /* NSApplication.ModalResponse+ReplaceFile.swift */, - 553740042E9372DF00AB56D0 /* ReplaceFileAttributeKey.swift */, - ); - path = ReplaceFile; - sourceTree = ""; - }; - 5537400B2E9372DF00AB56D0 /* Progress */ = { - isa = PBXGroup; - children = ( - 553740052E9372DF00AB56D0 /* ReplaceFile */, - 553740062E9372DF00AB56D0 /* CompletionIndicator.swift */, - 553740072E9372DF00AB56D0 /* ErrorsView.swift */, - 553740082E9372DF00AB56D0 /* FileOperationManagerDelegateImpl.swift */, - 553740092E9372DF00AB56D0 /* ProgressIndicatorController.swift */, - 5537400A2E9372DF00AB56D0 /* ReplaceInfoView.swift */, - ); - path = Progress; - sourceTree = ""; - }; - 553740102E9372DF00AB56D0 /* Sync */ = { - isa = PBXGroup; - children = ( - 5537400C2E9372DF00AB56D0 /* SyncFileController.swift */, - 5537400D2E9372DF00AB56D0 /* SyncFileOperationExecutor.swift */, - 5537400E2E9372DF00AB56D0 /* SyncItemsInfo.swift */, - 5537400F2E9372DF00AB56D0 /* SyncOutlineView.swift */, - ); - path = Sync; - sourceTree = ""; - }; - 553740152E9372DF00AB56D0 /* Touch */ = { - isa = PBXGroup; - children = ( - 553740112E9372DF00AB56D0 /* DatePickersStackView.swift */, - 553740122E9372DF00AB56D0 /* TouchController.swift */, - 553740132E9372DF00AB56D0 /* TouchFileOperationExecutor.swift */, - 553740142E9372DF00AB56D0 /* TouchPickersStackView.swift */, - ); - path = Touch; - sourceTree = ""; - }; - 5537401A2E9372DF00AB56D0 /* FileSystemController */ = { - isa = PBXGroup; - children = ( - 55373FFC2E9372DF00AB56D0 /* Copy */, - 55373FFE2E9372DF00AB56D0 /* Delete */, - 553740162E9372DF00AB56D0 /* FileOperationExecutor.swift */, - 553740172E9372DF00AB56D0 /* FileSummaryView.swift */, - 553740182E9372DF00AB56D0 /* FileSystemController.swift */, - 55D352E62EFD0C4D00C944D5 /* FileSystemTestHelper.swift */, - 553740002E9372DF00AB56D0 /* Move */, - 553740192E9372DF00AB56D0 /* OperationSummaryView.swift */, - 5537400B2E9372DF00AB56D0 /* Progress */, - 553740102E9372DF00AB56D0 /* Sync */, - 553740152E9372DF00AB56D0 /* Touch */, - ); - path = FileSystemController; - sourceTree = ""; - }; - 5537401D2E9372DF00AB56D0 /* FolderReader */ = { - isa = PBXGroup; - children = ( - 5537401B2E9372DF00AB56D0 /* FolderReader+Detached.swift */, - 5537401C2E9372DF00AB56D0 /* MainThreadFolderReaderDelegateBridge.swift */, - ); - path = FolderReader; - sourceTree = ""; - }; - 553740352E9372DF00AB56D0 /* Controller */ = { - isa = PBXGroup; - children = ( - 55EC0A4A2F3868E4008201E1 /* CopyFilesTag.swift */, - 55F4A0012FEE100000A1B2C3 /* ExternalFileOperationContext.swift */, - 5537401A2E9372DF00AB56D0 /* FileSystemController */, - 5537401D2E9372DF00AB56D0 /* FolderReader */, - 5537401E2E9372DF00AB56D0 /* FolderSelectionInfo.swift */, - 5537401F2E9372DF00AB56D0 /* FoldersWindowController.swift */, - 553740202E9372DF00AB56D0 /* FoldersWindowController+BaseFolder.swift */, - 553740212E9372DF00AB56D0 /* FoldersWindowController+Comparison.swift */, - 553740222E9372DF00AB56D0 /* FoldersWindowController+ConsoleViewDelegate.swift */, - 553740232E9372DF00AB56D0 /* FoldersWindowController+DisplayFiltersScopeBarDelegate.swift */, - 553740242E9372DF00AB56D0 /* FoldersWindowController+Document.swift */, - 553740252E9372DF00AB56D0 /* FoldersWindowController+Exclude.swift */, - 553740262E9372DF00AB56D0 /* FoldersWindowController+FileSystemControllerDelegate.swift */, - 553740272E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineView.swift */, - 553740282E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineViewContextMenu.swift */, - 553740292E9372DF00AB56D0 /* FoldersWindowController+Menu.swift */, - 5537402A2E9372DF00AB56D0 /* FoldersWindowController+MenuDelegate.swift */, - 5537402B2E9372DF00AB56D0 /* FoldersWindowController+Navigation.swift */, - 5537402C2E9372DF00AB56D0 /* FoldersWindowController+NSToolbarDelegate.swift */, - 5537402D2E9372DF00AB56D0 /* FoldersWindowController+NSWindowDelegate.swift */, - 55F9BAF12FA7259B004EA543 /* FoldersWindowController+Observers.swift */, - 5537402E2E9372DF00AB56D0 /* FoldersWindowController+PathControlDelegate.swift */, - 5537402F2E9372DF00AB56D0 /* FoldersWindowController+QLPreviewPanel.swift */, - 553740302E9372DF00AB56D0 /* FoldersWindowController+Select.swift */, - 553740312E9372DF00AB56D0 /* FoldersWindowController+SessionPreferences.swift */, - 553740322E9372DF00AB56D0 /* FoldersWindowController+UICreation.swift */, - 553740332E9372DF00AB56D0 /* FoldersWindowController+UISetup.swift */, - 553740342E9372DF00AB56D0 /* OutlineViewItemDelegate.swift */, - ); - path = Controller; - sourceTree = ""; - }; - 553740382E9372DF00AB56D0 /* Extensions */ = { - isa = PBXGroup; - children = ( - 553740362E9372DF00AB56D0 /* CommonPrefs+ConsoleLog.swift */, - 553740372E9372DF00AB56D0 /* DiffCountersItem+CompareItem.swift */, - ); - path = Extensions; - sourceTree = ""; - }; - 553740452E9372DF00AB56D0 /* FileManager */ = { - isa = PBXGroup; - children = ( - 553740402E9372DF00AB56D0 /* CompareItem+Metadata.swift */, - 553740392E9372DF00AB56D0 /* CopyCompareItem.swift */, - 5537403A2E9372DF00AB56D0 /* DeleteCompareItem.swift */, - 5537403B2E9372DF00AB56D0 /* FileAttributes.swift */, - 55A585762F52BD88006DD544 /* FileOperationDestination.swift */, - 5537403C2E9372DF00AB56D0 /* FileOperationManager.swift */, - 5537403D2E9372DF00AB56D0 /* FileOperationManager+FileOperationManagerAction.swift */, - 5537403E2E9372DF00AB56D0 /* FileOperationManager+Util.swift */, - 5537403F2E9372DF00AB56D0 /* FolderManagerError.swift */, - 553740412E9372DF00AB56D0 /* MoveCompareItem.swift */, - 553740422E9372DF00AB56D0 /* PathTimestamps.swift */, - 553740432E9372DF00AB56D0 /* RenameCompareItem.swift */, - 553740442E9372DF00AB56D0 /* TouchCompareItem.swift */, - ); - path = FileManager; - sourceTree = ""; - }; - 553740492E9372DF00AB56D0 /* ComparatorOptions */ = { - isa = PBXGroup; - children = ( - 553740462E9372DF00AB56D0 /* ComparatorOptions.swift */, - 553740472E9372DF00AB56D0 /* ComparatorOptions+Description.swift */, - 553740482E9372DF00AB56D0 /* ComparatorOptions+Helper.swift */, - ); - path = ComparatorOptions; - sourceTree = ""; - }; - 553740582E9372DF00AB56D0 /* CompareItem */ = { - isa = PBXGroup; - children = ( - 553740492E9372DF00AB56D0 /* ComparatorOptions */, - 5595901E2EABA8A100B5AB0B /* CompareChangeType.swift */, - 5595901C2EABA7FE00B5AB0B /* CompareSummary.swift */, - 5537404B2E9372DF00AB56D0 /* DisplayOptions.swift */, - 5537404C2E9372DF00AB56D0 /* DisplayOptions+Helper.swift */, - 5537404E2E9372DF00AB56D0 /* FileExtraOptions.swift */, - 5537404F2E9372DF00AB56D0 /* CompareItem.swift */, - 553740502E9372DF00AB56D0 /* CompareItem+Accessors.swift */, - 553740512E9372DF00AB56D0 /* CompareItem+Clone.swift */, - 553740522E9372DF00AB56D0 /* CompareItem+Comparison.swift */, - 553740532E9372DF00AB56D0 /* CompareItem+Description.swift */, - 553740542E9372DF00AB56D0 /* CompareItem+FilterConfig.swift */, - 553740562E9372DF00AB56D0 /* CompareItem+Path.swift */, - 553740572E9372DF00AB56D0 /* CompareItem+VisibleItem.swift */, - ); - path = CompareItem; - sourceTree = ""; - }; - 5537405C2E9372DF00AB56D0 /* Navigator */ = { - isa = PBXGroup; - children = ( - 553740592E9372DF00AB56D0 /* CommonPrefs+DifferenceNavigator.swift */, - 5537405A2E9372DF00AB56D0 /* FoldersOutlineView+DifferenceNavigator.swift */, - 5537405B2E9372DF00AB56D0 /* CompareItem+DifferenceNavigator.swift */, - ); - path = Navigator; - sourceTree = ""; - }; - 553740622E9372DF00AB56D0 /* FolderReader */ = { - isa = PBXGroup; - children = ( - 5537405D2E9372DF00AB56D0 /* FilterConfig.swift */, - 5537405E2E9372DF00AB56D0 /* FolderReader.swift */, - 5537405F2E9372DF00AB56D0 /* FolderReader+Log.swift */, - 553740602E9372DF00AB56D0 /* FolderReaderDelegate.swift */, - 553740612E9372DF00AB56D0 /* RefreshInfo.swift */, - ); - path = FolderReader; - sourceTree = ""; - }; - 553740672E9372DF00AB56D0 /* ItemComparator */ = { - isa = PBXGroup; - children = ( - 559590222EABB06100B5AB0B /* ItemComparator.swift */, - 559590232EABB06100B5AB0B /* ItemComparator+Align.swift */, - 559590242EABB06100B5AB0B /* ItemComparator+AlignRegularExpression.swift */, - 559590252EABB06100B5AB0B /* ItemComparator+Compare.swift */, - ); - path = ItemComparator; - sourceTree = ""; - }; - 553740682E9372DF00AB56D0 /* Services */ = { - isa = PBXGroup; - children = ( - 553740622E9372DF00AB56D0 /* FolderReader */, - 553740672E9372DF00AB56D0 /* ItemComparator */, - ); - path = Services; - sourceTree = ""; - }; - 5537406E2E9372DF00AB56D0 /* Validators */ = { - isa = PBXGroup; - children = ( - 553740692E9372DF00AB56D0 /* FolderSelectionInfo+CompareActionValidator.swift */, - 5537406A2E9372DF00AB56D0 /* FolderSelectionInfo+FileSystemActionValidator.swift */, - 5537406B2E9372DF00AB56D0 /* FolderSelectionInfo+FilterActionValidator.swift */, - 5537406C2E9372DF00AB56D0 /* FolderSelectionInfo+FolderActionValidator.swift */, - 5537406D2E9372DF00AB56D0 /* FolderSelectionInfo+ViewerActionValidator.swift */, - ); - path = Validators; - sourceTree = ""; - }; - 553740742E9372DF00AB56D0 /* VisibleItem */ = { - isa = PBXGroup; - children = ( - 5537406F2E9372DF00AB56D0 /* VisibleItem.swift */, - 553740702E9372DF00AB56D0 /* VisibleItem+Find.swift */, - 553740712E9372DF00AB56D0 /* VisibleItem+Log.swift */, - 553740722E9372DF00AB56D0 /* VisibleItem+QLPreviewItem.swift */, - 553740732E9372DF00AB56D0 /* VisibleItem+Sort.swift */, - ); - path = VisibleItem; - sourceTree = ""; - }; - 553740772E9372DF00AB56D0 /* FolderCompareInfoWindow */ = { - isa = PBXGroup; - children = ( - 553740752E9372DF00AB56D0 /* DescriptionOutlineNode+FolderCompareInfo.swift */, - 553740762E9372DF00AB56D0 /* FolderCompareInfoWindow.swift */, - ); - path = FolderCompareInfoWindow; - sourceTree = ""; - }; - 553740782E9372DF00AB56D0 /* Windows */ = { - isa = PBXGroup; - children = ( - 553740772E9372DF00AB56D0 /* FolderCompareInfoWindow */, - ); - path = Windows; - sourceTree = ""; - }; - 553740792E9372DF00AB56D0 /* FoldersCompare */ = { - isa = PBXGroup; - children = ( - 55373FF82E9372DF00AB56D0 /* Components */, - 553740352E9372DF00AB56D0 /* Controller */, - 553740382E9372DF00AB56D0 /* Extensions */, - 553740452E9372DF00AB56D0 /* FileManager */, - 553740582E9372DF00AB56D0 /* CompareItem */, - 5537405C2E9372DF00AB56D0 /* Navigator */, - 553740682E9372DF00AB56D0 /* Services */, - 5537406E2E9372DF00AB56D0 /* Validators */, - 553740742E9372DF00AB56D0 /* VisibleItem */, - 553740782E9372DF00AB56D0 /* Windows */, - ); - path = FoldersCompare; - sourceTree = ""; - }; - 5537407F2E9372DF00AB56D0 /* HistoryController */ = { - isa = PBXGroup; - children = ( - 5537407A2E9372DF00AB56D0 /* HistoryController.swift */, - 5537407B2E9372DF00AB56D0 /* HistoryEntityTableCellView.swift */, - 5537407C2E9372DF00AB56D0 /* HistoryFetchedResultsControllerDelegate.swift */, - 5537407D2E9372DF00AB56D0 /* HistorySearchField.swift */, - 5537407E2E9372DF00AB56D0 /* HistoryTableView.swift */, - ); - path = HistoryController; - sourceTree = ""; - }; - 553740852E9372DF00AB56D0 /* Box */ = { - isa = PBXGroup; - children = ( - 551DE3B72ED4399B0067AB18 /* PreferencesBoxDataSource+Default.swift */, - 553740802E9372DF00AB56D0 /* ComparisonStandardUserDataSource.swift */, - 553740812E9372DF00AB56D0 /* PreferencesBox.swift */, - 553740822E9372DF00AB56D0 /* PreferencesBoxDataSource.swift */, - 553740832E9372DF00AB56D0 /* PreferencesCheckbox.swift */, - 553740842E9372DF00AB56D0 /* StandardUserPreferencesBoxDataSource.swift */, - ); - path = Box; - sourceTree = ""; - }; - 553740882E9372DF00AB56D0 /* Controllers */ = { - isa = PBXGroup; - children = ( - 553740862E9372DF00AB56D0 /* BasePreferences.swift */, - 553740872E9372DF00AB56D0 /* Preferences.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - 5537408D2E9372DF00AB56D0 /* Confirmations */ = { - isa = PBXGroup; - children = ( - 553740892E9372DF00AB56D0 /* ConfirmationsDocumentsBox.swift */, - 5537408A2E9372DF00AB56D0 /* ConfirmationsFilesBox.swift */, - 5537408B2E9372DF00AB56D0 /* ConfirmationsFoldersBox.swift */, - 5537408C2E9372DF00AB56D0 /* ConfirmationsPreferencesPanel.swift */, - ); - path = Confirmations; - sourceTree = ""; - }; - 553740922E9372DF00AB56D0 /* Folder */ = { - isa = PBXGroup; - children = ( - 5537408E2E9372DF00AB56D0 /* DifferenceNavigatorBox.swift */, - 5537408F2E9372DF00AB56D0 /* FolderPreferencesPanel.swift */, - 553740902E9372DF00AB56D0 /* FoldersTraversalBox.swift */, - 553740912E9372DF00AB56D0 /* FolderViewBox.swift */, - ); - path = Folder; - sourceTree = ""; - }; - 553740952E9372DF00AB56D0 /* Font */ = { - isa = PBXGroup; - children = ( - 553740932E9372DF00AB56D0 /* FontBox.swift */, - 553740942E9372DF00AB56D0 /* FontPreferencesPanel.swift */, - ); - path = Font; - sourceTree = ""; - }; - 5537409A2E9372DF00AB56D0 /* General */ = { - isa = PBXGroup; - children = ( - 553740962E9372DF00AB56D0 /* AppearanceBox.swift */, - 553740972E9372DF00AB56D0 /* FolderComparisonBox.swift */, - 553740982E9372DF00AB56D0 /* GeneralPreferencesPanel.swift */, - 553740992E9372DF00AB56D0 /* PreferredEditorBox.swift */, - ); - path = General; - sourceTree = ""; - }; - 5537409D2E9372DF00AB56D0 /* Keyboard */ = { - isa = PBXGroup; - children = ( - 5537409B2E9372DF00AB56D0 /* KeyboardDocumentBox.swift */, - 5537409C2E9372DF00AB56D0 /* KeyboardPreferencesPanel.swift */, - ); - path = Keyboard; - sourceTree = ""; - }; - 553740A02E9372DF00AB56D0 /* Text */ = { - isa = PBXGroup; - children = ( - 551DE3B52ED42B4E0067AB18 /* FileComparisonBox.swift */, - 552430402F7673F800951F1A /* FileDifferenceNavigatorBox.swift */, - 5537409E2E9372DF00AB56D0 /* TextPreferencesPanel.swift */, - 55450B162ED4790B0048619F /* TextPreferencesPanel+PreferencesBoxDataSource.swift */, - 5537409F2E9372DF00AB56D0 /* VisualizationBox.swift */, - ); - path = Text; - sourceTree = ""; - }; - 553740A22E9372DF00AB56D0 /* TrustedPaths */ = { - isa = PBXGroup; - children = ( - 553740A12E9372DF00AB56D0 /* TrustedPathsPreferencesPanel.swift */, - ); - path = TrustedPaths; - sourceTree = ""; - }; - 553740A42E9372DF00AB56D0 /* Panels */ = { - isa = PBXGroup; - children = ( - 5537408D2E9372DF00AB56D0 /* Confirmations */, - 553740922E9372DF00AB56D0 /* Folder */, - 553740952E9372DF00AB56D0 /* Font */, - 5537409A2E9372DF00AB56D0 /* General */, - 5537409D2E9372DF00AB56D0 /* Keyboard */, - 553740A02E9372DF00AB56D0 /* Text */, - 553740A22E9372DF00AB56D0 /* TrustedPaths */, - 553740A32E9372DF00AB56D0 /* NSStackView+PreferencesPanel.swift */, - ); - path = Panels; - sourceTree = ""; - }; - 553740A52E9372DF00AB56D0 /* Main */ = { - isa = PBXGroup; - children = ( - 553740882E9372DF00AB56D0 /* Controllers */, - 553740A42E9372DF00AB56D0 /* Panels */, - ); - path = Main; - sourceTree = ""; - }; - 553740A82E9372DF00AB56D0 /* Controllers */ = { - isa = PBXGroup; - children = ( - 553740A62E9372DF00AB56D0 /* SessionPreferencesWindow.swift */, - 553740A72E9372DF00AB56D0 /* SessionPreferencesWindow+Data.swift */, - ); - path = Controllers; - sourceTree = ""; - }; - 553740B02E9372DF00AB56D0 /* AlignRule */ = { - isa = PBXGroup; - children = ( - 553740A92E9372DF00AB56D0 /* AlignmentPanel.swift */, - 553740AA2E9372DF00AB56D0 /* AlignRuleWindow.swift */, - 553740AB2E9372DF00AB56D0 /* AlignTestResultBox.swift */, - 553740AC2E9372DF00AB56D0 /* ExpressionBox.swift */, - 553740AD2E9372DF00AB56D0 /* ExpressionBox+Menu.swift */, - 553740AE2E9372DF00AB56D0 /* FileNameCaseBox.swift */, - 553740AF2E9372DF00AB56D0 /* UserDefinedRulesBox.swift */, - ); - path = AlignRule; - sourceTree = ""; - }; - 553740B22E9372DF00AB56D0 /* Comparison */ = { - isa = PBXGroup; - children = ( - 553740B12E9372DF00AB56D0 /* SessionPreferencesComparisonPanel.swift */, - ); - path = Comparison; - sourceTree = ""; - }; - 553740B62E9372DF00AB56D0 /* Filters */ = { - isa = PBXGroup; - children = ( - 553740B32E9372DF00AB56D0 /* FiltersPredicateEditor.swift */, - 553740B42E9372DF00AB56D0 /* SessionPreferencesFiltersBox.swift */, - 553740B52E9372DF00AB56D0 /* SessionPreferencesFiltersPanel.swift */, - ); - path = Filters; - sourceTree = ""; - }; - 553740B72E9372DF00AB56D0 /* Panels */ = { - isa = PBXGroup; - children = ( - 553740B02E9372DF00AB56D0 /* AlignRule */, - 553740B22E9372DF00AB56D0 /* Comparison */, - 553740B62E9372DF00AB56D0 /* Filters */, - ); - path = Panels; - sourceTree = ""; - }; - 553740B82E9372DF00AB56D0 /* Session */ = { - isa = PBXGroup; - children = ( - 553740A82E9372DF00AB56D0 /* Controllers */, - 553740B72E9372DF00AB56D0 /* Panels */, - ); - path = Session; - sourceTree = ""; - }; - 553740BA2E9372DF00AB56D0 /* Preferences */ = { - isa = PBXGroup; - children = ( - 553740852E9372DF00AB56D0 /* Box */, - 553740A52E9372DF00AB56D0 /* Main */, - 553740B82E9372DF00AB56D0 /* Session */, - 553740B92E9372DF00AB56D0 /* PreferencesPanelDataSource.swift */, - ); - path = Preferences; - sourceTree = ""; - }; - 553740BB2E9372DF00AB56D0 /* Features */ = { - isa = PBXGroup; - children = ( - 55373FE52E9372DF00AB56D0 /* FilesCompare */, - 553740792E9372DF00AB56D0 /* FoldersCompare */, - 5537407F2E9372DF00AB56D0 /* HistoryController */, - 553740BA2E9372DF00AB56D0 /* Preferences */, - ); - path = Features; - sourceTree = ""; - }; - 553740C02E9372DF00AB56D0 /* DiffEngine */ = { - isa = PBXGroup; - children = ( - 553740BC2E9372DF00AB56D0 /* UDiffScriptBuilder.h */, - 553740BD2E9372DF00AB56D0 /* UDiffScriptBuilder.m */, - 553740BE2E9372DF00AB56D0 /* UnifiedDiff.h */, - 553740BF2E9372DF00AB56D0 /* UnifiedDiff.m */, - ); - path = DiffEngine; - sourceTree = ""; - }; - 553740C92E9372DF00AB56D0 /* FileManager */ = { - isa = PBXGroup; - children = ( - 553740C72E9372DF00AB56D0 /* BigFileFileOperationManager.h */, - 553740C82E9372DF00AB56D0 /* BigFileFileOperationManager.m */, - ); - path = FileManager; - sourceTree = ""; - }; - 553740DC2E9372DF00AB56D0 /* Predicate */ = { - isa = PBXGroup; - children = ( - 553740D42E9372DF00AB56D0 /* NSPredicate+Objc.h */, - 553740D52E9372DF00AB56D0 /* NSPredicate+Objc.m */, - 553740D62E9372DF00AB56D0 /* TitlePredicateEditorRowTemplate.h */, - 553740D72E9372DF00AB56D0 /* TitlePredicateEditorRowTemplate.m */, - 553740D82E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.h */, - 553740D92E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.m */, - 553740DA2E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.h */, - 553740DB2E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.m */, - ); - path = Predicate; - sourceTree = ""; - }; - 553740E12E9372DF00AB56D0 /* Legacy */ = { - isa = PBXGroup; - children = ( - 553740C02E9372DF00AB56D0 /* DiffEngine */, - 553740C92E9372DF00AB56D0 /* FileManager */, - 553740DC2E9372DF00AB56D0 /* Predicate */, - ); - path = Legacy; - sourceTree = ""; - }; - 553740E32E9372DF00AB56D0 /* AppleScript */ = { - isa = PBXGroup; - children = ( - 553740E22E9372DF00AB56D0 /* OpenDiffCommand.swift */, - ); - path = AppleScript; - sourceTree = ""; - }; - 553740E42E9372DF00AB56D0 /* Services */ = { - isa = PBXGroup; - children = ( - 553740E32E9372DF00AB56D0 /* AppleScript */, - ); - path = Services; - sourceTree = ""; - }; - 553740E62E9372DF00AB56D0 /* Appearance */ = { - isa = PBXGroup; - children = ( - 553740E52E9372DF00AB56D0 /* NSAppearance+DarkMode.swift */, - ); - path = Appearance; - sourceTree = ""; - }; - 553740E82E9372DF00AB56D0 /* Collections */ = { - isa = PBXGroup; - children = ( - 553740E72E9372DF00AB56D0 /* Array+MoveIndexes.swift */, - ); - path = Collections; - sourceTree = ""; - }; - 553740EA2E9372DF00AB56D0 /* Color */ = { - isa = PBXGroup; - children = ( - 553740E92E9372DF00AB56D0 /* NSColor+Hex.swift */, - ); - path = Color; - sourceTree = ""; - }; - 553740EC2E9372DF00AB56D0 /* CoreData */ = { - isa = PBXGroup; - children = ( - 553740EB2E9372DF00AB56D0 /* NSManagedObjectContext+Helpers.swift */, - ); - path = CoreData; - sourceTree = ""; - }; - 553740EE2E9372DF00AB56D0 /* Date */ = { - isa = PBXGroup; - children = ( - 553740ED2E9372DF00AB56D0 /* TimeInterval+Helper.swift */, - ); - path = Date; - sourceTree = ""; - }; - 553740F02E9372DF00AB56D0 /* Error */ = { - isa = PBXGroup; - children = ( - 553740EF2E9372DF00AB56D0 /* NSError+Format.swift */, - ); - path = Error; - sourceTree = ""; - }; - 553740F22E9372DF00AB56D0 /* FileManager */ = { - isa = PBXGroup; - children = ( - 553740F12E9372DF00AB56D0 /* FileManager+Attributes.swift */, - ); - path = FileManager; - sourceTree = ""; - }; - 553740F42E9372DF00AB56D0 /* Formatters */ = { - isa = PBXGroup; - children = ( - 553740F32E9372DF00AB56D0 /* DateFormatter+Helper.swift */, - ); - path = Formatters; - sourceTree = ""; - }; - 553740F62E9372DF00AB56D0 /* Image */ = { - isa = PBXGroup; - children = ( - 553740F52E9372DF00AB56D0 /* NSImage+Tint.swift */, - ); - path = Image; - sourceTree = ""; - }; - 553740F82E9372DF00AB56D0 /* Menu */ = { - isa = PBXGroup; - children = ( - 553740F72E9372DF00AB56D0 /* NSMenu+File.swift */, - ); - path = Menu; - sourceTree = ""; - }; - 553740FA2E9372DF00AB56D0 /* OptionSet */ = { - isa = PBXGroup; - children = ( - 55FE89492F054B260003864A /* FlagSet.swift */, - 553740F92E9372DF00AB56D0 /* OptionSet+Toggle.swift */, - ); - path = OptionSet; - sourceTree = ""; - }; - 553740FE2E9372DF00AB56D0 /* Panel */ = { - isa = PBXGroup; - children = ( - 553740FB2E9372DF00AB56D0 /* NSAlert+Helper.swift */, - 553740FC2E9372DF00AB56D0 /* NSOpenPanel+Application.swift */, - 553740FD2E9372DF00AB56D0 /* QLPreviewPanel.swift */, - ); - path = Panel; - sourceTree = ""; - }; - 553741002E9372DF00AB56D0 /* Pasteboard */ = { - isa = PBXGroup; - children = ( - 553740FF2E9372DF00AB56D0 /* NSPasteboard+Helper.swift */, - ); - path = Pasteboard; - sourceTree = ""; - }; - 553741022E9372DF00AB56D0 /* Predicate */ = { - isa = PBXGroup; - children = ( - 553741012E9372DF00AB56D0 /* NSPredicateEditor+Fix.swift */, - ); - path = Predicate; - sourceTree = ""; - }; - 553741082E9372DF00AB56D0 /* String */ = { - isa = PBXGroup; - children = ( - 553741032E9372DF00AB56D0 /* String+Helper.swift */, - 553741042E9372DF00AB56D0 /* String+Highlights.swift */, - 553741052E9372DF00AB56D0 /* String+Occcurrences.swift */, - 553741062E9372DF00AB56D0 /* String+Path.swift */, - 553741072E9372DF00AB56D0 /* String+RegularExpression.swift */, - ); - path = String; - sourceTree = ""; - }; - 5537410B2E9372DF00AB56D0 /* TableView */ = { - isa = PBXGroup; - children = ( - 553741092E9372DF00AB56D0 /* NSTableView+Font.swift */, - 5537410A2E9372DF00AB56D0 /* NSTableView+Row.swift */, - ); - path = TableView; - sourceTree = ""; - }; - 5537410E2E9372DF00AB56D0 /* Toolbar */ = { - isa = PBXGroup; - children = ( - 5537410C2E9372DF00AB56D0 /* NSToolbar+Create.swift */, - 5537410D2E9372DF00AB56D0 /* NSToolbarItem+Create.swift */, - ); - path = Toolbar; - sourceTree = ""; - }; - 553741162E9372DF00AB56D0 /* URL */ = { - isa = PBXGroup; - children = ( - 5537410F2E9372DF00AB56D0 /* URL+FileManager.swift */, - 553741102E9372DF00AB56D0 /* URL+Finder.swift */, - 553741112E9372DF00AB56D0 /* URL+Metadata.swift */, - 553741122E9372DF00AB56D0 /* URL+Path.swift */, - 553741132E9372DF00AB56D0 /* URL+ResourceFork.swift */, - 553741142E9372DF00AB56D0 /* URL+StructuredContent.swift */, - 553741152E9372DF00AB56D0 /* URL+SymLink.swift */, - ); - path = URL; - sourceTree = ""; - }; - 553741182E9372DF00AB56D0 /* Box */ = { - isa = PBXGroup; - children = ( - 553741172E9372DF00AB56D0 /* NSBox+Helper.swift */, - ); - path = Box; - sourceTree = ""; - }; - 5537411B2E9372DF00AB56D0 /* Button */ = { - isa = PBXGroup; - children = ( - 553741192E9372DF00AB56D0 /* NSButton+Helper.swift */, - 5537411A2E9372DF00AB56D0 /* NSPopUpButton+Helper.swift */, - ); - path = Button; - sourceTree = ""; - }; - 5537411D2E9372DF00AB56D0 /* ProgressIndicator */ = { - isa = PBXGroup; - children = ( - 5537411C2E9372DF00AB56D0 /* NSProgressIndicator+Helper.swift */, - ); - path = ProgressIndicator; - sourceTree = ""; - }; - 5537411F2E9372DF00AB56D0 /* TextField */ = { - isa = PBXGroup; - children = ( - 5537411E2E9372DF00AB56D0 /* NSTextField+Helper.swift */, - ); - path = TextField; - sourceTree = ""; - }; - 553741212E9372DF00AB56D0 /* TextView */ = { - isa = PBXGroup; - children = ( - 553741202E9372DF00AB56D0 /* NSTextView+Style.swift */, - ); - path = TextView; - sourceTree = ""; - }; - 553741222E9372DF00AB56D0 /* View */ = { - isa = PBXGroup; - children = ( - 553741182E9372DF00AB56D0 /* Box */, - 5537411B2E9372DF00AB56D0 /* Button */, - 5537411D2E9372DF00AB56D0 /* ProgressIndicator */, - 5537411F2E9372DF00AB56D0 /* TextField */, - 553741212E9372DF00AB56D0 /* TextView */, - ); - path = View; - sourceTree = ""; - }; - 553741242E9372DF00AB56D0 /* Workspace */ = { - isa = PBXGroup; - children = ( - 553741232E9372DF00AB56D0 /* NSWorkspace+Finder.swift */, - ); - path = Workspace; - sourceTree = ""; - }; - 553741252E9372DF00AB56D0 /* Extensions */ = { - isa = PBXGroup; - children = ( - 553740E62E9372DF00AB56D0 /* Appearance */, - 553740E82E9372DF00AB56D0 /* Collections */, - 553740EA2E9372DF00AB56D0 /* Color */, - 553740EC2E9372DF00AB56D0 /* CoreData */, - 553740EE2E9372DF00AB56D0 /* Date */, - 558DEAC52EA263DE00F05A10 /* Encoding */, - 553740F02E9372DF00AB56D0 /* Error */, - 553740F22E9372DF00AB56D0 /* FileManager */, - 553740F42E9372DF00AB56D0 /* Formatters */, - 553740F62E9372DF00AB56D0 /* Image */, - 553740F82E9372DF00AB56D0 /* Menu */, - 553740FA2E9372DF00AB56D0 /* OptionSet */, - 553740FE2E9372DF00AB56D0 /* Panel */, - 553741002E9372DF00AB56D0 /* Pasteboard */, - 553741022E9372DF00AB56D0 /* Predicate */, - 553741082E9372DF00AB56D0 /* String */, - 5537410B2E9372DF00AB56D0 /* TableView */, - 5537410E2E9372DF00AB56D0 /* Toolbar */, - 553741162E9372DF00AB56D0 /* URL */, - 553741222E9372DF00AB56D0 /* View */, - 558DEACF2EA2642100F05A10 /* Window */, - 553741242E9372DF00AB56D0 /* Workspace */, - ); - path = Extensions; - sourceTree = ""; - }; - 553741272E9372DF00AB56D0 /* Document */ = { - isa = PBXGroup; - children = ( - 553741262E9372DF00AB56D0 /* SecureBookmark.swift */, - ); - path = Document; - sourceTree = ""; - }; - 553741292E9372DF00AB56D0 /* Encoding */ = { - isa = PBXGroup; - children = ( - 553741282E9372DF00AB56D0 /* EncodingError.swift */, - ); - path = Encoding; - sourceTree = ""; - }; - 5537412B2E9372DF00AB56D0 /* Event */ = { - isa = PBXGroup; - children = ( - 5537412A2E9372DF00AB56D0 /* NSEvent+VirtualKeys.swift */, - ); - path = Event; - sourceTree = ""; - }; - 5537412E2E9372DF00AB56D0 /* Formatters */ = { - isa = PBXGroup; - children = ( - 5537412C2E9372DF00AB56D0 /* FileSizeFormatter.swift */, - 5537412D2E9372DF00AB56D0 /* IntegerFormatter.swift */, - ); - path = Formatters; - sourceTree = ""; - }; - 553741312E9372DF00AB56D0 /* Image */ = { - isa = PBXGroup; - children = ( - 5537412F2E9372DF00AB56D0 /* IconUtils.swift */, - 553741302E9372DF00AB56D0 /* NoodleCustomImageRep.swift */, - 55BC333F2EDAC96F004035A0 /* TiledImageView.swift */, - ); - path = Image; - sourceTree = ""; - }; - 553741362E9372DF00AB56D0 /* IO */ = { - isa = PBXGroup; - children = ( - 553741322E9372DF00AB56D0 /* BufferedInputStream.swift */, - 553741332E9372DF00AB56D0 /* CompareUtil.swift */, - 553741342E9372DF00AB56D0 /* FileError.swift */, - 553741352E9372DF00AB56D0 /* PowerAssertion.swift */, - ); - path = IO; - sourceTree = ""; - }; - 553741382E9372DF00AB56D0 /* Menu */ = { - isa = PBXGroup; - children = ( - 553741372E9372DF00AB56D0 /* AttributedMenuItem.swift */, - ); - path = Menu; - sourceTree = ""; - }; - 5537413C2E9372DF00AB56D0 /* OpenEditor */ = { - isa = PBXGroup; - children = ( - 553741392E9372DF00AB56D0 /* OpenEditor.swift */, - 5537413A2E9372DF00AB56D0 /* OpenEditorAttribute.swift */, - 5537413B2E9372DF00AB56D0 /* OpenEditorError.swift */, - ); - path = OpenEditor; - sourceTree = ""; - }; - 5537413E2E9372DF00AB56D0 /* PopupButton */ = { - isa = PBXGroup; - children = ( - 5537413D2E9372DF00AB56D0 /* PreferredEditorPopupCell.swift */, - ); - path = PopupButton; - sourceTree = ""; - }; - 553741402E9372DF00AB56D0 /* String */ = { - isa = PBXGroup; - children = ( - 5537413F2E9372DF00AB56D0 /* VisibleWhitespaces.swift */, - ); - path = String; - sourceTree = ""; - }; - 553741442E9372DF00AB56D0 /* TableView */ = { - isa = PBXGroup; - children = ( - 553741412E9372DF00AB56D0 /* DescriptionOutlineNode.swift */, - 553741422E9372DF00AB56D0 /* TableViewCommon.swift */, - 553741432E9372DF00AB56D0 /* TableViewContextMenuDelegate.swift */, - ); - path = TableView; - sourceTree = ""; - }; - 553741472E9372DF00AB56D0 /* TextField */ = { - isa = PBXGroup; - children = ( - 553741452E9372DF00AB56D0 /* NSTextFieldCell+Helper.swift */, - 553741462E9372DF00AB56D0 /* RSVerticallyCenteredTextFieldCell.swift */, - ); - path = TextField; - sourceTree = ""; - }; - 553741492E9372DF00AB56D0 /* Toolbar */ = { - isa = PBXGroup; - children = ( - 553741482E9372DF00AB56D0 /* CustomValidationToolbarItem.swift */, - ); - path = Toolbar; - sourceTree = ""; - }; - 5537414B2E9372DF00AB56D0 /* ActionBar */ = { - isa = PBXGroup; - children = ( - 5537414A2E9372DF00AB56D0 /* ActionBarView.swift */, - ); - path = ActionBar; - sourceTree = ""; - }; - 5537414E2E9372DF00AB56D0 /* Button */ = { - isa = PBXGroup; - children = ( - 5537414C2E9372DF00AB56D0 /* LinkButton.swift */, - 5537414D2E9372DF00AB56D0 /* StandardButtons.swift */, - ); - path = Button; - sourceTree = ""; - }; - 553741512E9372DF00AB56D0 /* Console */ = { - isa = PBXGroup; - children = ( - 5537414F2E9372DF00AB56D0 /* ConsoleToolbarView.swift */, - 553741502E9372DF00AB56D0 /* ConsoleView.swift */, - ); - path = Console; - sourceTree = ""; - }; - 553741542E9372DF00AB56D0 /* DualPane */ = { - isa = PBXGroup; - children = ( - 553741522E9372DF00AB56D0 /* DualPaneSplitView.swift */, - 553741532E9372DF00AB56D0 /* DualPaneSplitViewDelegate.swift */, - ); - path = DualPane; - sourceTree = ""; - }; - 553741572E9372DF00AB56D0 /* FileDrop */ = { - isa = PBXGroup; - children = ( - 553741552E9372DF00AB56D0 /* FileDropView.swift */, - 553741562E9372DF00AB56D0 /* FileDropView+Helper.swift */, - ); - path = FileDrop; - sourceTree = ""; - }; - 553741592E9372DF00AB56D0 /* ProgressBar */ = { - isa = PBXGroup; - children = ( - 553741582E9372DF00AB56D0 /* ProgressBarView.swift */, - ); - path = ProgressBar; - sourceTree = ""; - }; - 5537415B2E9372DF00AB56D0 /* SynchroScroll */ = { - isa = PBXGroup; - children = ( - 5537415A2E9372DF00AB56D0 /* SynchroScrollView.swift */, - ); - path = SynchroScroll; +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 55C9D891305A665A00D4FD1E /* Sources */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Sources; sourceTree = ""; }; - 5537415D2E9372DF00AB56D0 /* TextField */ = { - isa = PBXGroup; - children = ( - 5537415C2E9372DF00AB56D0 /* TextFieldSelectionHolder.swift */, + 55C9DA5A305A697C00D4FD1E /* Tests */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 55C9DA8A305A697C00D4FD1E /* Exceptions for "Tests" folder in "VisualDifferTests" target */, ); - path = TextField; + path = Tests; sourceTree = ""; }; - 5537415E2E9372DF00AB56D0 /* View */ = { - isa = PBXGroup; - children = ( - 5537414B2E9372DF00AB56D0 /* ActionBar */, - 5537414E2E9372DF00AB56D0 /* Button */, - 553741512E9372DF00AB56D0 /* Console */, - 553741542E9372DF00AB56D0 /* DualPane */, - 558DEACA2EA263F500F05A10 /* Encoding */, - 553741572E9372DF00AB56D0 /* FileDrop */, - 553741592E9372DF00AB56D0 /* ProgressBar */, - 5537415B2E9372DF00AB56D0 /* SynchroScroll */, - 5537415D2E9372DF00AB56D0 /* TextField */, - ); - path = View; + 55C9DA95305A69E900D4FD1E /* visdiff */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = visdiff; sourceTree = ""; }; - 553741612E9372DF00AB56D0 /* Window */ = { - isa = PBXGroup; - children = ( - 5537415F2E9372DF00AB56D0 /* WindowCancelOperation.swift */, - 553741602E9372DF00AB56D0 /* WindowOSD.swift */, +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 55944CC42E5C8D9F00FB1F8C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + files = ( ); - path = Window; - sourceTree = ""; }; - 553741622E9372DF00AB56D0 /* Utilities */ = { - isa = PBXGroup; - children = ( - 553741272E9372DF00AB56D0 /* Document */, - 553741292E9372DF00AB56D0 /* Encoding */, - 5537412B2E9372DF00AB56D0 /* Event */, - 5537412E2E9372DF00AB56D0 /* Formatters */, - 553741312E9372DF00AB56D0 /* Image */, - 553741362E9372DF00AB56D0 /* IO */, - 553741382E9372DF00AB56D0 /* Menu */, - 5537413C2E9372DF00AB56D0 /* OpenEditor */, - 5537413E2E9372DF00AB56D0 /* PopupButton */, - 553741402E9372DF00AB56D0 /* String */, - 553741442E9372DF00AB56D0 /* TableView */, - 553741472E9372DF00AB56D0 /* TextField */, - 553741492E9372DF00AB56D0 /* Toolbar */, - 5537415E2E9372DF00AB56D0 /* View */, - 553741612E9372DF00AB56D0 /* Window */, + 55C1CBDF2E8BB6A2004FE322 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + files = ( ); - path = Utilities; - sourceTree = ""; }; - 553741632E9372DF00AB56D0 /* SharedKit */ = { - isa = PBXGroup; - children = ( - 553741252E9372DF00AB56D0 /* Extensions */, - 553741622E9372DF00AB56D0 /* Utilities */, + 8D15AC330486D014006FF6A4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + files = ( + 557EBD89305AC79D00A9BBD2 /* Cocoa.framework in Frameworks */, + 55C84A712DA93ADB00BA39DA /* UserNotifications.framework in Frameworks */, + 55A027D4158242310004583F /* Quartz.framework in Frameworks */, + 55C165622EE444050082A24C /* Sparkle in Frameworks */, + 55740BF512D710EC004BF6CC /* IOKit.framework in Frameworks */, + 55740BF912D71103004BF6CC /* Security.framework in Frameworks */, ); - path = SharedKit; - sourceTree = ""; }; - 553741682E9372DF00AB56D0 /* Cells */ = { +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 19C28FB0FE9D524F11CA2CBB /* Products */ = { isa = PBXGroup; children = ( - 553741642E9372DF00AB56D0 /* AlignPopupButtonCell.swift */, - 553741652E9372DF00AB56D0 /* ComparatorPopUpButtonCell.swift */, - 553741662E9372DF00AB56D0 /* DisplayFiltersPopUpButtonCell.swift */, - 553741672E9372DF00AB56D0 /* FilePathTableCellView.swift */, + 8D15AC370486D014006FF6A4 /* VisualDiffer.app */, + 55944CC72E5C8D9F00FB1F8C /* VisualDifferTests.xctest */, + 55C1CBE22E8BB6A2004FE322 /* visdiff */, ); - path = Cells; + name = Products; sourceTree = ""; }; - 5537416C2E9372DF00AB56D0 /* DiffCounters */ = { + 2A37F4AAFDCFA73011CA2CEA /* QuickDirtyDiff */ = { isa = PBXGroup; children = ( - 553741692E9372DF00AB56D0 /* DiffCountersItem.swift */, - 5537416A2E9372DF00AB56D0 /* DiffCountersTextFieldCell.swift */, - 5537416B2E9372DF00AB56D0 /* DifferenceCounters.swift */, + 55D71FD72EE584FF007E0FA0 /* AppConfig-Sparkle.xcconfig */, + 55D71FD82EE584FF007E0FA0 /* VisualDiffer-Sparkle.entitlements */, + 55D71FD92EE584FF007E0FA0 /* VisualDiffer-Sparkle-Info.plist */, + 55F013FC27A90C46003EA2D4 /* AppConfig.xcconfig */, + 557C181B2E7E8BDD00381A3A /* Signing.local.xcconfig */, + 557C181C2E7E8BDD00381A3A /* Versions.local.xcconfig */, + 555CAD9B1C2710D000371E4D /* Warnings.xcconfig */, + 55C9D891305A665A00D4FD1E /* Sources */, + 2A37F4C3FDCFA73011CA2CEA /* Frameworks */, + 55916A0A2E8A814200ED6629 /* Models */, + 19C28FB0FE9D524F11CA2CBB /* Products */, + 2A37F4B8FDCFA73011CA2CEA /* Resources */, + 55C9DA5A305A697C00D4FD1E /* Tests */, + 55C9DA95305A69E900D4FD1E /* visdiff */, + 55656A852DD34C5100DD5835 /* VisualDiffer-Bridging-Header.h */, ); - path = DiffCounters; + name = QuickDirtyDiff; sourceTree = ""; }; - 553741752E9372DF00AB56D0 /* Components */ = { + 2A37F4B8FDCFA73011CA2CEA /* Resources */ = { isa = PBXGroup; children = ( - 5537416C2E9372DF00AB56D0 /* DiffCounters */, - 5537416D2E9372DF00AB56D0 /* FilePathTextField.swift */, - 5537416E2E9372DF00AB56D0 /* FindText.swift */, - 5537416F2E9372DF00AB56D0 /* NSTextField+CenterVertically.swift */, - 553741702E9372DF00AB56D0 /* PathControl.swift */, - 553741712E9372DF00AB56D0 /* PathView.swift */, - 5577FFE72FAF2FC30084FB06 /* PopUpButtonURL.swift */, - 55858305300DE12E00EDC56C /* ScopeBarView.swift */, - 553741732E9372DF00AB56D0 /* TablePanelView.swift */, - 553741742E9372DF00AB56D0 /* TimeToleranceView.swift */, + 5537434A2E937B7F00AB56D0 /* colors.json */, + 5537434B2E937B7F00AB56D0 /* colorsDark.json */, + 2A37F4B9FDCFA73011CA2CEA /* Credits.rtf */, + 55157D5C12898F8800826FF8 /* images */, + 557A47F12FF7DE3800D0F6B4 /* Localizable.xcstrings */, + 2F7446A70DB6BCF400F9684A /* MainMenu.xib */, + 5541CE4A2FFCD9E70044A569 /* vd.iconset */, + 5537434C2E937B7F00AB56D0 /* VDDefaults.plist */, + 55ECD8671476B34600D8BEB4 /* VisualDiffer.entitlements */, + 5537434D2E937B7F00AB56D0 /* VisualDiffer.sdef */, + 8D15AC360486D014006FF6A4 /* VisualDiffer-Info.plist */, ); - path = Components; + name = Resources; sourceTree = ""; }; - 553741762E9372DF00AB56D0 /* SharedUI */ = { + 2A37F4C3FDCFA73011CA2CEA /* Frameworks */ = { isa = PBXGroup; children = ( - 553741682E9372DF00AB56D0 /* Cells */, - 553741752E9372DF00AB56D0 /* Components */, + 557EBD88305AC79D00A9BBD2 /* Cocoa.framework */, + 55C84A702DA93ADB00BA39DA /* UserNotifications.framework */, + 55740BF412D710EC004BF6CC /* IOKit.framework */, + 55A027D3158242310004583F /* Quartz.framework */, + 55740BF812D71103004BF6CC /* Security.framework */, ); - path = SharedUI; + name = Frameworks; sourceTree = ""; }; - 553741772E9372DF00AB56D0 /* Sources */ = { + 55157D5C12898F8800826FF8 /* images */ = { isa = PBXGroup; children = ( - 55373F8E2E9372DF00AB56D0 /* App */, - 55373FB52E9372DF00AB56D0 /* Core */, - 553740BB2E9372DF00AB56D0 /* Features */, - 553740E12E9372DF00AB56D0 /* Legacy */, - 553740E42E9372DF00AB56D0 /* Services */, - 553741632E9372DF00AB56D0 /* SharedKit */, - 553741762E9372DF00AB56D0 /* SharedUI */, + 5541CE442FFCD8DA0044A569 /* folder */, + 557315B02551307800CF4372 /* Images.xcassets */, + 559614B014B0471E0075ED34 /* aliasbadge.png */, + 5550B5921A21CD1F009BC4A4 /* aliasbadge@2x.png */, + 55E1029912C9E17300432C61 /* empty.png */, + 5550B5941A21D2F3009BC4A4 /* empty@2x.png */, + 55E3A5641826552A002A890B /* lockedbadge.png */, + 55E3A5651826552A002A890B /* lockedbadge@2x.png */, ); - path = Sources; + path = images; sourceTree = ""; }; 5541CE442FFCD8DA0044A569 /* folder */ = { @@ -2542,61 +271,6 @@ path = folder; sourceTree = ""; }; - 554B4A442E9B7B8F0023AAF0 /* DiffResult */ = { - isa = PBXGroup; - children = ( - 55373FDE2E9372DF00AB56D0 /* DiffChangeType.swift */, - 554B4A432E9B7B8F0023AAF0 /* DiffLine.swift */, - 557BBB0D2ED1BBB00016CBD4 /* DiffLineComponent.swift */, - 55B93A442FAC42910024AEC0 /* DiffProcessor.swift */, - 554B4A3B2E9B7B8F0023AAF0 /* DiffResult.swift */, - 55F9E7992EDD5DAD001218C7 /* DiffResult.Options.swift */, - 55858303300DD8B100EDC56C /* DiffResult.Options+Description.swift */, - 554B4A3D2E9B7B8F0023AAF0 /* DiffResult+Dump.swift */, - 554B4A3E2E9B7B8F0023AAF0 /* DiffResult+Lines.swift */, - 554B4A3F2E9B7B8F0023AAF0 /* DiffResult+Section.swift */, - 554B4A412E9B7B8F0023AAF0 /* DiffSection.swift */, - 554B4A402E9B7B8F0023AAF0 /* DiffSide.swift */, - 559186402E9B821B00A30620 /* DiffSummary.swift */, - 554B4A3A2E9B7B8F0023AAF0 /* EndOfLine.swift */, - ); - path = DiffResult; - sourceTree = ""; - }; - 557C18172E7E6E0100381A3A /* Resources */ = { - isa = PBXGroup; - children = ( - 557C18162E7E6E0100381A3A /* big_file_5000_lines.txt */, - ); - path = Resources; - sourceTree = ""; - }; - 558DEAC52EA263DE00F05A10 /* Encoding */ = { - isa = PBXGroup; - children = ( - 558DEAC42EA263DE00F05A10 /* CFStringEncoding+StringEncoding.swift */, - ); - path = Encoding; - sourceTree = ""; - }; - 558DEACA2EA263F500F05A10 /* Encoding */ = { - isa = PBXGroup; - children = ( - 558DEAC72EA263F500F05A10 /* EncodingManager.swift */, - 558DEAC82EA263F500F05A10 /* EncodingPopUpButtonCell.swift */, - 558DEAC92EA263F500F05A10 /* SelectEncodingsPanel.swift */, - ); - path = Encoding; - sourceTree = ""; - }; - 558DEACF2EA2642100F05A10 /* Window */ = { - isa = PBXGroup; - children = ( - 558DEACE2EA2642100F05A10 /* NSWindow+Editing.swift */, - ); - path = Window; - sourceTree = ""; - }; 55916A0A2E8A814200ED6629 /* Models */ = { isa = PBXGroup; children = ( @@ -2606,186 +280,6 @@ path = Models; sourceTree = ""; }; - 55944CDA2E5C8E5B00FB1F8C /* Tests */ = { - isa = PBXGroup; - children = ( - 557C18202E7E8C0D00381A3A /* AppConfig.xcconfig */, - 557C18212E7E8C0D00381A3A /* Signing.local.xcconfig */, - 557C18222E7E8C0D00381A3A /* Versions.local.xcconfig */, - 5580A6E32E5D7B19008D3D3A /* NoSandbox.entitlements */, - 557C18172E7E6E0100381A3A /* Resources */, - 55DF3DD12E924BB10044CC0C /* Sources */, - 553033AC2E5DBABF0016F101 /* VisualDiffer.xctestplan */, - ); - path = Tests; - sourceTree = ""; - }; - 55B1CE2D2E9934F800426FEC /* Tests */ = { - isa = PBXGroup; - children = ( - 55B1CE2C2E9934F800426FEC /* DiffResultTests.swift */, - 55A4DF332FFD1FFF00ED11BB /* DiffSideTests.swift */, - 55A4DF312E9A49A700ED11BB /* EndOfLineTests.swift */, - 55F9E79B2EDD6221001218C7 /* WhitespacesTests.swift */, - ); - path = Tests; - sourceTree = ""; - }; - 55B1CE2F2E9934F800426FEC /* DiffResult */ = { - isa = PBXGroup; - children = ( - 55B1CE2D2E9934F800426FEC /* Tests */, - 55B1CE2E2E9934F800426FEC /* DiffResultBaseTests.swift */, - ); - path = DiffResult; - sourceTree = ""; - }; - 55B1CE302E9934F800426FEC /* FileCompare */ = { - isa = PBXGroup; - children = ( - 55B1CE2F2E9934F800426FEC /* DiffResult */, - ); - path = FileCompare; - sourceTree = ""; - }; - 55C1CBFC2E8BB6AD004FE322 /* visdiff */ = { - isa = PBXGroup; - children = ( - 55C1CBF32E8BB6AD004FE322 /* AppConfig.xcconfig */, - 55C1CBF52E8BB6AD004FE322 /* Signing.local.xcconfig */, - 55C1CBF82E8BB6AD004FE322 /* Versions.local.xcconfig */, - 55C1CC062E8BBAD1004FE322 /* DocumentWaiter.swift */, - 55C1CBF42E8BB6AD004FE322 /* main.swift */, - 55C1CBF92E8BB6AD004FE322 /* visdiff.1 */, - 55C1CBFA2E8BB6AD004FE322 /* visdiff.entitlements */, - 55C1CBFB2E8BB6AD004FE322 /* visdiff-Info.plist */, - ); - path = visdiff; - sourceTree = ""; - }; - 55DF3DAB2E924BB10044CC0C /* BaseTests */ = { - isa = PBXGroup; - children = ( - 55B1CE2A2E9934E900426FEC /* DateBuilder.swift */, - 55DF3DA92E924BB10044CC0C /* BaseTests.swift */, - 55DF3DAA2E924BB10044CC0C /* BaseTests+AssertCompareItem.swift */, - ); - path = BaseTests; - sourceTree = ""; - }; - 55DF3DAF2E924BB10044CC0C /* Mock */ = { - isa = PBXGroup; - children = ( - 55DF3DAC2E924BB10044CC0C /* MockFileOperationManagerDelegate.swift */, - 55DF3DAD2E924BB10044CC0C /* MockFolderReaderDelegate.swift */, - 5595902A2EABB15F00B5AB0B /* MockItemComparatorDelegate.swift */, - ); - path = Mock; - sourceTree = ""; - }; - 55DF3DB72E924BB10044CC0C /* MetaData */ = { - isa = PBXGroup; - children = ( - 55DF3DB02E924BB10044CC0C /* LabelsCopyTests.swift */, - 55DF3DB12E924BB10044CC0C /* LabelsCreateTests.swift */, - 55DF3DB22E924BB10044CC0C /* LabelsMoveTests.swift */, - 55DF3DB32E924BB10044CC0C /* TagsCopyTests.swift */, - 55DF3DB42E924BB10044CC0C /* TagsCreateTests.swift */, - 55DF3DB52E924BB10044CC0C /* TagsDeleteTests.swift */, - 55DF3DB62E924BB10044CC0C /* TagsMoveTests.swift */, - ); - path = MetaData; - sourceTree = ""; - }; - 55DF3DC32E924BB10044CC0C /* Tests */ = { - isa = PBXGroup; - children = ( - 55DF3DB82E924BB10044CC0C /* AlignmentTests.swift */, - 55DF3DB92E924BB10044CC0C /* BufferedInputStreamTests.swift */, - 554750C52F5C0A0A00C1747A /* CommonAncestorPathTests.swift */, - 55DF3DBA2E924BB10044CC0C /* ComparatorTests.swift */, - 55DF3DBB2E924BB10044CC0C /* CopyFilesTests.swift */, - 55D8812D2F3DA329008DE497 /* CopyFinderMetadata.swift */, - 55DF3DBC2E924BB10044CC0C /* DeleteFileTests.swift */, - 55DF3DBD2E924BB10044CC0C /* DisplayTests.swift */, - 55DF3DBE2E924BB10044CC0C /* EmptyFolderColorTests.swift */, - 55DF3DBF2E924BB10044CC0C /* FiltersTests.swift */, - 55DF3DB72E924BB10044CC0C /* MetaData */, - 55DF3DC02E924BB10044CC0C /* MoveFilesTests.swift */, - 55DF3DC12E924BB10044CC0C /* RenameFilesTests.swift */, - 55E381C02EEFFCF500CB2F00 /* SymbolicLinkTests.swift */, - 55DF3DC22E924BB10044CC0C /* TouchFilesTests.swift */, - ); - path = Tests; - sourceTree = ""; - }; - 55DF3DC72E924BB10044CC0C /* FileSystem */ = { - isa = PBXGroup; - children = ( - 55DF3DAF2E924BB10044CC0C /* Mock */, - 55DF3DC32E924BB10044CC0C /* Tests */, - 55DF3DC42E924BB10044CC0C /* BaseTests+AssertFileSystem.swift */, - 55B9B73D2F5C26F800DB8326 /* BaseTests+CompareItem.swift */, - 55DF3DC52E924BB10044CC0C /* BaseTests+FileSystemOperations.swift */, - 55DF3DC62E924BB10044CC0C /* CaseSensitiveBaseTest.swift */, - ); - path = FileSystem; - sourceTree = ""; - }; - 55DF3DC92E924BB10044CC0C /* Path */ = { - isa = PBXGroup; - children = ( - 55DF3DC82E924BB10044CC0C /* LeafPathTests.swift */, - ); - path = Path; - sourceTree = ""; - }; - 55DF3DCC2E924BB10044CC0C /* Tests */ = { - isa = PBXGroup; - children = ( - 55DF3DCA2E924BB10044CC0C /* FoldersWindowControllerTests.swift */, - 55DF3DCB2E924BB10044CC0C /* RefreshInfoTests.swift */, - ); - path = Tests; - sourceTree = ""; - }; - 55DF3DCD2E924BB10044CC0C /* UI */ = { - isa = PBXGroup; - children = ( - 55DF3DCC2E924BB10044CC0C /* Tests */, - ); - path = UI; - sourceTree = ""; - }; - 55DF3DCF2E924BB10044CC0C /* Utils */ = { - isa = PBXGroup; - children = ( - 55DF3DCE2E924BB10044CC0C /* StringUtils.swift */, - ); - path = Utils; - sourceTree = ""; - }; - 55DF3DD12E924BB10044CC0C /* Sources */ = { - isa = PBXGroup; - children = ( - 55DF3DAB2E924BB10044CC0C /* BaseTests */, - 55B1CE302E9934F800426FEC /* FileCompare */, - 55DF3DC72E924BB10044CC0C /* FileSystem */, - 55DF3DC92E924BB10044CC0C /* Path */, - 55DF3DCD2E924BB10044CC0C /* UI */, - 55DF3DCF2E924BB10044CC0C /* Utils */, - ); - path = Sources; - sourceTree = ""; - }; - 55F42EE02F76798A009B211C /* Navigator */ = { - isa = PBXGroup; - children = ( - 55F42EDF2F76798A009B211C /* CommonPrefs+FileDifferenceNavigator.swift */, - ); - path = Navigator; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -2802,9 +296,10 @@ dependencies = ( 55944CCC2E5C8D9F00FB1F8C /* PBXTargetDependency */, ); - name = VisualDifferTests; - packageProductDependencies = ( + fileSystemSynchronizedGroups = ( + 55C9DA5A305A697C00D4FD1E /* Tests */, ); + name = VisualDifferTests; productName = VisualDifferTests; productReference = 55944CC72E5C8D9F00FB1F8C /* VisualDifferTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -2815,15 +310,13 @@ buildPhases = ( 55C1CBDE2E8BB6A2004FE322 /* Sources */, 55C1CBDF2E8BB6A2004FE322 /* Frameworks */, - 55C1CBE02E8BB6A2004FE322 /* CopyFiles */, ); buildRules = ( ); - dependencies = ( + fileSystemSynchronizedGroups = ( + 55C9DA95305A69E900D4FD1E /* visdiff */, ); name = visdiff; - packageProductDependencies = ( - ); productName = visdiff; productReference = 55C1CBE22E8BB6A2004FE322 /* visdiff */; productType = "com.apple.product-type.tool"; @@ -2843,6 +336,9 @@ dependencies = ( 55C1CC002E8BB749004FE322 /* PBXTargetDependency */, ); + fileSystemSynchronizedGroups = ( + 55C9D891305A665A00D4FD1E /* Sources */, + ); name = VisualDiffer; productInstallPath = "$(HOME)/Applications"; productName = QuickDirtyDiff; @@ -2856,10 +352,10 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - CLASSPREFIX = VD; + CLASSPREFIX = ""; LastSwiftUpdateCheck = 2600; LastTestingUpgradeCheck = 0600; - LastUpgradeCheck = 2600; + LastUpgradeCheck = 2630; ORGANIZATIONNAME = visualdiffer.com; TargetAttributes = { 55944CC62E5C8D9F00FB1F8C = { @@ -2878,7 +374,6 @@ }; }; buildConfigurationList = 26FC0AA90875C8B900E6366F /* Build configuration list for PBXProject "VisualDiffer" */; - compatibilityVersion = "Xcode 3.2"; developmentRegion = en; hasScannedForEncodings = 1; knownRegions = ( @@ -2889,6 +384,8 @@ packageReferences = ( 55C165602EE444050082A24C /* XCRemoteSwiftPackageReference "Sparkle" */, ); + preferredProjectObjectVersion = 90; + productRefGroup = 19C28FB0FE9D524F11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( @@ -2902,18 +399,11 @@ /* Begin PBXResourcesBuildPhase section */ 55944CC52E5C8D9F00FB1F8C /* Resources */ = { isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; files = ( - 557C18182E7E6E0100381A3A /* big_file_5000_lines.txt in Resources */, - 557C18232E7E8C0D00381A3A /* Signing.local.xcconfig in Resources */, - 557C18242E7E8C0D00381A3A /* Versions.local.xcconfig in Resources */, - 557C18252E7E8C0D00381A3A /* AppConfig.xcconfig in Resources */, ); - runOnlyForDeploymentPostprocessing = 0; }; 8D15AC2B0486D014006FF6A4 /* Resources */ = { isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; files = ( 8D15AC2C0486D014006FF6A4 /* Credits.rtf in Resources */, 2F7446AB0DB6BCF400F9684A /* MainMenu.xib in Resources */, @@ -2936,457 +426,26 @@ 5541CE492FFCD8DA0044A569 /* mask-back.png in Resources */, 553743512E937B7F00AB56D0 /* VisualDiffer.sdef in Resources */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 55944CC32E5C8D9F00FB1F8C /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( - 55DF3DD22E924BB10044CC0C /* BaseTests+FileSystemOperations.swift in Sources */, - 55DF3DD32E924BB10044CC0C /* FiltersTests.swift in Sources */, - 55DF3DD42E924BB10044CC0C /* LeafPathTests.swift in Sources */, - 55DF3DD52E924BB10044CC0C /* DeleteFileTests.swift in Sources */, - 55DF3DD62E924BB10044CC0C /* TagsCopyTests.swift in Sources */, - 55DF3DD82E924BB10044CC0C /* CopyFilesTests.swift in Sources */, - 55DF3DD92E924BB10044CC0C /* StringUtils.swift in Sources */, - 55DF3DDB2E924BB10044CC0C /* BufferedInputStreamTests.swift in Sources */, - 55DF3DDC2E924BB10044CC0C /* TagsMoveTests.swift in Sources */, - 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 */, - 55E381C12EEFFD0500CB2F00 /* SymbolicLinkTests.swift in Sources */, - 55B1CE312E9934F800426FEC /* DiffResultBaseTests.swift in Sources */, - 55B1CE322E9934F800426FEC /* DiffResultTests.swift in Sources */, - 55DF3DE02E924BB10044CC0C /* RefreshInfoTests.swift in Sources */, - 55DF3DE12E924BB10044CC0C /* EmptyFolderColorTests.swift in Sources */, - 55DF3DE22E924BB10044CC0C /* BaseTests.swift in Sources */, - 55DF3DE32E924BB10044CC0C /* FoldersWindowControllerTests.swift in Sources */, - 55DF3DE42E924BB10044CC0C /* TagsDeleteTests.swift in Sources */, - 55DF3DE52E924BB10044CC0C /* ComparatorTests.swift in Sources */, - 55B1CE2B2E9934E900426FEC /* DateBuilder.swift in Sources */, - 55DF3DE62E924BB10044CC0C /* TouchFilesTests.swift in Sources */, - 55DF3DE72E924BB10044CC0C /* DisplayTests.swift in Sources */, - 55DF3DE82E924BB10044CC0C /* BaseTests+AssertFileSystem.swift in Sources */, - 55DF3DE92E924BB10044CC0C /* LabelsCreateTests.swift in Sources */, - 55DF3DEA2E924BB10044CC0C /* MockFileOperationManagerDelegate.swift in Sources */, - 55DF3DEB2E924BB10044CC0C /* LabelsMoveTests.swift in Sources */, - 5595902B2EABB15F00B5AB0B /* MockItemComparatorDelegate.swift in Sources */, - 55F9E79C2EDD6221001218C7 /* WhitespacesTests.swift in Sources */, - 55DF3DEC2E924BB10044CC0C /* MoveFilesTests.swift in Sources */, - 55DF3DED2E924BB10044CC0C /* TagsCreateTests.swift in Sources */, - 55DF3DEE2E924BB10044CC0C /* LabelsCopyTests.swift in Sources */, - 55DF3DEF2E924BB10044CC0C /* MockFolderReaderDelegate.swift in Sources */, - 55B9B73E2F5C26F800DB8326 /* BaseTests+CompareItem.swift in Sources */, - 55DF3DF02E924BB10044CC0C /* RenameFilesTests.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; 55C1CBDE2E8BB6A2004FE322 /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( - 55C1CBFD2E8BB6AD004FE322 /* main.swift in Sources */, - 55C1CC072E8BBD85004FE322 /* DocumentWaiter.swift in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; 8D15AC300486D014006FF6A4 /* Sources */ = { isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; files = ( 55916A102E8A814200ED6629 /* MyDocument.xcdatamodeld in Sources */, - 55EC0A4B2F3868E4008201E1 /* CopyFilesTag.swift in Sources */, - 553741792E9372DF00AB56D0 /* NSAppearance+DarkMode.swift in Sources */, - 5537417A2E9372DF00AB56D0 /* QLPreviewPanel.swift in Sources */, - 55FE894A2F054B260003864A /* FlagSet.swift in Sources */, - 5537417B2E9372DF00AB56D0 /* TrustedPathsPreferencesPanel.swift in Sources */, - 5537417C2E9372DF00AB56D0 /* TouchPickersStackView.swift in Sources */, - 5537417D2E9372DF00AB56D0 /* BasePreferences.swift in Sources */, - 5537417E2E9372DF00AB56D0 /* DualPaneSplitViewDelegate.swift in Sources */, - 5537417F2E9372DF00AB56D0 /* FoldersOutlineView+Menu.swift in Sources */, - 553741802E9372DF00AB56D0 /* AlignRule.swift in Sources */, - 553741812E9372DF00AB56D0 /* VisualizationBox.swift in Sources */, - 55CE5BC62FFCBC8500B0F3F5 /* VDSymbol.swift in Sources */, - 553741822E9372DF00AB56D0 /* CommonPrefs+FileCompare.swift in Sources */, - 553741832E9372DF00AB56D0 /* TablePanelView.swift in Sources */, - 553741852E9372DF00AB56D0 /* NSOpenPanel+Application.swift in Sources */, - 553741862E9372DF00AB56D0 /* ConfirmReplace.swift in Sources */, - 553741872E9372DF00AB56D0 /* SessionDiff.ItemType+Paths.swift in Sources */, - 553741882E9372DF00AB56D0 /* FileSizeFormatter.swift in Sources */, - 553741892E9372DF00AB56D0 /* TOPTimestampPredicateEditorRowTemplate.m in Sources */, - 5537418A2E9372DF00AB56D0 /* DocumentWindow.swift in Sources */, - 5537418B2E9372DF00AB56D0 /* CommonPrefs+Name.swift in Sources */, - 5537418D2E9372DF00AB56D0 /* FilePathTextField.swift in Sources */, - 5537418E2E9372DF00AB56D0 /* CopyCompareItem.swift in Sources */, - 5537418F2E9372DF00AB56D0 /* ColoredFoldersManager.swift in Sources */, - 553741902E9372DF00AB56D0 /* FilesTableView+Menu.swift in Sources */, - 553741912E9372DF00AB56D0 /* NSBox+Helper.swift in Sources */, - 553741922E9372DF00AB56D0 /* FontPreferencesPanel.swift in Sources */, - 558DEAD02EA2642100F05A10 /* NSWindow+Editing.swift in Sources */, - 553741932E9372DF00AB56D0 /* DisplayOptions.swift in Sources */, - 553741942E9372DF00AB56D0 /* FilesWindowController+Menu.swift in Sources */, - 553741952E9372DF00AB56D0 /* OptionSet+Toggle.swift in Sources */, - 553741962E9372DF00AB56D0 /* URL+Finder.swift in Sources */, - 553741982E9372DF00AB56D0 /* CommonPrefs+FolderCompare.swift in Sources */, - 553741992E9372DF00AB56D0 /* FoldersWindowController+PathControlDelegate.swift in Sources */, - 551DE3B82ED439AF0067AB18 /* PreferencesBoxDataSource+Default.swift in Sources */, - 55D352E72EFD0C5B00C944D5 /* FileSystemTestHelper.swift in Sources */, - 5537419A2E9372DF00AB56D0 /* FontBox.swift in Sources */, - 55F9BAF22FA7259B004EA543 /* FoldersWindowController+Observers.swift in Sources */, - 5537419B2E9372DF00AB56D0 /* FilesTableViewFindTextDelegate.swift in Sources */, - 5537419C2E9372DF00AB56D0 /* DeleteFileOperationExecutor.swift in Sources */, - 5537419D2E9372DF00AB56D0 /* DualPaneSplitView.swift in Sources */, - 5537419E2E9372DF00AB56D0 /* StandardButtons.swift in Sources */, - 5537419F2E9372DF00AB56D0 /* ConfirmationsFoldersBox.swift in Sources */, - 553741A02E9372DF00AB56D0 /* DifferenceNavigatorBox.swift in Sources */, - 55BC33402EDAC96F004035A0 /* TiledImageView.swift in Sources */, - 553741A12E9372DF00AB56D0 /* FolderReaderDelegate.swift in Sources */, - 553741A22E9372DF00AB56D0 /* UDiffScriptBuilder.m in Sources */, - 553741A32E9372DF00AB56D0 /* String+RegularExpression.swift in Sources */, - 553741A42E9372DF00AB56D0 /* FileSystemController.swift in Sources */, - 553741A52E9372DF00AB56D0 /* ComparatorOptions+Helper.swift in Sources */, - 553741A62E9372DF00AB56D0 /* AppearanceBox.swift in Sources */, - 553741A72E9372DF00AB56D0 /* FolderComparisonBox.swift in Sources */, - 553741A82E9372DF00AB56D0 /* FoldersWindowController+Document.swift in Sources */, - 553741A92E9372DF00AB56D0 /* FilterConfig.swift in Sources */, - 553741AA2E9372DF00AB56D0 /* IconUtils.swift in Sources */, - 553741AC2E9372DF00AB56D0 /* NSTextView+Style.swift in Sources */, - 553741AD2E9372DF00AB56D0 /* FoldersOutlineView.swift in Sources */, - 559186412E9B821B00A30620 /* DiffSummary.swift in Sources */, - 553741AE2E9372DF00AB56D0 /* PreferredEditorBox.swift in Sources */, - 553741AF2E9372DF00AB56D0 /* MoveCompareItem.swift in Sources */, - 55A585772F52BD8A006DD544 /* FileOperationDestination.swift in Sources */, - 553741B12E9372DF00AB56D0 /* TextFieldSelectionHolder.swift in Sources */, - 553741B22E9372DF00AB56D0 /* TouchFileOperationExecutor.swift in Sources */, - 553741B32E9372DF00AB56D0 /* NSTextFieldCell+Helper.swift in Sources */, - 553741B42E9372DF00AB56D0 /* NSApplication.ModalResponse+ReplaceFile.swift in Sources */, - 553741B52E9372DF00AB56D0 /* DiffOpenerDelegate.swift in Sources */, - 55B93A452FAC42950024AEC0 /* DiffProcessor.swift in Sources */, - 553741B72E9372DF00AB56D0 /* PreferencesCheckbox.swift in Sources */, - 553741B82E9372DF00AB56D0 /* FileManager+Attributes.swift in Sources */, - 553741B92E9372DF00AB56D0 /* MainThreadFolderReaderDelegateBridge.swift in Sources */, - 55F9E79A2EDD5DBD001218C7 /* DiffResult.Options.swift in Sources */, - 553741BA2E9372DF00AB56D0 /* VisibleWhitespaces.swift in Sources */, - 553741BB2E9372DF00AB56D0 /* DisplayPositionable.swift in Sources */, - 553741BC2E9372DF00AB56D0 /* AlignRuleWindow.swift in Sources */, - 553741BD2E9372DF00AB56D0 /* DeleteCompareItem.swift in Sources */, - 553741BE2E9372DF00AB56D0 /* DiffChangeType+Color.swift in Sources */, - 553741BF2E9372DF00AB56D0 /* DescriptionOutlineNode.swift in Sources */, - 553741C02E9372DF00AB56D0 /* FilesTableView.swift in Sources */, - 553741C12E9372DF00AB56D0 /* String+Occcurrences.swift in Sources */, - 553741C22E9372DF00AB56D0 /* FoldersWindowController+QLPreviewPanel.swift in Sources */, - 553741C32E9372DF00AB56D0 /* FoldersOutlineView+DifferenceNavigator.swift in Sources */, - 553741C42E9372DF00AB56D0 /* JumpToLineWindow.swift in Sources */, - 553741C52E9372DF00AB56D0 /* EncodingError.swift in Sources */, - 553741C62E9372DF00AB56D0 /* FileExtraOptions.swift in Sources */, - 553741C72E9372DF00AB56D0 /* Array+MoveIndexes.swift in Sources */, - 553741C82E9372DF00AB56D0 /* BigFileFileOperationManager.m in Sources */, - 553741C92E9372DF00AB56D0 /* FoldersWindowController+BaseFolder.swift in Sources */, - 553741CB2E9372DF00AB56D0 /* FoldersWindowController+UICreation.swift in Sources */, - 553741CC2E9372DF00AB56D0 /* FilesWindowController+UICreation.swift in Sources */, - 557BBB0E2ED1BBB00016CBD4 /* DiffLineComponent.swift in Sources */, - 553741CD2E9372DF00AB56D0 /* NSTableView+Row.swift in Sources */, - 553741CE2E9372DF00AB56D0 /* FindText.swift in Sources */, - 553741CF2E9372DF00AB56D0 /* FoldersTraversalBox.swift in Sources */, - 553741D02E9372DF00AB56D0 /* FileError.swift in Sources */, - 55450B192ED47B700048619F /* FileSessionPreferencesWindow+PreferencesBoxDataSource.swift in Sources */, - 553741D12E9372DF00AB56D0 /* OpenEditorError.swift in Sources */, - 551DE39F2ED422820067AB18 /* FilesWindowController+SessionPreferences.swift in Sources */, - 553741D22E9372DF00AB56D0 /* GeneralPreferencesPanel.swift in Sources */, - 553741D32E9372DF00AB56D0 /* CompareItem+DifferenceNavigator.swift in Sources */, - 558DEAD22EA26C5B00F05A10 /* Logger+App.swift in Sources */, - 551DE3AA2ED428B40067AB18 /* FilePreferences.swift in Sources */, - 553741D42E9372DF00AB56D0 /* PreferencesBox.swift in Sources */, - 553741D52E9372DF00AB56D0 /* NSAlert+ReplaceFile.swift in Sources */, - 553741D72E9372DF00AB56D0 /* URL+SymLink.swift in Sources */, - 553741D82E9372DF00AB56D0 /* RecentDocumentPopupMenu.swift in Sources */, - 553741D92E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineViewContextMenu.swift in Sources */, - 553741DA2E9372DF00AB56D0 /* FileAttributes.swift in Sources */, - 553741DB2E9372DF00AB56D0 /* SyncFileController.swift in Sources */, - 553741DC2E9372DF00AB56D0 /* ConfirmationsFilesBox.swift in Sources */, - 553741DD2E9372DF00AB56D0 /* CompareItem+Accessors.swift in Sources */, - 553741DE2E9372DF00AB56D0 /* FilesWindowController+JumpLine.swift in Sources */, - 5595901F2EABA8A100B5AB0B /* CompareChangeType.swift in Sources */, - 553741DF2E9372DF00AB56D0 /* CommonPrefs+ConsoleLog.swift in Sources */, - 553741E02E9372DF00AB56D0 /* NSToolbar+Create.swift in Sources */, - 553741E12E9372DF00AB56D0 /* String+Helper.swift in Sources */, - 553741E32E9372DF00AB56D0 /* LineNumberTableRowView.swift in Sources */, - 553741E42E9372DF00AB56D0 /* FolderReader.swift in Sources */, - 553741E52E9372DF00AB56D0 /* DateFormatter+Helper.swift in Sources */, - 551DE3A62ED4237B0067AB18 /* FileSessionPreferencesWindow.swift in Sources */, - 553741E62E9372DF00AB56D0 /* FoldersWindowController+Navigation.swift in Sources */, - 553741E72E9372DF00AB56D0 /* HistoryEntity.swift in Sources */, - 553741E82E9372DF00AB56D0 /* CompareItem+Clone.swift in Sources */, - 553741E92E9372DF00AB56D0 /* URL+Path.swift in Sources */, - 55BC33432EDACB09004035A0 /* FilesWindowController+RowHeightDataSource.swift in Sources */, - 55BC33442EDACB09004035A0 /* FilesWindowController+SplitViewDelegate.swift in Sources */, - 55BC333E2EDAC69C004035A0 /* RowHeightCalculator.swift in Sources */, - 553741EA2E9372DF00AB56D0 /* PathChooser.swift in Sources */, - 553741EB2E9372DF00AB56D0 /* VDDocumentController.swift in Sources */, - 553741EC2E9372DF00AB56D0 /* CopyFileOperationExecutor.swift in Sources */, - 553741ED2E9372DF00AB56D0 /* FoldersOutlineView+Columns.swift in Sources */, - 553741EE2E9372DF00AB56D0 /* NSTextField+CenterVertically.swift in Sources */, - 554B4A452E9B7B8F0023AAF0 /* EndOfLine.swift in Sources */, - 554B4A462E9B7B8F0023AAF0 /* DiffSide.swift in Sources */, - 554B4A472E9B7B8F0023AAF0 /* DiffSection.swift in Sources */, - 554B4A482E9B7B8F0023AAF0 /* DiffResult+Dump.swift in Sources */, - 554B4A4A2E9B7B8F0023AAF0 /* DiffResult+Section.swift in Sources */, - 554B4A4B2E9B7B8F0023AAF0 /* DiffResult.swift in Sources */, - 554B4A4D2E9B7B8F0023AAF0 /* DiffResult+Lines.swift in Sources */, - 554B4A4E2E9B7B8F0023AAF0 /* DiffLine.swift in Sources */, - 553741EF2E9372DF00AB56D0 /* BufferedInputStream.swift in Sources */, - 553741F02E9372DF00AB56D0 /* ComparisonStandardUserDataSource.swift in Sources */, - 5548ABA82F7FE2BB0064B964 /* LineDetailTextView.swift in Sources */, - 553741F12E9372DF00AB56D0 /* TextPreferencesPanel.swift in Sources */, - 553741F22E9372DF00AB56D0 /* AlignPopupButtonCell.swift in Sources */, - 553741F32E9372DF00AB56D0 /* FolderSelectionInfo+ViewerActionValidator.swift in Sources */, - 553741F52E9372DF00AB56D0 /* OpenEditor.swift in Sources */, - 553741F62E9372DF00AB56D0 /* NSMenu+File.swift in Sources */, - 553741F72E9372DF00AB56D0 /* FilesWindowController+FileInfoBarDelegate.swift in Sources */, - 553741F82E9372DF00AB56D0 /* KeyboardPreferencesPanel.swift in Sources */, - 553741F92E9372DF00AB56D0 /* CompareItem+Metadata.swift in Sources */, - 55858306300DE12E00EDC56C /* ScopeBarView.swift in Sources */, - 553741FA2E9372DF00AB56D0 /* TextFieldVerticalCentered.swift in Sources */, - 553741FB2E9372DF00AB56D0 /* KeyEquivalent.swift in Sources */, - 553741FC2E9372DF00AB56D0 /* CompareItem+VisibleItem.swift in Sources */, - 553741FD2E9372DF00AB56D0 /* FoldersWindowController+SessionPreferences.swift in Sources */, - 553741FE2E9372DF00AB56D0 /* CompareItemTableCellView.swift in Sources */, - 553741FF2E9372DF00AB56D0 /* ProgressBarView.swift in Sources */, - 553742002E9372DF00AB56D0 /* FolderManagerError.swift in Sources */, - 553742012E9372DF00AB56D0 /* FolderSelectionInfo.swift in Sources */, - 55F4A0032FEE100000A1B2C3 /* ExternalFileOperationContext.swift in Sources */, - 553742022E9372DF00AB56D0 /* SynchroScrollView.swift in Sources */, - 553742032E9372DF00AB56D0 /* FilesWindowController+Save.swift in Sources */, - 553742042E9372DF00AB56D0 /* FoldersWindowController+ConsoleViewDelegate.swift in Sources */, - 553742052E9372DF00AB56D0 /* PreferencesBoxDataSource.swift in Sources */, - 553742062E9372DF00AB56D0 /* TOPFileSizePredicateEditorRowTemplate.m in Sources */, - 553742072E9372DF00AB56D0 /* FolderReader+Detached.swift in Sources */, - 553742082E9372DF00AB56D0 /* ViewLinkable.swift in Sources */, - 553742092E9372DF00AB56D0 /* SessionPreferencesFiltersBox.swift in Sources */, - 55D71FD62EE584B7007E0FA0 /* AppUpdater.swift in Sources */, - 5537420A2E9372DF00AB56D0 /* FilesWindowController+Common.swift in Sources */, - 5537420B2E9372DF00AB56D0 /* FilesWindowController+NSToolbarDelegate.swift in Sources */, - 5537420C2E9372DF00AB56D0 /* FilesWindowController+TableView.swift in Sources */, - 5537420D2E9372DF00AB56D0 /* NSProgressIndicator+Helper.swift in Sources */, - 5537420E2E9372DF00AB56D0 /* ComparatorOptions.swift in Sources */, - 5537420F2E9372DF00AB56D0 /* PathTimestamps.swift in Sources */, - 553742102E9372DF00AB56D0 /* FilesWindowController+Clipboard.swift in Sources */, - 553742112E9372DF00AB56D0 /* RSVerticallyCenteredTextFieldCell.swift in Sources */, - 553742122E9372DF00AB56D0 /* CompareItem+Description.swift in Sources */, - 553742132E9372DF00AB56D0 /* SessionPreferencesWindow.swift in Sources */, - 553742142E9372DF00AB56D0 /* UserDefinedRulesBox.swift in Sources */, - 553742162E9372DF00AB56D0 /* ProgressIndicatorController.swift in Sources */, - 553742172E9372DF00AB56D0 /* FolderPanelView.swift in Sources */, - 553742182E9372DF00AB56D0 /* ComparatorOptions+Description.swift in Sources */, - 553742192E9372DF00AB56D0 /* FoldersOutlineView+Clipboard.swift in Sources */, - 5536ADBA2EF169820019EFBF /* SessionDiff+ExtraData.swift in Sources */, - 5537421A2E9372DF00AB56D0 /* DatePickersStackView.swift in Sources */, - 5537421B2E9372DF00AB56D0 /* UnifiedDiff.m in Sources */, - 5537421C2E9372DF00AB56D0 /* SyncOutlineView.swift in Sources */, - 5537421D2E9372DF00AB56D0 /* CompareItem+Comparison.swift in Sources */, - 5537421E2E9372DF00AB56D0 /* NSError+Format.swift in Sources */, - 5537421F2E9372DF00AB56D0 /* RefreshInfo.swift in Sources */, - 553742202E9372DF00AB56D0 /* FileDropView+Helper.swift in Sources */, - 553742212E9372DF00AB56D0 /* SyncFileOperationExecutor.swift in Sources */, - 553742222E9372DF00AB56D0 /* CompareItem+FilterConfig.swift in Sources */, - 553742232E9372DF00AB56D0 /* NSStackView+PreferencesPanel.swift in Sources */, - 553742242E9372DF00AB56D0 /* CustomValidationToolbarItem.swift in Sources */, - 553742252E9372DF00AB56D0 /* VisibleItem+Sort.swift in Sources */, - 553742262E9372DF00AB56D0 /* LineNumberTableCellView.swift in Sources */, - 553742272E9372DF00AB56D0 /* FilesWindowController+Slider.swift in Sources */, - 553742282E9372DF00AB56D0 /* VDDocument.swift in Sources */, - 553742292E9372DF00AB56D0 /* FilesWindowController+Read.swift in Sources */, - 5537422A2E9372DF00AB56D0 /* PowerAssertion.swift in Sources */, - 5537422C2E9372DF00AB56D0 /* FilesWindowController+UISetup.swift in Sources */, - 551DE3B42ED42AD90067AB18 /* FilePreferencesComparisonPanel.swift in Sources */, - 5537422D2E9372DF00AB56D0 /* CompareUtil.swift in Sources */, - 5537422E2E9372DF00AB56D0 /* DisplayFiltersScopeBar.swift in Sources */, - 5537422F2E9372DF00AB56D0 /* HistorySessionManager.swift in Sources */, - 553742302E9372DF00AB56D0 /* FilePanelView.swift in Sources */, - 553742312E9372DF00AB56D0 /* FilesWindowController.swift in Sources */, - 553742322E9372DF00AB56D0 /* FilesWindowController+MenuDelegate.swift in Sources */, - 553742332E9372DF00AB56D0 /* DifferenceCounters.swift in Sources */, - 553742342E9372DF00AB56D0 /* ReplaceFileAttributeKey.swift in Sources */, - 553742352E9372DF00AB56D0 /* FoldersWindowController+Select.swift in Sources */, - 553742362E9372DF00AB56D0 /* FoldersWindowController+Comparison.swift in Sources */, - 553742372E9372DF00AB56D0 /* PathView.swift in Sources */, - 553742382E9372DF00AB56D0 /* DiffLine+Color.swift in Sources */, - 553742392E9372DF00AB56D0 /* FileDropView.swift in Sources */, - 5537423A2E9372DF00AB56D0 /* PreferredEditorPopupCell.swift in Sources */, - 5537423B2E9372DF00AB56D0 /* FilePanelView+File.swift in Sources */, - 5537423C2E9372DF00AB56D0 /* KeyboardDocumentBox.swift in Sources */, - 5537423D2E9372DF00AB56D0 /* FoldersOutlineView+VisibleItem.swift in Sources */, - 5537423E2E9372DF00AB56D0 /* FileNameCaseBox.swift in Sources */, - 5537423F2E9372DF00AB56D0 /* URL+Metadata.swift in Sources */, - 553742402E9372DF00AB56D0 /* FoldersWindowController+Menu.swift in Sources */, - 553742412E9372DF00AB56D0 /* OperationSummaryView.swift in Sources */, - 55450B172ED4790B0048619F /* TextPreferencesPanel+PreferencesBoxDataSource.swift in Sources */, - 551DE3B62ED42B4E0067AB18 /* FileComparisonBox.swift in Sources */, - 553742422E9372DF00AB56D0 /* AppDelegate.swift in Sources */, - 559590262EABB06100B5AB0B /* ItemComparator+AlignRegularExpression.swift in Sources */, - 559590272EABB06100B5AB0B /* ItemComparator+Align.swift in Sources */, - 559590282EABB06100B5AB0B /* ItemComparator+Compare.swift in Sources */, - 559590292EABB06100B5AB0B /* ItemComparator.swift in Sources */, - 55858304300DD8B100EDC56C /* DiffResult.Options+Description.swift in Sources */, - 553742432E9372DF00AB56D0 /* ExpressionBox+Menu.swift in Sources */, - 553742442E9372DF00AB56D0 /* FilesWindowController+PathControlDelegate.swift in Sources */, - 553742452E9372DF00AB56D0 /* VisibleItem.swift in Sources */, - 553742462E9372DF00AB56D0 /* DiffChangeType.swift in Sources */, - 553742472E9372DF00AB56D0 /* NotificationCenter+Helper.swift in Sources */, - 553742482E9372DF00AB56D0 /* NSImage+Tint.swift in Sources */, - 558DEAC62EA263DE00F05A10 /* CFStringEncoding+StringEncoding.swift in Sources */, - 553742492E9372DF00AB56D0 /* NSButton+Helper.swift in Sources */, - 5537424A2E9372DF00AB56D0 /* FoldersWindowController.swift in Sources */, - 5537424B2E9372DF00AB56D0 /* VisibleItem+Find.swift in Sources */, - 5537424C2E9372DF00AB56D0 /* String+Highlights.swift in Sources */, - 5537424D2E9372DF00AB56D0 /* SyncItemsInfo.swift in Sources */, - 5537424E2E9372DF00AB56D0 /* FileOperationManager+FileOperationManagerAction.swift in Sources */, - 5537424F2E9372DF00AB56D0 /* NoodleCustomImageRep.swift in Sources */, - 553742502E9372DF00AB56D0 /* FoldersWindowController+FileSystemControllerDelegate.swift in Sources */, - 553742512E9372DF00AB56D0 /* FilesWindowController+NSWindowDelegate.swift in Sources */, - 553742522E9372DF00AB56D0 /* FilesWindowController+Navigate.swift in Sources */, - 553742532E9372DF00AB56D0 /* DiffCountersItem.swift in Sources */, - 553742542E9372DF00AB56D0 /* SaveFileAccessoryView.swift in Sources */, - 553742552E9372DF00AB56D0 /* FileThumbnailView.swift in Sources */, - 553742562E9372DF00AB56D0 /* FilesTableView+EditorData.swift in Sources */, - 553742572E9372DF00AB56D0 /* ComparatorPopUpButtonCell.swift in Sources */, - 553742582E9372DF00AB56D0 /* IntegerFormatter.swift in Sources */, - 553742592E9372DF00AB56D0 /* SessionDiff+ResolvePath.swift in Sources */, - 5537425A2E9372DF00AB56D0 /* CompareItem+NSTableCellView.swift in Sources */, - 559590212EABAFC200B5AB0B /* SessionDiff+ItemComparator.swift in Sources */, - 5537425B2E9372DF00AB56D0 /* DocumentError.swift in Sources */, - 5537425C2E9372DF00AB56D0 /* FolderSelectionInfo+FileSystemActionValidator.swift in Sources */, - 5595901D2EABA7FE00B5AB0B /* CompareSummary.swift in Sources */, - 5537425D2E9372DF00AB56D0 /* FoldersWindowController+Exclude.swift in Sources */, - 5537425E2E9372DF00AB56D0 /* Preferences.swift in Sources */, - 5537425F2E9372DF00AB56D0 /* OutlineViewItemDelegate.swift in Sources */, - 553742622E9372DF00AB56D0 /* TableViewContextMenuDelegate.swift in Sources */, - 553742632E9372DF00AB56D0 /* NSPredicate+Objc.m in Sources */, - 553742642E9372DF00AB56D0 /* FileSummaryView.swift in Sources */, - 553742652E9372DF00AB56D0 /* CompareItemTableRowView.swift in Sources */, - 553742662E9372DF00AB56D0 /* FolderSelectionInfo+FolderActionValidator.swift in Sources */, - 553742672E9372DF00AB56D0 /* CommonPrefs+Helper.swift in Sources */, - 553742682E9372DF00AB56D0 /* FilePathTableCellView.swift in Sources */, - 553742692E9372DF00AB56D0 /* AttributedMenuItem.swift in Sources */, - 5537426A2E9372DF00AB56D0 /* URL+StructuredContent.swift in Sources */, - 5537426B2E9372DF00AB56D0 /* SessionPreferencesComparisonPanel.swift in Sources */, - 552B85082F5FE29A00A9268A /* VDDocumentController+BlankDocument.swift in Sources */, - 5537426C2E9372DF00AB56D0 /* FolderCompareInfoWindow.swift in Sources */, - 5537426D2E9372DF00AB56D0 /* NSToolbarItem+Create.swift in Sources */, - 5537426E2E9372DF00AB56D0 /* MoveFileOperationExecutor.swift in Sources */, - 5537426F2E9372DF00AB56D0 /* URL+FileManager.swift in Sources */, - 553742712E9372DF00AB56D0 /* ConfirmationsDocumentsBox.swift in Sources */, - 553742722E9372DF00AB56D0 /* ExpressionBox.swift in Sources */, - 553742732E9372DF00AB56D0 /* CompareItem+LeafPath.swift in Sources */, - 553742742E9372DF00AB56D0 /* SessionDiff.swift in Sources */, - 553742762E9372DF00AB56D0 /* TimeToleranceView.swift in Sources */, - 553742782E9372DF00AB56D0 /* FoldersOutlineView+Enumerate.swift in Sources */, - 553742792E9372DF00AB56D0 /* HistoryController.swift in Sources */, - 5537427A2E9372DF00AB56D0 /* ConsoleToolbarView.swift in Sources */, - 5537427B2E9372DF00AB56D0 /* HistorySearchField.swift in Sources */, - 5537427C2E9372DF00AB56D0 /* URL+ResourceFork.swift in Sources */, - 5537427D2E9372DF00AB56D0 /* LinkButton.swift in Sources */, - 5537427E2E9372DF00AB56D0 /* ReplaceInfoView.swift in Sources */, - 5537427F2E9372DF00AB56D0 /* NSAlert+DirtyFiles.swift in Sources */, - 553742802E9372DF00AB56D0 /* CommonPrefs.swift in Sources */, - 553742812E9372DF00AB56D0 /* FilesWindowController+LinesDetail.swift in Sources */, - 553742822E9372DF00AB56D0 /* FileOperationManager+Util.swift in Sources */, - 553742832E9372DF00AB56D0 /* ConsoleView.swift in Sources */, - 553742842E9372DF00AB56D0 /* CommonPrefs+DifferenceNavigator.swift in Sources */, - 553742852E9372DF00AB56D0 /* FileOperationExecutor.swift in Sources */, - 553742862E9372DF00AB56D0 /* AlignmentPanel.swift in Sources */, - 553742872E9372DF00AB56D0 /* FoldersOutlineView+ExternalApp.swift in Sources */, - 553742882E9372DF00AB56D0 /* PreferencesPanelDataSource.swift in Sources */, - 553742892E9372DF00AB56D0 /* DiffCountersTextFieldCell.swift in Sources */, - 5537428A2E9372DF00AB56D0 /* FoldersWindowController+FoldersOutlineView.swift in Sources */, - 5537428B2E9372DF00AB56D0 /* TableViewCommon.swift in Sources */, - 5537428C2E9372DF00AB56D0 /* CompareItem+Path.swift in Sources */, - 5537428D2E9372DF00AB56D0 /* FolderPreferencesPanel.swift in Sources */, - 5537428E2E9372DF00AB56D0 /* FileOperationManager.swift in Sources */, - 5537428F2E9372DF00AB56D0 /* StandardUserPreferencesBoxDataSource.swift in Sources */, - 553742902E9372DF00AB56D0 /* SecureBookmark.swift in Sources */, - 553742912E9372DF00AB56D0 /* FilesScopeBar.swift in Sources */, - 55BCFE472F627DDF00E704D8 /* SessionPopupButton.swift in Sources */, - 553742922E9372DF00AB56D0 /* OpenEditorAttribute.swift in Sources */, - 553742932E9372DF00AB56D0 /* WindowCancelOperation.swift in Sources */, - 553742942E9372DF00AB56D0 /* VisibleItem+QLPreviewItem.swift in Sources */, - 553742952E9372DF00AB56D0 /* FilesWindowController+FilesTableViewContextMenu.swift in Sources */, - 553742962E9372DF00AB56D0 /* FolderSelectionInfo+CompareActionValidator.swift in Sources */, - 553742982E9372DF00AB56D0 /* DiffCountersItem+DiffResult.swift in Sources */, - 553742992E9372DF00AB56D0 /* FileOperationManagerDelegateImpl.swift in Sources */, - 5537429A2E9372DF00AB56D0 /* SessionDiff+AlignRule.swift in Sources */, - 5537429B2E9372DF00AB56D0 /* FoldersWindowController+MenuDelegate.swift in Sources */, - 552430412F7673F800951F1A /* FileDifferenceNavigatorBox.swift in Sources */, - 5537429D2E9372DF00AB56D0 /* FolderSelectionInfo+FilterActionValidator.swift in Sources */, - 5537429E2E9372DF00AB56D0 /* OpenDiffCommand.swift in Sources */, - 5537429F2E9372DF00AB56D0 /* RenameCompareItem.swift in Sources */, - 553742A02E9372DF00AB56D0 /* DisplayOptions+Helper.swift in Sources */, - 553742A12E9372DF00AB56D0 /* SessionPreferencesWindow+Data.swift in Sources */, - 553742A22E9372DF00AB56D0 /* FolderReader+Log.swift in Sources */, - 553742A32E9372DF00AB56D0 /* NSArrayController+Paths.swift in Sources */, - 553742A42E9372DF00AB56D0 /* NSPredicateEditor+Fix.swift in Sources */, - 553742A52E9372DF00AB56D0 /* FoldersWindowController+NSToolbarDelegate.swift in Sources */, - 553742A62E9372DF00AB56D0 /* NSAlert+Helper.swift in Sources */, - 553742A72E9372DF00AB56D0 /* FoldersWindowController+NSWindowDelegate.swift in Sources */, - 553742A82E9372DF00AB56D0 /* FilesWindowController+FilesScopeBar.swift in Sources */, - 553742A92E9372DF00AB56D0 /* NSManagedObjectContext+Helpers.swift in Sources */, - 553742AA2E9372DF00AB56D0 /* String+Path.swift in Sources */, - 553742AB2E9372DF00AB56D0 /* TimeInterval+Helper.swift in Sources */, - 553742AC2E9372DF00AB56D0 /* ColorScheme.swift in Sources */, - 553742AD2E9372DF00AB56D0 /* FilesWindowController+Document.swift in Sources */, - 553742AE2E9372DF00AB56D0 /* PathControl.swift in Sources */, - 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 */, - 553742B52E9372DF00AB56D0 /* HistoryFetchedResultsControllerDelegate.swift in Sources */, - 553742B62E9372DF00AB56D0 /* CommonPrefs+Font.swift in Sources */, - 553742B72E9372DF00AB56D0 /* NSWorkspace+Finder.swift in Sources */, - 553742B82E9372DF00AB56D0 /* FoldersOutlineViewFindTextDelegate.swift in Sources */, - 553742B92E9372DF00AB56D0 /* AlignTestResultBox.swift in Sources */, - 553742BA2E9372DF00AB56D0 /* VisibleItem+Log.swift in Sources */, - 553742BB2E9372DF00AB56D0 /* HistoryTableView.swift in Sources */, - 553742BC2E9372DF00AB56D0 /* SessionDiff+NSSortDescriptor.swift in Sources */, - 55F42EE12F76798A009B211C /* CommonPrefs+FileDifferenceNavigator.swift in Sources */, - 553742BD2E9372DF00AB56D0 /* NSPopUpButton+Helper.swift in Sources */, - 553742BE2E9372DF00AB56D0 /* NSColor+Hex.swift in Sources */, - 553742BF2E9372DF00AB56D0 /* FileInfoBar.swift in Sources */, - 553742C02E9372DF00AB56D0 /* TitlePredicateEditorRowTemplate.m in Sources */, - 553742C12E9372DF00AB56D0 /* ErrorsView.swift in Sources */, - 553742C22E9372DF00AB56D0 /* IconUtils+CompareItemIconUtils.swift in Sources */, - 553742C32E9372DF00AB56D0 /* SessionTypeError.swift in Sources */, - 553742C42E9372DF00AB56D0 /* FoldersOutlineView+FileCount.swift in Sources */, - 553742C52E9372DF00AB56D0 /* SessionPreferencesFiltersPanel.swift in Sources */, - 553742C62E9372DF00AB56D0 /* FoldersWindowController+DisplayFiltersScopeBarDelegate.swift in Sources */, - 553742C72E9372DF00AB56D0 /* DiffCountersItem+CompareItem.swift in Sources */, - 553742C82E9372DF00AB56D0 /* DisplayFiltersPopUpButtonCell.swift in Sources */, - 553742C92E9372DF00AB56D0 /* NSEvent+VirtualKeys.swift in Sources */, - 553742CA2E9372DF00AB56D0 /* DescriptionOutlineNode+FolderCompareInfo.swift in Sources */, - 553742CB2E9372DF00AB56D0 /* WindowOSD.swift in Sources */, - 553742CC2E9372DF00AB56D0 /* NSPasteboard+Helper.swift in Sources */, - 553742CD2E9372DF00AB56D0 /* ConfirmationsPreferencesPanel.swift in Sources */, - 558DEACB2EA263F500F05A10 /* EncodingManager.swift in Sources */, - 558DEACC2EA263F500F05A10 /* SelectEncodingsPanel.swift in Sources */, - 558DEACD2EA263F500F05A10 /* EncodingPopUpButtonCell.swift in Sources */, - 553742CE2E9372DF00AB56D0 /* CompareItem.swift in Sources */, - 553742CF2E9372DF00AB56D0 /* NSTextField+Helper.swift in Sources */, - 553742D02E9372DF00AB56D0 /* FolderViewBox.swift in Sources */, - 553742D12E9372DF00AB56D0 /* SessionDiff+Types.swift in Sources */, - 553742D22E9372DF00AB56D0 /* FiltersPredicateEditor.swift in Sources */, - 553742D32E9372DF00AB56D0 /* HistoryEntityTableCellView.swift in Sources */, 55916A112E8A814200ED6629 /* History.xcdatamodeld in Sources */, ); - runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ @@ -3424,7 +483,7 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 26FC0AA60875C8B900E6366F /* Debug */ = { + 26FC0AA60875C8B900E6366F /* Debug configuration for PBXNativeTarget "VisualDiffer" */ = { isa = XCBuildConfiguration; baseConfigurationReference = 55F013FC27A90C46003EA2D4 /* AppConfig.xcconfig */; buildSettings = { @@ -3435,38 +494,23 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; - DEFINES_MODULE = YES; - ENABLE_MODULE_VERIFIER = YES; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); GCC_DYNAMIC_NO_PIC = NO; GCC_ENABLE_OBJC_EXCEPTIONS = YES; - GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = VisualDiffer_Prefix.pch; GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; INSTALL_PATH = /Applications; - MACOSX_DEPLOYMENT_TARGET = 13.5; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++14"; - OTHER_CFLAGS = "-DDEBUG"; - OTHER_LDFLAGS = ""; PRODUCT_NAME = VisualDiffer; - PROVISIONING_PROFILE = ""; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "VisualDiffer-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = Debug; }; - 26FC0AA70875C8B900E6366F /* Release */ = { + 26FC0AA70875C8B900E6366F /* Release configuration for PBXNativeTarget "VisualDiffer" */ = { isa = XCBuildConfiguration; baseConfigurationReference = 55F013FC27A90C46003EA2D4 /* AppConfig.xcconfig */; buildSettings = { @@ -3477,17 +521,8 @@ COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; - ENABLE_MODULE_VERIFIER = YES; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); GCC_ENABLE_OBJC_EXCEPTIONS = YES; - GCC_MODEL_TUNING = G5; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = VisualDiffer_Prefix.pch; GCC_PREPROCESSOR_DEFINITIONS = ( NDEBUG, NS_BLOCK_ASSERTIONS, @@ -3495,19 +530,13 @@ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; INSTALL_PATH = /Applications; - MACOSX_DEPLOYMENT_TARGET = 13.5; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++14"; - OTHER_CFLAGS = ""; - OTHER_LDFLAGS = ""; PRODUCT_NAME = VisualDiffer; - PROVISIONING_PROFILE = ""; SWIFT_OBJC_BRIDGING_HEADER = "VisualDiffer-Bridging-Header.h"; - SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = Release; }; - 26FC0AAA0875C8B900E6366F /* Debug */ = { + 26FC0AAA0875C8B900E6366F /* Debug configuration for PBXProject "VisualDiffer" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3523,20 +552,25 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 13.5; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-DDEBUG", @@ -3545,14 +579,14 @@ "-Wno-unused-parameter", "-Wno-missing-field-initializers", ); - PROVISIONING_PROFILE = "59d2fc6b-4fa7-4828-8895-d3d003484da9"; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; }; name = Debug; }; - 26FC0AAB0875C8B900E6366F /* Release */ = { + 26FC0AAB0875C8B900E6366F /* Release configuration for PBXProject "VisualDiffer" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3568,34 +602,39 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 13.5; OTHER_CFLAGS = ( "-Wall", "-Wextra", "-Wno-unused-parameter", "-Wno-missing-field-initializers", ); - PROVISIONING_PROFILE = "59d2fc6b-4fa7-4828-8895-d3d003484da9"; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; }; name = Release; }; - 558893662E7FF93B003B7117 /* DebugNoSandbox */ = { + 558893662E7FF93B003B7117 /* DebugNoSandbox configuration for PBXProject "VisualDiffer" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3611,20 +650,25 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 13.5; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-DDEBUG", @@ -3633,14 +677,14 @@ "-Wno-unused-parameter", "-Wno-missing-field-initializers", ); - PROVISIONING_PROFILE = "59d2fc6b-4fa7-4828-8895-d3d003484da9"; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; }; name = DebugNoSandbox; }; - 558893672E7FF93B003B7117 /* DebugNoSandbox */ = { + 558893672E7FF93B003B7117 /* DebugNoSandbox configuration for PBXNativeTarget "VisualDiffer" */ = { isa = XCBuildConfiguration; baseConfigurationReference = 55F013FC27A90C46003EA2D4 /* AppConfig.xcconfig */; buildSettings = { @@ -3651,40 +695,26 @@ COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; - DEFINES_MODULE = YES; - ENABLE_MODULE_VERIFIER = YES; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); GCC_DYNAMIC_NO_PIC = NO; GCC_ENABLE_OBJC_EXCEPTIONS = YES; - GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = VisualDiffer_Prefix.pch; GCC_SYMBOLS_PRIVATE_EXTERN = NO; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; INSTALL_PATH = /Applications; - MACOSX_DEPLOYMENT_TARGET = 13.5; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++14"; - OTHER_CFLAGS = "-DDEBUG"; - OTHER_LDFLAGS = ""; PRODUCT_NAME = VisualDiffer; - PROVISIONING_PROFILE = ""; "SWIFT_ACTIVE_COMPILATION_CONDITIONS[arch=*]" = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "VisualDiffer-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = DebugNoSandbox; }; - 558893692E7FF93B003B7117 /* DebugNoSandbox */ = { + 558893692E7FF93B003B7117 /* DebugNoSandbox configuration for PBXNativeTarget "VisualDifferTests" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 557C18202E7E8C0D00381A3A /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA5A305A697C00D4FD1E /* Tests */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -3709,7 +739,6 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_USER_SELECTED_FILES = readwrite; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3723,23 +752,21 @@ GCC_WARN_UNUSED_FUNCTION = YES; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VisualDiffer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VisualDiffer"; }; name = DebugNoSandbox; }; - 55944CCE2E5C8D9F00FB1F8C /* Debug */ = { + 55944CCE2E5C8D9F00FB1F8C /* Debug configuration for PBXNativeTarget "VisualDifferTests" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 557C18202E7E8C0D00381A3A /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA5A305A697C00D4FD1E /* Tests */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -3764,7 +791,6 @@ CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_USER_SELECTED_FILES = readwrite; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3778,23 +804,21 @@ GCC_WARN_UNUSED_FUNCTION = YES; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VisualDiffer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VisualDiffer"; }; name = Debug; }; - 55944CD02E5C8D9F00FB1F8C /* Release */ = { + 55944CD02E5C8D9F00FB1F8C /* Release configuration for PBXNativeTarget "VisualDifferTests" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 557C18202E7E8C0D00381A3A /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA5A305A697C00D4FD1E /* Tests */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -3819,7 +843,6 @@ CURRENT_PROJECT_VERSION = 1; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SELECTED_FILES = readwrite; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -3827,21 +850,19 @@ GCC_WARN_UNUSED_FUNCTION = YES; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VisualDiffer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VisualDiffer"; }; name = Release; }; - 55C1CBE72E8BB6A2004FE322 /* Debug */ = { + 55C1CBE72E8BB6A2004FE322 /* Debug configuration for PBXNativeTarget "visdiff" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 55C1CBF32E8BB6AD004FE322 /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA95305A69E900D4FD1E /* visdiff */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; @@ -3864,7 +885,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - GCC_C_LANGUAGE_STANDARD = gnu17; + ENABLE_HARDENED_RUNTIME = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3877,20 +898,19 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 15.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; }; name = Debug; }; - 55C1CBE82E8BB6A2004FE322 /* DebugNoSandbox */ = { + 55C1CBE82E8BB6A2004FE322 /* DebugNoSandbox configuration for PBXNativeTarget "visdiff" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 55C1CBF32E8BB6AD004FE322 /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA95305A69E900D4FD1E /* visdiff */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; @@ -3913,7 +933,7 @@ CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; - GCC_C_LANGUAGE_STANDARD = gnu17; + ENABLE_HARDENED_RUNTIME = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -3926,20 +946,19 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 15.7; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; }; name = DebugNoSandbox; }; - 55C1CBE92E8BB6A2004FE322 /* Release */ = { + 55C1CBE92E8BB6A2004FE322 /* Release configuration for PBXNativeTarget "visdiff" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 55C1CBF32E8BB6AD004FE322 /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA95305A69E900D4FD1E /* visdiff */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; @@ -3961,24 +980,22 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 15.7; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; }; name = Release; }; - 55D71FDA2EE58630007E0FA0 /* Sparkle */ = { + 55D71FDA2EE58630007E0FA0 /* Sparkle configuration for PBXProject "VisualDiffer" */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; @@ -3994,34 +1011,39 @@ CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ""; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_NEWLINE = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES; GCC_WARN_SIGN_COMPARE = YES; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES; + GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.12; + MACOSX_DEPLOYMENT_TARGET = 13.5; OTHER_CFLAGS = ( "-Wall", "-Wextra", "-Wno-unused-parameter", "-Wno-missing-field-initializers", ); - PROVISIONING_PROFILE = "59d2fc6b-4fa7-4828-8895-d3d003484da9"; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; }; name = Sparkle; }; - 55D71FDB2EE58630007E0FA0 /* Sparkle */ = { + 55D71FDB2EE58630007E0FA0 /* Sparkle configuration for PBXNativeTarget "VisualDiffer" */ = { isa = XCBuildConfiguration; baseConfigurationReference = 55D71FD72EE584FF007E0FA0 /* AppConfig-Sparkle.xcconfig */; buildSettings = { @@ -4032,17 +1054,8 @@ COMBINE_HIDPI_IMAGES = YES; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - DEFINES_MODULE = YES; - ENABLE_MODULE_VERIFIER = YES; ENABLE_TESTING_SEARCH_PATHS = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)", - ); GCC_ENABLE_OBJC_EXCEPTIONS = YES; - GCC_MODEL_TUNING = G5; - GCC_PRECOMPILE_PREFIX_HEADER = YES; - GCC_PREFIX_HEADER = VisualDiffer_Prefix.pch; GCC_PREPROCESSOR_DEFINITIONS = ( NDEBUG, NS_BLOCK_ASSERTIONS, @@ -4050,21 +1063,16 @@ GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES; INSTALL_PATH = /Applications; - MACOSX_DEPLOYMENT_TARGET = 13.5; - MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++14"; - OTHER_CFLAGS = ""; - OTHER_LDFLAGS = ""; PRODUCT_NAME = VisualDiffer; - PROVISIONING_PROFILE = ""; SWIFT_OBJC_BRIDGING_HEADER = "VisualDiffer-Bridging-Header.h"; - SWIFT_VERSION = 6.0; WRAPPER_EXTENSION = app; }; name = Sparkle; }; - 55D71FDC2EE58630007E0FA0 /* Sparkle */ = { + 55D71FDC2EE58630007E0FA0 /* Sparkle configuration for PBXNativeTarget "visdiff" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 55C1CBF32E8BB6AD004FE322 /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA95305A69E900D4FD1E /* visdiff */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; @@ -4086,26 +1094,25 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; + ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 15.7; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - SWIFT_VERSION = 5.0; }; name = Sparkle; }; - 55D71FDD2EE58630007E0FA0 /* Sparkle */ = { + 55D71FDD2EE58630007E0FA0 /* Sparkle configuration for PBXNativeTarget "VisualDifferTests" */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 557C18202E7E8C0D00381A3A /* AppConfig.xcconfig */; + baseConfigurationReferenceAnchor = 55C9DA5A305A697C00D4FD1E /* Tests */; + baseConfigurationReferenceRelativePath = AppConfig.xcconfig; buildSettings = { ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; @@ -4130,7 +1137,6 @@ CURRENT_PROJECT_VERSION = 1; ENABLE_NS_ASSERTIONS = NO; ENABLE_USER_SELECTED_FILES = readwrite; - GCC_C_LANGUAGE_STANDARD = gnu17; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; @@ -4138,14 +1144,11 @@ GCC_WARN_UNUSED_FUNCTION = YES; GENERATE_INFOPLIST_FILE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MACOSX_DEPLOYMENT_TARGET = 14.6; MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE = ""; SWIFT_EMIT_LOC_STRINGS = NO; - SWIFT_VERSION = 5.0; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VisualDiffer.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/VisualDiffer"; }; name = Sparkle; @@ -4156,45 +1159,41 @@ 26FC0AA50875C8B900E6366F /* Build configuration list for PBXNativeTarget "VisualDiffer" */ = { isa = XCConfigurationList; buildConfigurations = ( - 26FC0AA60875C8B900E6366F /* Debug */, - 558893672E7FF93B003B7117 /* DebugNoSandbox */, - 26FC0AA70875C8B900E6366F /* Release */, - 55D71FDB2EE58630007E0FA0 /* Sparkle */, + 26FC0AA60875C8B900E6366F /* Debug configuration for PBXNativeTarget "VisualDiffer" */, + 558893672E7FF93B003B7117 /* DebugNoSandbox configuration for PBXNativeTarget "VisualDiffer" */, + 26FC0AA70875C8B900E6366F /* Release configuration for PBXNativeTarget "VisualDiffer" */, + 55D71FDB2EE58630007E0FA0 /* Sparkle configuration for PBXNativeTarget "VisualDiffer" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 26FC0AA90875C8B900E6366F /* Build configuration list for PBXProject "VisualDiffer" */ = { isa = XCConfigurationList; buildConfigurations = ( - 26FC0AAA0875C8B900E6366F /* Debug */, - 558893662E7FF93B003B7117 /* DebugNoSandbox */, - 26FC0AAB0875C8B900E6366F /* Release */, - 55D71FDA2EE58630007E0FA0 /* Sparkle */, + 26FC0AAA0875C8B900E6366F /* Debug configuration for PBXProject "VisualDiffer" */, + 558893662E7FF93B003B7117 /* DebugNoSandbox configuration for PBXProject "VisualDiffer" */, + 26FC0AAB0875C8B900E6366F /* Release configuration for PBXProject "VisualDiffer" */, + 55D71FDA2EE58630007E0FA0 /* Sparkle configuration for PBXProject "VisualDiffer" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 55944CCD2E5C8D9F00FB1F8C /* Build configuration list for PBXNativeTarget "VisualDifferTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 55944CCE2E5C8D9F00FB1F8C /* Debug */, - 558893692E7FF93B003B7117 /* DebugNoSandbox */, - 55944CD02E5C8D9F00FB1F8C /* Release */, - 55D71FDD2EE58630007E0FA0 /* Sparkle */, + 55944CCE2E5C8D9F00FB1F8C /* Debug configuration for PBXNativeTarget "VisualDifferTests" */, + 558893692E7FF93B003B7117 /* DebugNoSandbox configuration for PBXNativeTarget "VisualDifferTests" */, + 55944CD02E5C8D9F00FB1F8C /* Release configuration for PBXNativeTarget "VisualDifferTests" */, + 55D71FDD2EE58630007E0FA0 /* Sparkle configuration for PBXNativeTarget "VisualDifferTests" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 55C1CBE62E8BB6A2004FE322 /* Build configuration list for PBXNativeTarget "visdiff" */ = { isa = XCConfigurationList; buildConfigurations = ( - 55C1CBE72E8BB6A2004FE322 /* Debug */, - 55C1CBE82E8BB6A2004FE322 /* DebugNoSandbox */, - 55C1CBE92E8BB6A2004FE322 /* Release */, - 55D71FDC2EE58630007E0FA0 /* Sparkle */, + 55C1CBE72E8BB6A2004FE322 /* Debug configuration for PBXNativeTarget "visdiff" */, + 55C1CBE82E8BB6A2004FE322 /* DebugNoSandbox configuration for PBXNativeTarget "visdiff" */, + 55C1CBE92E8BB6A2004FE322 /* Release configuration for PBXNativeTarget "visdiff" */, + 55D71FDC2EE58630007E0FA0 /* Sparkle configuration for PBXNativeTarget "visdiff" */, ); - defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ diff --git a/VisualDiffer.xcodeproj/xcshareddata/xcschemes/Sparkle.xcscheme b/VisualDiffer.xcodeproj/xcshareddata/xcschemes/Sparkle.xcscheme index 01bf4fd..c0d58e1 100644 --- a/VisualDiffer.xcodeproj/xcshareddata/xcschemes/Sparkle.xcscheme +++ b/VisualDiffer.xcodeproj/xcshareddata/xcschemes/Sparkle.xcscheme @@ -1,6 +1,6 @@ @@ -108,13 +108,6 @@ ReferencedContainer = "container:VisualDiffer.xcodeproj"> - - - - diff --git a/VisualDiffer_Prefix.pch b/VisualDiffer_Prefix.pch deleted file mode 100644 index e0d8658..0000000 --- a/VisualDiffer_Prefix.pch +++ /dev/null @@ -1,9 +0,0 @@ -// -// Prefix header for all source files of the 'VisualDiffer' target in the 'VisualDiffer' project -// - -#ifdef __OBJC__ - #import - #import - #import -#endif diff --git a/Warnings.xcconfig b/Warnings.xcconfig index c1a18eb..afa340f 100644 --- a/Warnings.xcconfig +++ b/Warnings.xcconfig @@ -13,8 +13,8 @@ GCC_WARN_64_TO_32_BIT_CONVERSION = YES CLANG_WARN_ENUM_CONVERSION = YES CLANG_WARN_INT_CONVERSION = YES CLANG_WARN_CONSTANT_CONVERSION = YES -CLANG_WARN_BOOL_CONVERSION = YES; -CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE; +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES GCC_WARN_ABOUT_RETURN_TYPE = YES GCC_WARN_MISSING_PARENTHESES = YES @@ -22,7 +22,7 @@ GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES GCC_WARN_ABOUT_MISSING_NEWLINE = YES GCC_WARN_SIGN_COMPARE = YES GCC_WARN_UNDECLARED_SELECTOR = YES -GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE GCC_WARN_UNUSED_FUNCTION = YES GCC_WARN_UNUSED_LABEL = YES GCC_WARN_UNUSED_VALUE = YES diff --git a/assets/Images.xcassets/Toolbar/Contents.json b/assets/Images.xcassets/Toolbar/Contents.json index 74d6a72..73c0059 100644 --- a/assets/Images.xcassets/Toolbar/Contents.json +++ b/assets/Images.xcassets/Toolbar/Contents.json @@ -1,6 +1,6 @@ { - "info": { - "author": "xcode", - "version": 1 + "info" : { + "author" : "xcode", + "version" : 1 } } diff --git a/assets/Images.xcassets/Toolbar/compareItems.symbolset/Contents.json b/assets/Images.xcassets/Toolbar/compareItems.symbolset/Contents.json new file mode 100644 index 0000000..02cdaac --- /dev/null +++ b/assets/Images.xcassets/Toolbar/compareItems.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "symbols": [ + { + "filename": "compareItems.svg", + "idiom": "universal" + } + ] +} diff --git a/assets/Images.xcassets/Toolbar/compareItems.symbolset/compareItems.svg b/assets/Images.xcassets/Toolbar/compareItems.symbolset/compareItems.svg new file mode 100644 index 0000000..10d1608 --- /dev/null +++ b/assets/Images.xcassets/Toolbar/compareItems.symbolset/compareItems.svg @@ -0,0 +1,46 @@ + + + + + + Small + Medium + Large + + + Ultralight + Regular + Black + Template v.3.0 + + https://github.com/swhitty/SwiftDraw + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/fastlane/.env.default b/fastlane/.env.default index f1db723..7ed2637 100644 --- a/fastlane/.env.default +++ b/fastlane/.env.default @@ -4,11 +4,13 @@ APP_IDENTIFIER="com.visualdiffer" SCHEME="VisualDiffer" TARGET="VisualDiffer" PROJECT_PATH="VisualDiffer.xcodeproj" +WORKSPACE_PATH="VisualDiffer.xcworkspace" TEAM_ID= APPLE_ID= RELEASE_NOTE_PATH=notes CHECK_VERSION=true +LINT_ENABLED=true # === Build === BUILD_PATH="build" diff --git a/fastlane/Fastfile b/fastlane/Fastfile index c706dd1..10563cb 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -55,7 +55,7 @@ platform :mac do UI.header("🧱 Build & Archive") clean_build_artifacts gym( - project: ENV["PROJECT_PATH"], + workspace: find_workspace, scheme: find_scheme, silent: true, archive_path: ENV["ARCHIVE_PATH"], @@ -70,16 +70,19 @@ platform :mac do end lane :tests do - run_tests(scheme: ENV["SCHEME"]) + run_tests(workspace: find_workspace, scheme: ENV["SCHEME"]) end private_lane :release_precondition do check_current_branch unless ENV["IGNORE_BUILD_BRANCH"] == "true" check_release_version if ENV["CHECK_VERSION"] == "true" - lint_app - format_app( - check: true - ) + + if ENV["LINT_ENABLED"] == "true" + lint_app + format_app( + check: true + ) + end note_file_name = File.join(ENV["BUILD_PATH"], ENV["RELEASE_NOTE_PATH"], ENV["APP_VERSION"]) notes_full_path = File.join(ENV["PROJECT_ROOT"], "#{note_file_name}.md") @@ -144,6 +147,13 @@ platform :mac do UI.success("✅ GitHub release completed") end + def find_workspace + workspace = ENV["WORKSPACE_PATH"] + UI.user_error!("Workspace not defined") if workspace.nil? || workspace.empty? + + return workspace + end + def find_scheme if ENV["SPARKLE_ENABLED"] == "true" scheme = ENV["SPARKLE_SCHEME"] diff --git a/mul.lproj/MainMenu.xcstrings b/mul.lproj/MainMenu.xcstrings index f921664..37a5ef5 100644 --- a/mul.lproj/MainMenu.xcstrings +++ b/mul.lproj/MainMenu.xcstrings @@ -505,18 +505,6 @@ } } }, - "608.title" : { - "comment" : "Class = \"NSMenuItem\"; title = \"Compare Files\"; ObjectID = \"608\";", - "extractionState" : "extracted_with_value", - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "new", - "value" : "Compare Files" - } - } - } - }, "609.title" : { "comment" : "Class = \"NSMenuItem\"; title = \"Copy Files...\"; ObjectID = \"609\";", "extractionState" : "extracted_with_value", diff --git a/scripts/setup-local-env.sh b/scripts/setup-local-env.sh index 982b923..72622c9 100755 --- a/scripts/setup-local-env.sh +++ b/scripts/setup-local-env.sh @@ -3,12 +3,12 @@ # Bootstrap script for local xcconfig files. # Combines the local and maintainer setups in a single entry point. # -# Default mode copies the template xcconfig files (same as setup-local-env.sh), -# it is safe to run multiple times and will NOT overwrite existing .local files. +# Default mode copies the template xcconfig files, it is safe to run multiple +# times and will NOT overwrite existing .local files. # # Maintainer mode (-m admin) installs the maintainer configurations as symbolic -# links (same as setup-maintainer-env.sh) and must be run only on maintainer -# machines because it requires access to sensible informations. +# links and must be run only on maintainer machines because it requires access +# to sensible informations. set -euo pipefail diff --git a/visdiff/AppConfig.xcconfig b/visdiff/AppConfig.xcconfig index 18865e5..bdda450 100644 --- a/visdiff/AppConfig.xcconfig +++ b/visdiff/AppConfig.xcconfig @@ -6,8 +6,7 @@ // Copyright (c) 2022 visualdiffer.com // -// The directory containing the project -// must be used to prefix the value for CODE_SIGN_ENTITLEMENTS in Signing.local.xcconfig +// the directory containing the visdiff target, used to prefix the paths below VISDIFF_ROOT = visdiff PRODUCT_BUNDLE_IDENTIFIER = com.visualdiffer.visdiff @@ -18,9 +17,13 @@ REGISTER_APP_GROUPS = NO INFOPLIST_FILE = $(VISDIFF_ROOT)/visdiff-Info.plist -ENABLE_APP_SANDBOX[config=Debug] = YES -ENABLE_APP_SANDBOX[config=Release] = YES -ENABLE_APP_SANDBOX[config=DebugNoSandbox] = YES +// every binary shipped in a release must be sandboxed, the Sparkle configuration included, so this is +// set unconditionally instead of one line per configuration, which silently left Sparkle out +ENABLE_APP_SANDBOX = YES + +// the sandbox above is useless without entitlements, so the two are kept together here +// unlike the values in Signing.local.xcconfig this path is neither secret nor machine specific +CODE_SIGN_ENTITLEMENTS = $(VISDIFF_ROOT)/visdiff.entitlements AUTOMATION_APPLE_EVENTS = YES diff --git a/visdiff/DocumentWaiter.swift b/visdiff/DocumentWaiter.swift index c771e1e..bff78e0 100644 --- a/visdiff/DocumentWaiter.swift +++ b/visdiff/DocumentWaiter.swift @@ -75,9 +75,17 @@ class DocumentWaiter: NSObject, SBApplicationDelegate { app.delegate = self app.activate() + let openDiffSelector = NSSelectorFromString("openDiffLeftPath:rightPath:") + + guard app.responds(to: openDiffSelector) else { + throw ComparisonLaunchError.launch( + description: "VisualDiffer is not scriptable, unable to read its scripting definition" + ) + } + // in Swift is more simple to use the dynamic approach uuid = app.perform( - NSSelectorFromString("openDiffLeftPath:rightPath:"), + openDiffSelector, with: leftPath.path(percentEncoded: false), with: rightPath.path(percentEncoded: false) )?.takeRetainedValue() as? String // swiftlint:disable:this multiline_function_chains diff --git a/visdiff/main.swift b/visdiff/main.swift index a71a712..ea2f9a6 100644 --- a/visdiff/main.swift +++ b/visdiff/main.swift @@ -8,7 +8,8 @@ import Foundation -let version = Bundle.main.infoDictionary?["CFBundleVersion"] ?? "n/a" +// cast needed in Swift 6 language mode +let version = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "n/a" enum Flags: String { case version = "--version" diff --git a/visdiff/visdiff.1 b/visdiff/visdiff.1 deleted file mode 100644 index 2b367cd..0000000 --- a/visdiff/visdiff.1 +++ /dev/null @@ -1,42 +0,0 @@ -.Dd 17/04/26 -.Dt visdiff 1 -.Os Darwin -.Sh NAME -.Nm visdiff -.Nd compare two files or two folders with VisualDiffer -.Sh SYNOPSIS -.Nm -.Op Fl -wait -.Op Fl -focus -.Op Fl -no-warning -.Op Fl -version -.Ar left\ file\ or\ folder -.Ar right\ file\ or\ folder -.Sh DESCRIPTION -.Nm -opens a VisualDiffer comparison between two paths. -The left and right arguments must both be files or both be folders. -.Pp -Relative paths require the caller shell to provide -.Ev PWD . -If -.Ev PWD -is not available, -.Nm -cannot safely resolve relative paths because the tool is sandboxed. -.Sh OPTIONS -.Bl -tag -width "--no-warning" -.It Fl -wait -wait until the diff window is closed before returning -.It Fl -focus -restore the caller application focus after waiting -This option is valid only when -.Fl -wait -is also used -.It Fl -no-warning -suppress the sandbox warning message -.It Fl -version -show the application version and exit -.El -.Sh SEE ALSO -.Lk https://wiki.visualdiffer.com/unixshell.html