diff --git a/.gitignore b/.gitignore index 22a8285676..de38ca42c1 100644 --- a/.gitignore +++ b/.gitignore @@ -71,7 +71,6 @@ rider/**/out/* # Traditional C# excludes **/.vs/ -**/Packages/* *.suo *.user [Bb]in diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2e894772..5f880a0917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Since 2018.1, the version numbers and release cycle match Rider's versions and r - Rider: Add Code Vision actions to show call tree for performance critical, expensive and Burst compiled methods ([RIDER-35169](https://youtrack.jetbrains.com/issue/RIDER-35169), [#1995](https://github.com/JetBrains/resharper-unity/pull/1995)) - Rider: Add custom debugger type views for `SerializedObject` and `SerializedProperty` ([#1991](https://github.com/JetBrains/resharper-unity/pull/1991)) - Rider: Show Unity editor plugin errors and exceptions in Unity console, not just log ([RIDER-54352](https://youtrack.jetbrains.com/issue/RIDER-54352), [#2012](https://github.com/JetBrains/resharper-unity/pull/2012)) +- Rider: Add marker to ignored folders in Unity Explorer ([#2024](https://github.com/JetBrains/resharper-unity/pull/2024)) ### Changed @@ -31,6 +32,8 @@ Since 2018.1, the version numbers and release cycle match Rider's versions and r - Rider: Double click on assembly files in Unity Explorer will add the file to Assembly Explorer ([RIDER-54873](https://youtrack.jetbrains.com/issue/RIDER-54873), [#1996](https://github.com/JetBrains/resharper-unity/pull/1996)) - Rider: Improve wording on notifications when Rider is not set as Unity's External Editor ([#1958](https://github.com/JetBrains/resharper-unity/pull/1958)) - Rider: Improve performance adding log events to Unity tool window ([RIDER-57100](https://youtrack.jetbrains.com/issue/RIDER-57100), [#2014](https://github.com/JetBrains/resharper-unity/pull/2014)) +- Rider: Improve discovery and display of packages in Unity Explorer ([#2024](https://github.com/JetBrains/resharper-unity/pull/2024)) +- Rider: Stop showing VCS status colours in Unity Explorer References node ([#2024](https://github.com/JetBrains/resharper-unity/pull/2024)) ### Fixed diff --git a/resharper/.idea/.idea.rider-unity/.idea/indexLayout.xml b/resharper/.idea/.idea.rider-unity/.idea/indexLayout.xml index 27ba142e96..273542638a 100644 --- a/resharper/.idea/.idea.rider-unity/.idea/indexLayout.xml +++ b/resharper/.idea/.idea.rider-unity/.idea/indexLayout.xml @@ -5,4 +5,9 @@ + + + + + \ No newline at end of file diff --git a/resharper/resharper-unity/src/Packages/PackageData.cs b/resharper/resharper-unity/src/Packages/PackageData.cs new file mode 100644 index 0000000000..6d63421bb8 --- /dev/null +++ b/resharper/resharper-unity/src/Packages/PackageData.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using JetBrains.Util; +using Newtonsoft.Json; + +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable NotAccessedField.Global + +namespace JetBrains.ReSharper.Plugins.Unity.Packages +{ + public class PackageData + { + [NotNull] public readonly string Id; + [CanBeNull] public readonly FileSystemPath PackageFolder; + public readonly DateTime PackageJsonTimestamp; + public readonly PackageDetails PackageDetails; + public readonly PackageSource Source; + [CanBeNull] public readonly GitDetails GitDetails; + [CanBeNull] public readonly FileSystemPath TarballLocation; + + public PackageData([NotNull] string id, + [CanBeNull] FileSystemPath packageFolder, + DateTime packageJsonTimestamp, + PackageDetails packageDetails, + PackageSource source, + [CanBeNull] GitDetails gitDetails, + [CanBeNull] FileSystemPath tarballLocation) + { + Id = id; + PackageFolder = packageFolder; + PackageJsonTimestamp = packageJsonTimestamp; + PackageDetails = packageDetails; + Source = source; + GitDetails = gitDetails; + TarballLocation = tarballLocation; + } + + public static PackageData CreateUnknown(string id, string version, + PackageSource packageSource = PackageSource.Unknown) + { + return new PackageData(id, null, DateTime.MinValue, + new PackageDetails(id, $"{id}@{version}", version, + $"Cannot resolve package '{id}' with version '{version}'", + new Dictionary()), packageSource, null, null); + } + } + + public class PackageDetails + { + // Note that canonical name is the name field from package.json. It is the truth about the name of the package. + // The id field in PackageData is the ID used to reference the package in manifest.json or packages-lock.json. + // The assumption is that these values are always the same. + [NotNull] public readonly string CanonicalName; + [NotNull] public readonly string DisplayName; + [NotNull] public readonly string Version; + [CanBeNull] public readonly string Description; + // [CanBeNull] public readonly string Author; // Author might actually be a dictionary + [NotNull] public readonly Dictionary Dependencies; + + public PackageDetails([NotNull] string canonicalName, + [NotNull] string displayName, + [NotNull] string version, + [CanBeNull] string description, + [NotNull] Dictionary dependencies) + { + CanonicalName = canonicalName; + DisplayName = displayName; + Version = version; + Description = description; + Dependencies = dependencies; + } + + [NotNull] + internal static PackageDetails FromPackageJson([NotNull] PackageJson packageJson, FileSystemPath packageFolder) + { + var name = packageJson.Name ?? packageFolder.Name; + return new PackageDetails(name, packageJson.DisplayName ?? name, packageJson.Version ?? string.Empty, + packageJson.Description, packageJson.Dependencies); + } + } + + public class GitDetails + { + [NotNull] public readonly string Url; + [NotNull] public readonly string Hash; + [CanBeNull] public readonly string Revision; + + public GitDetails([NotNull] string url, [NotNull] string hash, [CanBeNull] string revision) + { + Url = url; + Hash = hash; + Revision = revision; + } + } + + // packages-lock.json (note the 's', this isn't NPM's package-lock.json) + // This file was introduced in Unity 2019.4 and is a complete list of all packages, including dependencies and + // transitive dependencies. Versions are fully resolved from manifest.json, fixing conflicts, and also handling the + // editor minimum version levels. If also contains the appropriate hashes for git and local tarball packages + // By observation: + // * `source` can be `builtin`, `registry`, `embedded`, `git`, `local` and `localTarball` + // * `version` is a semver value for `builtin` and `registry`, a `file:` url for `embedded` and a url for `git` + // TODO: document local + // * `url` is only available for registry packages, and is the url of the registry, e.g. https://packages.unity.com + // * `hash` is the commit hash for git packages + // * `dependencies` is a map of package name to version + // * `depth` is unknown, but seems to be an indicator of a transitive dependency rather than a direct dependency. + // E.g. a package only used as a dependency of another package can have a depth of 1, while the parent package + // has a depth of 0 + internal class PackagesLockJson + { + public readonly Dictionary Dependencies; + + [JsonConstructor] + private PackagesLockJson(Dictionary dependencies) + { + Dependencies = dependencies; + } + + public static PackagesLockJson FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + internal class PackagesLockDependency + { + public readonly string Version; + public readonly int? Depth; + [CanBeNull] public readonly string Source; + [NotNull] public readonly Dictionary Dependencies; + [CanBeNull] public readonly string Url; + [CanBeNull] public readonly string Hash; + + public PackagesLockDependency(string version, int? depth, [CanBeNull] string source, + [NotNull] Dictionary dependencies, [CanBeNull] string url, + [CanBeNull] string hash) + { + Version = version; + Depth = depth; + Source = source; + Dependencies = dependencies; + Url = url; + Hash = hash; + } + } + + internal class PackageJson + { + [CanBeNull] public readonly string Name; + [CanBeNull] public readonly string DisplayName; + [CanBeNull] public readonly string Version; + [CanBeNull] public readonly string Description; + // [CanBeNull] public readonly string Author; // TODO: Author might be a map, e.g. author[name] + [NotNull] public readonly Dictionary Dependencies; + + [JsonConstructor] + private PackageJson([CanBeNull] string name, + [CanBeNull] string displayName, + [CanBeNull] string version, + [CanBeNull] string description, + [CanBeNull] Dictionary dependencies) + { + Name = name; + DisplayName = displayName; + Version = version; + Description = description; + Dependencies = dependencies ?? new Dictionary(); + } + + public static PackageJson FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + } + + internal class ManifestJson + { + [NotNull] public readonly IDictionary Dependencies; + [CanBeNull] public readonly string Registry; + [NotNull] public readonly IDictionary Lock; + public readonly bool? EnableLockFile; + + [JsonConstructor] + private ManifestJson([CanBeNull] IDictionary dependencies, [CanBeNull] string registry, + [CanBeNull] IDictionary @lock, bool? enableLockFile) + { + Dependencies = dependencies ?? EmptyDictionary.InstanceDictionary; + Registry = registry; + Lock = @lock ?? EmptyDictionary.InstanceDictionary; + EnableLockFile = enableLockFile; + } + + public static ManifestJson FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + internal class ManifestLockDetails + { + [CanBeNull] public readonly string Hash; + [CanBeNull] public readonly string Revision; + + [JsonConstructor] + private ManifestLockDetails([CanBeNull] string hash, [CanBeNull] string revision) + { + Hash = hash; + Revision = revision; + } + } + + internal class EditorManifestJson + { + [CanBeNull] public readonly IDictionary Recommended; + [CanBeNull] public readonly IDictionary DefaultDependencies; + [NotNull] public readonly IDictionary Packages; + + [JsonConstructor] + private EditorManifestJson([CanBeNull] IDictionary recommended, + [CanBeNull] IDictionary defaultDependencies, + [CanBeNull] IDictionary packages) + { + Recommended = recommended; + DefaultDependencies = defaultDependencies; + Packages = packages ?? EmptyDictionary.InstanceDictionary; + } + + public static EditorManifestJson FromJson(string json) + { + return JsonConvert.DeserializeObject(json); + } + + public static EditorManifestJson CreateEmpty() + { + return new EditorManifestJson(null, null, null); + } + } + + [UsedImplicitly(ImplicitUseKindFlags.InstantiatedNoFixedConstructorSignature)] + internal class EditorPackageDetails + { + [CanBeNull] public readonly string Introduced; + [CanBeNull] public readonly string MinimumVersion; + [NotNull] public readonly string Version; + + [JsonConstructor] + public EditorPackageDetails([CanBeNull] string introduced, [CanBeNull] string minimumVersion, + [CanBeNull] string version) + { + Introduced = introduced; + MinimumVersion = minimumVersion; + Version = version ?? string.Empty; + } + } +} diff --git a/resharper/resharper-unity/src/Packages/PackageManager.cs b/resharper/resharper-unity/src/Packages/PackageManager.cs new file mode 100644 index 0000000000..2a7114b37a --- /dev/null +++ b/resharper/resharper-unity/src/Packages/PackageManager.cs @@ -0,0 +1,711 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using JetBrains.Annotations; +using JetBrains.Application.FileSystemTracker; +using JetBrains.Collections; +using JetBrains.Collections.Viewable; +using JetBrains.DataFlow; +using JetBrains.Diagnostics; +using JetBrains.Extension; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Plugins.Unity.ProjectModel; +using JetBrains.ReSharper.Plugins.Unity.Utils; +using JetBrains.Threading; +using JetBrains.Util; +using JetBrains.Util.Logging; + +namespace JetBrains.ReSharper.Plugins.Unity.Packages +{ +#region Notes + // Empirically, this appears to be the process that Unity goes through when adding a new package: + // 1. Download package + // 2. Extract to tmp folder in global cache dir, e.g. ~/Library/Unity/cache/packages/packages.unity.com + // 3. Rename tmp folder to id@version + // 4. Copy extracted package to tmp folder in project's Library/PackageCache + // 5. Rename tmp folder to id@version + // 6. Modify manifest.json (save to copy and replace) + // 7. Modify packages-lock.json (save to copy and replace) (twice, oddly) + // 8. Refresh assets database/recompile + // + // And when removing: + // 1. Modify manifest.json (save to copy and replace) + // 2. Modify packages-lock.json (save to copy and replace) (twice, oddly) + // 3. Rename Library/PackageCache/id@version to tmp name + // 4. Delete the tmp folder + // 5. Refresh assets database/recompile + // + // Updating: + // 1. Download package + // 2. Extract to tmp folder in global cache dir, e.g. ~/Library/Unity/cache/packages/packages.unity.com + // 3. Rename tmp folder to id@version + // 4. Copy extracted package to tmp folder in project's Library/PackageCache + // 5. Rename tmp folder to id@version + // 6. Modify manifest.json (save to copy and replace) + // 7. Modify packages-lock.json (save to copy and replace) (twice, oddly) + // 8. Rename Library/PackageCache/id@version to tmp name + // 9. Delete the tmp folder + // 10. Refresh assets database/recompile +#endregion + + [SolutionComponent] + public class PackageManager + { + private const string DefaultRegistryUrl = "https://packages.unity.com"; + + private readonly Lifetime myLifetime; + private readonly ISolution mySolution; + private readonly ILogger myLogger; + private readonly UnityVersion myUnityVersion; + private readonly IFileSystemTracker myFileSystemTracker; + private readonly GroupingEvent myGroupingEvent; + private readonly DictionaryEvents myPackagesById; + private readonly Dictionary myPackageLifetimes; + private readonly FileSystemPath myPackagesFolder; + private readonly FileSystemPath myPackagesLockPath; + private readonly FileSystemPath myManifestPath; + + [CanBeNull] private FileSystemPath myLastReadGlobalManifestPath; + [CanBeNull] private EditorManifestJson myGlobalManifest; + + public PackageManager(Lifetime lifetime, ISolution solution, ILogger logger, + UnitySolutionTracker unitySolutionTracker, + UnityVersion unityVersion, + IFileSystemTracker fileSystemTracker) + { + myLifetime = lifetime; + mySolution = solution; + myLogger = logger; + myUnityVersion = unityVersion; + myFileSystemTracker = fileSystemTracker; + + // Refresh the packages in the guarded context, safe from reentrancy. + myGroupingEvent = solution.Locks.GroupingEvents.CreateEvent(lifetime, "Unity::PackageManager", + TimeSpan.FromMilliseconds(500), Rgc.Guarded, DoRefresh); + + myPackagesById = new DictionaryEvents(lifetime, "Unity::PackageManager"); + myPackageLifetimes = new Dictionary(); + + myPackagesFolder = mySolution.SolutionDirectory.Combine("Packages"); + myPackagesLockPath = myPackagesFolder.Combine("packages-lock.json"); + myManifestPath = myPackagesFolder.Combine("manifest.json"); + + unitySolutionTracker.IsUnityProject.AdviseUntil(lifetime, value => + { + if (!value) return false; + + ScheduleRefresh(); + + // Track changes to manifest.json and packages-lock.json. Also track changes in the Packages folder, but + // only top level, not recursively. We only want to update the packages if a new package has been added + // or removed + var packagesFolder = mySolution.SolutionDirectory.Combine("Packages"); + fileSystemTracker.AdviseFileChanges(lifetime, packagesFolder.Combine("packages-lock.json"), + _ => ScheduleRefresh()); + fileSystemTracker.AdviseFileChanges(lifetime, packagesFolder.Combine("manifest.json"), + _ => ScheduleRefresh()); + fileSystemTracker.AdviseDirectoryChanges(lifetime, packagesFolder, false, _ => ScheduleRefresh()); + + // We're all set up, terminate the advise + return true; + }); + } + + // DictionaryEvents uses locks internally, so this is thread safe. It gets updated from the guarded reentrancy + // context, so all callbacks also happen within the guarded reentrancy context + public IReadonlyCollectionEvents> Packages => myPackagesById; + + [CanBeNull] + public PackageData GetPackageById(string id) + { + return myPackagesById.TryGetValue(id, out var packageData) ? packageData : null; + } + + public void RefreshPackages() => ScheduleRefresh(); + + private void ScheduleRefresh() => myGroupingEvent.FireIncoming(); + + + [Guard(Rgc.Guarded)] + private void DoRefresh() + { + // If we're reacting to changes in manifest.json, give Unity a chance to update and refresh packages-lock.json + if (!AreFilesReadyForReading()) + { + ScheduleRefresh(); + return; + } + + var newPackages = GetPackages(); + if (newPackages == null) + { + // Something's gone wrong. E.g. invalid JSON files. If things were ok, we'd at least get an empty list. + // Don't wipe out the current packages, it's better to show outdated info rather than nothing. It should + // be fixed soon enough, when the game won't start in Unity + return; + } + + myLogger.DoActivity("UpdatePackages", null, () => UpdatePackages(newPackages)); + } + + private bool AreFilesReadyForReading() + { + if (!myPackagesLockPath.ExistsFile || !myManifestPath.ExistsFile) + return true; + + return IsPackagesLockUpToDate() || ShouldSkipPackagesLock(); + } + + private bool IsPackagesLockUpToDate() + { + return myPackagesLockPath.FileModificationTimeUtc >= myManifestPath.FileModificationTimeUtc; + } + + private bool ShouldSkipPackagesLock() + { + // Has Unity taken too long to update packages-lock.json? If it's taken longer that 2 seconds to update, we + // fall back to manifest.json. If Unity isn't running, is going slow or doesn't get notified of the change + // to manifest.json, we'll fall back and still show up to date results. When Unity catches up, we'll get a + // file change notification on packages-lock.json and update to canonical results. + // Two seconds is a good default, as Unity appears to resolve packages before reloading the AppDomain + + return myManifestPath.FileModificationTimeUtc - myPackagesLockPath.FileModificationTimeUtc > + TimeSpan.FromSeconds(2); + } + + private void UpdatePackages(IEnumerable newPackages) + { + var existingPackages = myPackagesById.Keys.ToJetHashSet(); + foreach (var packageData in newPackages) + { + // If the package.json file has been updated, remove the entry and add the new one. This should capture + // all changes to data + metadata. We don't care too much about duplicates, as this is invalid JSON and + // Unity complains. Last write wins, but at least we won't crash + if (myPackagesById.TryGetValue(packageData.Id, out var existingPackageData) + && existingPackageData.PackageJsonTimestamp != packageData.PackageJsonTimestamp) + { + RemovePackage(packageData.Id); + } + + if (!myPackagesById.ContainsKey(packageData.Id)) + { + var lifetimeDefinition = myLifetime.CreateNested(); + myPackagesById.Add(lifetimeDefinition.Lifetime, packageData.Id, packageData); + + // Note that myPackagesLifetimes is only accessed inside this method, so is thread safe + myPackageLifetimes.Add(lifetimeDefinition.Lifetime, packageData.Id, lifetimeDefinition); + + // Refresh if any editable package.json is modified, so we pick up changes to e.g. dependencies, + // display name, etc. We don't care if BuiltIn or Registry packages are modified because they're + // not user editable + if (packageData.Source == PackageSource.Local && packageData.PackageFolder != null) + { + myFileSystemTracker.AdviseFileChanges(lifetimeDefinition.Lifetime, + packageData.PackageFolder.Combine("package.json"), + _ => ScheduleRefresh()); + } + } + + existingPackages.Remove(packageData.Id); + } + + // Remove any left overs + foreach (var id in existingPackages) + RemovePackage(id); + } + + private void RemovePackage(string packageId) + { + if (myPackageLifetimes.TryGetValue(packageId, out var lifetimeDefinition)) + lifetimeDefinition.Terminate(); + } + + [CanBeNull] + private List GetPackages() + { + return myLogger.Verbose().DoCalculation("GetPackages", null, + () => GetPackagesFromPackagesLockJson() ?? GetPackagesFromManifestJson(), + p => p != null ? $"{p.Count} packages" : "Null list of packages. Something went wrong"); + } + + // Introduced officially in 2019.4, but available behind a switch in manifest.json in 2019.3 + // https://forum.unity.com/threads/add-an-option-to-auto-update-packages.730628/#post-4931882 + [CanBeNull] + private List GetPackagesFromPackagesLockJson() + { + if (!myPackagesLockPath.ExistsFile) + return null; + + if (myManifestPath.ExistsFile && ShouldSkipPackagesLock()) + { + myLogger.Info("packages-lock.json is out of date. Skipping"); + return null; + } + + myLogger.Verbose("Getting packages from packages-lock.json"); + + var appPath = myUnityVersion.GetActualAppPathForSolution(); + var builtInPackagesFolder = UnityInstallationFinder.GetBuiltInPackagesFolder(appPath); + + return myLogger.CatchSilent(() => + { + var packagesLockJson = PackagesLockJson.FromJson(myPackagesLockPath.ReadAllText2().Text); + + var packages = new List(); + foreach (var (id, details) in packagesLockJson.Dependencies) + packages.Add(GetPackageData(id, details, builtInPackagesFolder)); + + return packages; + }); + } + + [CanBeNull] + private List GetPackagesFromManifestJson() + { + if (!myManifestPath.ExistsFile) + return null; + + myLogger.Verbose("Getting packages from manifest.json"); + + try + { + var projectManifest = ManifestJson.FromJson(myManifestPath.ReadAllText2().Text); + + // Now we've deserialised manifest.json, log why we skipped packages-lock.json + LogWhySkippedPackagesLock(projectManifest); + + var appPath = myUnityVersion.GetActualAppPathForSolution(); + var builtInPackagesFolder = UnityInstallationFinder.GetBuiltInPackagesFolder(appPath); + + // Read the editor's default manifest, which gives us the minimum versions for various packages + var globalManifestPath = UnityInstallationFinder.GetPackageManagerDefaultManifest(appPath); + if (globalManifestPath.ExistsFile && myLastReadGlobalManifestPath != globalManifestPath) + { + myLastReadGlobalManifestPath = globalManifestPath; + myGlobalManifest = SafelyReadGlobalManifestFile(globalManifestPath); + } + + var registry = projectManifest.Registry ?? DefaultRegistryUrl; + + var packages = new Dictionary(); + foreach (var (id, version) in projectManifest.Dependencies) + { + if (version.Equals("exclude", StringComparison.OrdinalIgnoreCase)) + continue; + + projectManifest.Lock.TryGetValue(id, out var lockDetails); + packages[id] = GetPackageData(id, version, registry, builtInPackagesFolder, + lockDetails); + } + + // From observation, Unity treats package folders in the Packages folder as actual packages, even if they're + // not registered in manifest.json. They must have a */package.json file, in the root of the package itself + foreach (var child in myPackagesFolder.GetChildDirectories()) + { + // The folder name is not reliable to act as ID, so we'll use the ID from package.json. All other + // packages get the ID from manifest.json or packages-lock.json. This is assumed to be the same as + // the ID in package.json + var packageData = GetPackageDataFromFolder(null, child, PackageSource.Embedded); + if (packageData != null) + packages[packageData.Id] = packageData; + } + + // Calculate the transitive dependencies. Based on observation, we simply go with the highest available + var packagesToProcess = new List(packages.Values); + while (packagesToProcess.Count > 0) + { + var foundDependencies = GetPackagesFromDependencies(registry, builtInPackagesFolder, + packages, packagesToProcess); + foreach (var package in foundDependencies) + packages[package.Id] = package; + + packagesToProcess = foundDependencies; + } + + return new List(packages.Values); + } + catch (Exception e) + { + myLogger.LogExceptionSilently(e); + return null; + } + } + + private void LogWhySkippedPackagesLock(ManifestJson projectManifest) + { + if (ShouldSkipPackagesLock()) + { + if (projectManifest.EnableLockFile.HasValue && !projectManifest.EnableLockFile.Value) + { + myLogger.Info("packages-lock.json is disabled in manifest.json. Old file needs deleting"); + } + else if (myUnityVersion.ActualVersionForSolution.Value < new Version(2019, 3)) + { + myLogger.Info( + "packages-lock.json is not supported by this version of Unity. Perhaps the file is from a newer version?"); + } + + // Most likely reason now is that Unity isn't running + } + } + + [NotNull] + private EditorManifestJson SafelyReadGlobalManifestFile(FileSystemPath globalManifestPath) + { + try + { + return EditorManifestJson.FromJson(globalManifestPath.ReadAllText2().Text); + } + catch (Exception e) + { + // Even if there's an error, cache an empty file, so we don't continually try to read a broken file + myLogger.LogExceptionSilently(e); + return EditorManifestJson.CreateEmpty(); + } + } + + [NotNull] + private PackageData GetPackageData(string id, PackagesLockDependency details, + FileSystemPath builtInPackagesFolder) + { + try + { + PackageData packageData = null; + switch (details.Source) + { + case "embedded": + packageData = GetEmbeddedPackage(id, details.Version); + break; + case "registry": + packageData = GetRegistryPackage(id, details.Version, details.Url ?? DefaultRegistryUrl); + break; + case "builtin": + packageData = GetBuiltInPackage(id, details.Version, builtInPackagesFolder); + break; + case "git": + packageData = GetGitPackage(id, details.Version, details.Hash); + break; + case "local": + packageData = GetLocalPackage(id, details.Version); + break; + case "local-tarball": + packageData = GetLocalTarballPackage(id, details.Version); + break; + } + + return packageData ?? PackageData.CreateUnknown(id, details.Version); + } + catch (Exception e) + { + myLogger.Error(e, $"Error resolving package {id}"); + return PackageData.CreateUnknown(id, details.Version); + } + } + + private PackageData GetPackageData(string id, string version, string registry, + FileSystemPath builtInPackagesFolder, + [CanBeNull] ManifestLockDetails lockDetails) + { + // Order is important here. A package can be listed in manifest.json, but if it also exists in Packages, + // that embedded copy takes precedence. We look for an embedded folder with the same name, but it can be + // under any name - we'll find it again and override it when we look at the other embedded packages. + // Registry packages are the most likely to match, and can't clash with other packages, so check them early. + // The file: protocol is used by local but it can also be a protocol for git, so check git before local. + try + { + return GetEmbeddedPackage(id, id) + ?? GetRegistryPackage(id, version, registry) + ?? GetGitPackage(id, version, lockDetails?.Hash, lockDetails?.Revision) + ?? GetLocalPackage(id, version) + ?? GetLocalTarballPackage(id, version) + ?? GetBuiltInPackage(id, version, builtInPackagesFolder) + ?? PackageData.CreateUnknown(id, version); + } + catch (Exception e) + { + myLogger.Error(e, $"Error resolving package {id}"); + return PackageData.CreateUnknown(id, version); + } + } + + [CanBeNull] + private PackageData GetEmbeddedPackage(string id, string filePath) + { + // Embedded packages live in the Packages folder. When reading from packages-lock.json, the filePath has a + // 'file:' prefix. We make sure it's the folder name when there is no packages-lock.json + var packageFolder = myPackagesFolder.Combine(filePath.TrimFromStart("file:")); + return GetPackageDataFromFolder(id, packageFolder, PackageSource.Embedded); + } + + [CanBeNull] + private PackageData GetRegistryPackage(string id, string version, string registryUrl) + { + // The version parameter isn't necessarily a version, and might not parse correctly. When using + // manifest.json to load packages, we will try to match a registry package before we try to match a git + // package, so the version might even be a URL + var cacheFolder = RelativePath.TryParse($"{id}@{version}"); + if (cacheFolder.IsEmpty) + return null; + + // Unity 2018.3 introduced an additional layer of caching for registry based packages, local to the + // project, so that any edits to the files in the package only affect this project. This is primarily + // for the API updater, which would otherwise modify files in the product wide cache + var packageCacheFolder = mySolution.SolutionDirectory.Combine("Library/PackageCache"); + var packageFolder = packageCacheFolder.Combine(cacheFolder); + var packageData = GetPackageDataFromFolder(id, packageFolder, PackageSource.Registry); + if (packageData != null) + return packageData; + + // Fall back to the product wide cache + packageCacheFolder = UnityCachesFinder.GetPackagesCacheFolder(registryUrl); + if (packageCacheFolder == null || !packageCacheFolder.ExistsDirectory) + return null; + + packageFolder = packageCacheFolder.Combine(cacheFolder); + return GetPackageDataFromFolder(id, packageFolder, PackageSource.Registry); + } + + [CanBeNull] + private PackageData GetBuiltInPackage(string id, string version, FileSystemPath builtInPackagesFolder) + { + // If we can identify the module root of the current project, use it to look up the module + if (builtInPackagesFolder.ExistsDirectory) + { + var packageFolder = builtInPackagesFolder.Combine(id); + return GetPackageDataFromFolder(id, packageFolder, PackageSource.BuiltIn); + } + + // We can't find the actual package. If we "know" it's a module/built in package, then mark it as an + // unresolved built in package, rather than just an unresolved package. The Unity Explorer can use this to + // put the unresolved package in the right place, rather than show as a top level unresolved package simply + // because we haven't found the application package cache yet. + // We can rely on an ID starting with "com.unity.modules." as this is the convention Unity uses. Since they + // control the namespace of their own registries, we can be confident that they won't publish normal + // packages with the same naming convention. We can't be sure for third part registries, but if anyone does + // that, they only have themselves to blame. + // If we don't recognise the package as a built in, let someone else handle it + return id.StartsWith("com.unity.modules.") + ? PackageData.CreateUnknown(id, version, PackageSource.BuiltIn) + : null; + } + + [CanBeNull] + private PackageData GetGitPackage(string id, string version, [CanBeNull] string hash, + [CanBeNull] string revision = null) + { + // If we don't have a hash, we know this isn't a git package + if (hash == null) + return null; + + // This must be a git package, make sure we return something + try + { + var packageFolder = mySolution.SolutionDirectory.Combine($"Library/PackageCache/{id}@{hash}"); + if (!packageFolder.ExistsDirectory) + { + var shortHash = hash.Substring(0, Math.Min(hash.Length, 10)); + packageFolder = mySolution.SolutionDirectory.Combine($"Library/PackageCache/{id}@{shortHash}"); + } + + return GetPackageDataFromFolder(id, packageFolder, PackageSource.Git, + new GitDetails(version, hash, revision)); + } + catch (Exception e) + { + myLogger.Error(e, "Error resolving git package"); + return PackageData.CreateUnknown(id, version); + } + } + + [CanBeNull] + private PackageData GetLocalPackage(string id, string version) + { + // If the version doesn't start with "file:" we know it's not a local package + if (!version.StartsWith("file:")) + return null; + + // This might be a local package, or it might be a local tarball. Or a git package (although we'll have + // resolved that before trying local packages), so return null if we can't resolve it + try + { + var path = version.Substring(5); + var packageFolder = myPackagesFolder.Combine(path); + return packageFolder.ExistsDirectory + ? GetPackageDataFromFolder(id, packageFolder, PackageSource.Local) + : null; + } + catch (Exception e) + { + myLogger.Error(e, $"Error resolving local package {id} at {version}"); + return null; + } + } + + [CanBeNull] + private PackageData GetLocalTarballPackage(string id, string version) + { + if (!version.StartsWith("file:")) + return null; + + // This is a package installed from a package.tgz file. The original file is referenced, but not touched. + // It is expanded into Library/PackageCache with a filename of name@{md5-of-path}-{file-modification-in-epoch-ms} + try + { + var path = version.Substring(5); + var tarballPath = myPackagesFolder.Combine(path); + if (tarballPath.ExistsFile) + { + // Note that this is inherently fragile. If the file is touched, but not imported (i.e. Unity isn't + // running), we won't be able to resolve it at all. We also don't have a watch on the file, so we + // have no way of knowing that the package has been updated or imported. + // On the plus side, I have yet to see anyone talk about tarball packages in the wild. + // Also, once it's been imported, we'll refresh and all will be good + var timestamp = (long) (tarballPath.FileModificationTimeUtc - DateTimeEx.UnixEpoch).TotalMilliseconds; + var hash = GetMd5OfString(tarballPath.FullPath).Substring(0, 12).ToLowerInvariant(); + + var packageFolder = mySolution.SolutionDirectory.Combine($"Library/PackageCache/{id}@{hash}-{timestamp}"); + var tarballLocation = tarballPath.StartsWith(mySolution.SolutionDirectory) + ? tarballPath.RemovePrefix(mySolution.SolutionDirectory.Parent) + : tarballPath; + return GetPackageDataFromFolder(id, packageFolder, PackageSource.LocalTarball, + tarballLocation: tarballLocation); + } + + return null; + } + catch (Exception e) + { + myLogger.Error(e, $"Error resolving local tarball package {version}"); + return null; + } + } + + [CanBeNull] + private PackageData GetPackageDataFromFolder([CanBeNull] string id, + [NotNull] FileSystemPath packageFolder, + PackageSource packageSource, + [CanBeNull] GitDetails gitDetails = null, + [CanBeNull] FileSystemPath tarballLocation = null) + { + if (packageFolder.ExistsDirectory) + { + var packageJsonFile = packageFolder.Combine("package.json"); + if (packageJsonFile.ExistsFile) + { + try + { + var packageJson = PackageJson.FromJson(packageJsonFile.ReadAllText2().Text); + var packageDetails = PackageDetails.FromPackageJson(packageJson, packageFolder); + return new PackageData(id ?? packageDetails.CanonicalName, packageFolder, + packageJsonFile.FileModificationTimeUtc, packageDetails, packageSource, gitDetails, + tarballLocation); + } + catch (Exception e) + { + myLogger.LogExceptionSilently(e); + return null; + } + } + } + + return null; + } + + private static string GetMd5OfString(string value) + { + // Use input string to calculate MD5 hash + using (var md5 = MD5.Create()) + { + var inputBytes = Encoding.UTF8.GetBytes(value); + var hashBytes = md5.ComputeHash(inputBytes); + + // Convert the byte array to hexadecimal string + var sb = new StringBuilder(); + foreach (var t in hashBytes) + sb.Append(t.ToString("X2")); + + return sb.ToString().PadLeft(32, '0'); + } + } + + private List GetPackagesFromDependencies([NotNull] string registry, + [NotNull] FileSystemPath builtInPackagesFolder, + Dictionary resolvedPackages, + List packagesToProcess) + { + var dependencies = new Dictionary(); + + // Find the highest requested version of each dependency of each package being processed. Check all + // dependencies, even if we've already resolved it, in case we find a higher version + foreach (var packageData in packagesToProcess) + { + foreach (var (id, versionString) in packageData.PackageDetails.Dependencies) + { + // Embedded packages take precedence over any version + if (IsEmbeddedPackage(id, resolvedPackages)) + continue; + + if (!JetSemanticVersion.TryParse(versionString, out var dependencyVersion)) + continue; + + var currentMaxVersion = GetCurrentMaxVersion(id, dependencies); + var minimumVersion = GetMinimumVersion(id); + + dependencies[id] = Max(dependencyVersion, Max(currentMaxVersion, minimumVersion)); + } + } + + var newPackages = new List(); + foreach (var (id, version) in dependencies) + { + if (version > GetResolvedVersion(id, resolvedPackages)) + newPackages.Add(GetPackageData(id, version.ToString(), registry, builtInPackagesFolder, null)); + } + + return newPackages; + } + + private static bool IsEmbeddedPackage(string id, Dictionary resolvedPackages) + { + return resolvedPackages.TryGetValue(id, out var packageData) && + packageData.Source == PackageSource.Embedded; + } + + private static JetSemanticVersion GetCurrentMaxVersion( + string id, IReadOnlyDictionary dependencies) + { + return dependencies.TryGetValue(id, out var version) ? version : JetSemanticVersion.Empty; + } + + private JetSemanticVersion GetMinimumVersion(string id) + { + EditorPackageDetails editorPackageDetails = null; + if (myGlobalManifest?.Packages.TryGetValue(id, out editorPackageDetails) == true + && JetSemanticVersion.TryParse(editorPackageDetails?.MinimumVersion, out var version)) + { + return version; + } + + return JetSemanticVersion.Empty; + } + + private static JetSemanticVersion GetResolvedVersion(string id, Dictionary resolvedPackages) + { + if (resolvedPackages.TryGetValue(id, out var packageData) && + JetSemanticVersion.TryParse(packageData.PackageDetails.Version, out var version)) + { + return version; + } + + return JetSemanticVersion.Empty; + } + + private static JetSemanticVersion Max(JetSemanticVersion v1, JetSemanticVersion v2) + { + return v1 > v2 ? v1 : v2; + } + } +} diff --git a/resharper/resharper-unity/src/Packages/PackageSource.cs b/resharper/resharper-unity/src/Packages/PackageSource.cs new file mode 100644 index 0000000000..5df8c4df32 --- /dev/null +++ b/resharper/resharper-unity/src/Packages/PackageSource.cs @@ -0,0 +1,13 @@ +namespace JetBrains.ReSharper.Plugins.Unity.Packages +{ + public enum PackageSource + { + Unknown, + BuiltIn, + Registry, + Embedded, + Local, + LocalTarball, + Git + } +} \ No newline at end of file diff --git a/resharper/resharper-unity/src/Rider/Protocol/BackendUnityHost.cs b/resharper/resharper-unity/src/Rider/Protocol/BackendUnityHost.cs index f3b9169519..3bf413eade 100644 --- a/resharper/resharper-unity/src/Rider/Protocol/BackendUnityHost.cs +++ b/resharper/resharper-unity/src/Rider/Protocol/BackendUnityHost.cs @@ -11,12 +11,19 @@ using JetBrains.ProjectModel; using JetBrains.Rd.Base; using JetBrains.Rd.Tasks; +using JetBrains.ReSharper.Plugins.Unity.Packages; using JetBrains.Rider.Model.Unity; using JetBrains.Rider.Model.Unity.BackendUnity; using JetBrains.Util; namespace JetBrains.ReSharper.Plugins.Unity.Rider.Protocol { + // This component manages subscriptions to the backend/Unity protocol + // * BackendUnityHost should subscribe to the protocol and push into a component, or subscribe to a component and + // push into the protocol + // * Use PassthroughHost to set up subscriptions between frontend and Unity + // * Avoid using BackendUnityModel for subscriptions. It should be used to get values and start tasks + // These guidelines help avoid introducing circular dependencies. Subscriptions should be handled by the host [SolutionComponent] public class BackendUnityHost { @@ -24,13 +31,17 @@ public class BackendUnityHost private UnityEditorState myEditorState; - // The property value will be null when the backend/Unity protocol is not available + // Do not use for subscriptions! Should only be used to read values and start tasks. + // The property's value will be null when the backend/Unity protocol is not available [NotNull] public readonly ViewableProperty BackendUnityModel = new ViewableProperty(null); + // TODO: Remove FrontendBackendHost. It's too easy to get circular dependencies public BackendUnityHost(Lifetime lifetime, ILogger logger, FrontendBackendHost frontendBackendHost, - IThreading threading, IIsApplicationActiveState isApplicationActiveState, + IThreading threading, + IIsApplicationActiveState isApplicationActiveState, + PackageManager packageManager, JetBrains.Application.ActivityTrackingNew.UsageStatistics usageStatistics) { myUsageStatistics = usageStatistics; @@ -40,7 +51,7 @@ public BackendUnityHost(Lifetime lifetime, ILogger logger, BackendUnityModel.ViewNotNull(lifetime, (modelLifetime, backendUnityModel) => { InitialiseModel(backendUnityModel); - AdviseModel(backendUnityModel, modelLifetime); + AdviseModel(backendUnityModel, modelLifetime, packageManager); StartPollingUnityEditorState(backendUnityModel, modelLifetime, frontendBackendHost, threading, isApplicationActiveState, logger); }); @@ -68,6 +79,7 @@ public bool IsConnectionEstablished() return myEditorState != UnityEditorState.Refresh && myEditorState != UnityEditorState.Disconnected; } + // Push values into the protocol private static void InitialiseModel(BackendUnityModel backendUnityModel) { SetConnectionPollHandler(backendUnityModel); @@ -93,11 +105,26 @@ private static void SetRiderProcessId(BackendUnityModel backendUnityModel) } } - private void AdviseModel(BackendUnityModel backendUnityModel, in Lifetime modelLifetime) + // Subscribe to changes from the protocol + private void AdviseModel(BackendUnityModel backendUnityModel, Lifetime modelLifetime, + PackageManager packageManager) { + AdvisePackages(backendUnityModel, modelLifetime, packageManager); TrackActivity(backendUnityModel, modelLifetime); } + private void AdvisePackages(BackendUnityModel backendUnityModel, Lifetime modelLifetime, + PackageManager packageManager) + { + backendUnityModel.UnityApplicationData.Advise(modelLifetime, _ => + { + // When the backend gets new application data, refresh packages, so we can be up to date with + // builtin packages. Note that we don't refresh when we lose the model. This means we're + // potentially viewing stale builtin packages, but that's ok. It's better than clearing all packages + packageManager.RefreshPackages(); + }); + } + private void TrackActivity(BackendUnityModel backendUnityModel, Lifetime modelLifetime) { backendUnityModel.UnityApplicationData.AdviseOnce(modelLifetime, data => diff --git a/resharper/resharper-unity/src/Rider/Protocol/FrontendBackendHost.cs b/resharper/resharper-unity/src/Rider/Protocol/FrontendBackendHost.cs index c3428fbcc5..aa69e83376 100644 --- a/resharper/resharper-unity/src/Rider/Protocol/FrontendBackendHost.cs +++ b/resharper/resharper-unity/src/Rider/Protocol/FrontendBackendHost.cs @@ -1,11 +1,14 @@ using System; +using System.Linq; using JetBrains.Annotations; using JetBrains.Application.Threading; using JetBrains.Application.Threading.Tasks; +using JetBrains.Collections.Viewable; using JetBrains.Lifetimes; using JetBrains.ProjectModel; using JetBrains.ReSharper.Host.Features; using JetBrains.ReSharper.Plugins.Unity.Feature.Caches; +using JetBrains.ReSharper.Plugins.Unity.Packages; using JetBrains.Rider.Model.Unity.FrontendBackend; namespace JetBrains.ReSharper.Plugins.Unity.Rider.Protocol @@ -20,7 +23,9 @@ public class FrontendBackendHost [CanBeNull] public readonly FrontendBackendModel Model; public FrontendBackendHost(Lifetime lifetime, ISolution solution, IShellLocks shellLocks, - DeferredCacheController deferredCacheController, bool isInTests = false) + PackageManager packageManager, + DeferredCacheController deferredCacheController, + bool isInTests = false) { myIsInTests = isInTests; if (myIsInTests) @@ -28,23 +33,10 @@ public FrontendBackendHost(Lifetime lifetime, ISolution solution, IShellLocks sh // This will throw in tests, as GetProtocolSolution will return null var model = solution.GetProtocolSolution().GetFrontendBackendModel(); - AdviseModel(lifetime, model, deferredCacheController, shellLocks); + AdviseModel(lifetime, model, packageManager, deferredCacheController, shellLocks); Model = model; } - private static void AdviseModel(Lifetime lifetime, FrontendBackendModel frontendBackendModel, - DeferredCacheController deferredCacheController, IThreading shellLocks) - { - deferredCacheController.CompletedOnce.Advise(lifetime, v => - { - if (v) - { - shellLocks.Tasks.StartNew(lifetime, Scheduling.MainDispatcher, - () => { frontendBackendModel.IsDeferredCachesCompletedOnce.Value = true; }); - } - }); - } - public bool IsAvailable => !myIsInTests && Model != null; // Convenience method to fire and forget an action on the model (e.g. set a value, fire a signal, etc). Fire and @@ -59,5 +51,78 @@ public void Do(Action action) action(Model); } + + private static void AdviseModel(Lifetime lifetime, + FrontendBackendModel frontendBackendModel, + PackageManager packageManager, + DeferredCacheController deferredCacheController, + IThreading shellLocks) + { + AdvisePackages(lifetime, frontendBackendModel, packageManager); + AdviseIntegrationTestHelpers(lifetime, frontendBackendModel, deferredCacheController, shellLocks); + } + + private static void AdvisePackages(Lifetime lifetime, + FrontendBackendModel frontendBackendModel, + PackageManager packageManager) + { + // Called in the Guarded reentrancy context + packageManager.Packages.AddRemove.Advise(lifetime, args => + { + switch (args.Action) + { + case AddRemove.Add: + var packageData = args.Value.Value; + var packageDetails = packageData.PackageDetails; + var source = ToProtocolPackageSource(packageData.Source); + var dependencies = (from d in packageDetails.Dependencies + select new UnityPackageDependency(d.Key, d.Value)).ToArray(); + var gitDetails = packageData.GitDetails != null + ? new UnityGitDetails(packageData.GitDetails.Url, packageData.GitDetails.Hash, + packageData.GitDetails.Revision) + : null; + var package = new UnityPackage(args.Value.Key, packageDetails.Version, + packageData.PackageFolder?.FullPath, source, packageDetails.DisplayName, + packageDetails.Description, dependencies, packageData.TarballLocation?.FullPath, + gitDetails); + frontendBackendModel.Packages.Add(args.Value.Key, package); + break; + + case AddRemove.Remove: + frontendBackendModel.Packages.Remove(args.Value.Key); + break; + } + }); + } + + private static void AdviseIntegrationTestHelpers(Lifetime lifetime, FrontendBackendModel frontendBackendModel, + DeferredCacheController deferredCacheController, + IThreading shellLocks) + { + deferredCacheController.CompletedOnce.Advise(lifetime, v => + { + if (v) + { + shellLocks.Tasks.StartNew(lifetime, Scheduling.MainDispatcher, + () => { frontendBackendModel.IsDeferredCachesCompletedOnce.Value = true; }); + } + }); + } + + private static UnityPackageSource ToProtocolPackageSource(PackageSource source) + { + switch (source) + { + case PackageSource.Unknown: return UnityPackageSource.Unknown; + case PackageSource.BuiltIn: return UnityPackageSource.BuiltIn; + case PackageSource.Registry: return UnityPackageSource.Registry; + case PackageSource.Embedded: return UnityPackageSource.Embedded; + case PackageSource.Local: return UnityPackageSource.Local; + case PackageSource.LocalTarball: return UnityPackageSource.LocalTarball; + case PackageSource.Git: return UnityPackageSource.Git; + default: + throw new ArgumentOutOfRangeException(nameof(source), source, null); + } + } } } \ No newline at end of file diff --git a/resharper/resharper-unity/src/Rider/Protocol/PassthroughHost.cs b/resharper/resharper-unity/src/Rider/Protocol/PassthroughHost.cs index a53f6f5125..a0613b64d1 100644 --- a/resharper/resharper-unity/src/Rider/Protocol/PassthroughHost.cs +++ b/resharper/resharper-unity/src/Rider/Protocol/PassthroughHost.cs @@ -49,7 +49,8 @@ public PassthroughHost(Lifetime lifetime, { AdviseFrontendToUnityModel(unityProjectLifetime, model); - // Advise the backend/Unity model as high priority so we can add our subscriptions first + // Advise the backend/Unity model as high priority so we get called back before other subscribers. + // This allows us to populate the protocol on reconnection before other subscribes start to advise using (Signal.PriorityAdviseCookie.Create()) { backendUnityHost.BackendUnityModel.ViewNotNull(unityProjectLifetime, diff --git a/resharper/resharper-unity/src/UnityInstallationFinder.cs b/resharper/resharper-unity/src/UnityInstallationFinder.cs index 06578863f4..83f1530f5c 100644 --- a/resharper/resharper-unity/src/UnityInstallationFinder.cs +++ b/resharper/resharper-unity/src/UnityInstallationFinder.cs @@ -26,13 +26,13 @@ public static UnityInstallationInfo GetApplicationInfo(Version version, UnityVer var bestChoice = TryGetBestChoice(version, possibleWithVersion); if (bestChoice != null) return bestChoice; - + // best choice not found by version - try version by path then var pathForSolution = unityVersion.GetActualAppPathForSolution(); var versionByAppPath = UnityVersion.GetVersionByAppPath(pathForSolution); if (versionByAppPath!=null) possibleWithVersion.Add(new UnityInstallationInfo(versionByAppPath, pathForSolution)); - + // check best choice again, since newly added version may be best one bestChoice = TryGetBestChoice(version, possibleWithVersion); if (bestChoice != null) @@ -89,6 +89,24 @@ public static FileSystemPath GetApplicationContentsPath(FileSystemPath applicati return FileSystemPath.Empty; } + // TODO: We shouldn't have to pass in appPath here + // But appPath is being calculated by UnityVersion, not UnityInstallationFinder + [NotNull] + public static FileSystemPath GetBuiltInPackagesFolder([NotNull] FileSystemPath applicationPath) + { + return applicationPath.IsEmpty + ? applicationPath + : GetApplicationContentsPath(applicationPath).Combine("Resources/PackageManager/BuiltInPackages"); + } + + [NotNull] + public static FileSystemPath GetPackageManagerDefaultManifest(FileSystemPath applicationPath) + { + return applicationPath.IsEmpty + ? applicationPath + : GetApplicationContentsPath(applicationPath).Combine("Resources/PackageManager/Editor/manifest.json"); + } + private static List GetPossibleInstallationInfos() { var installations = GetPossibleApplicationPaths(); @@ -234,7 +252,7 @@ public static FileSystemPath GetAppPathByDll(XmlElement documentElement) { var referencePathElement = documentElement.ChildElements() .Where(a => a.Name == "ItemGroup").SelectMany(b => b.ChildElements()) - .Where(c => c.Name == "Reference" && + .Where(c => c.Name == "Reference" && (c.GetAttribute("Include").Equals("UnityEngine") // we can't use StartsWith here, some "UnityEngine*" libs are in packages || c.GetAttribute("Include").Equals("UnityEngine.CoreModule") // Dll project may have this reference instead of UnityEngine.dll || c.GetAttribute("Include").Equals("UnityEditor"))) @@ -287,7 +305,7 @@ private static FileSystemPath GoUpForUnityExecutable(FileSystemPath filePath, st // For Player Projects it might be: Editor/Data/PlaybackEngines/LinuxStandaloneSupport/Variations/mono/Managed/UnityEngine.dll // For Editor: Editor\Data\Managed\UnityEngine.dll // Or // Editor\Data\Managed\UnityEngine\UnityEngine.dll - + var path = filePath; while (!path.IsEmpty) { @@ -327,13 +345,13 @@ private static void AssertApplicationPath(FileSystemPath path) break; } } - + public static List GetPossibleMonoPaths() { var possibleApplicationPaths = GetPossibleApplicationPaths(); switch (PlatformUtil.RuntimePlatform) { - // dotTrace team uses these constants to detect unity's mono. + // dotTrace team uses these constants to detect unity's mono. // If you want change any constant, please notify dotTrace team case PlatformUtil.Platform.MacOsX: { diff --git a/resharper/resharper-unity/src/Utils/UnityCachesFinder.cs b/resharper/resharper-unity/src/Utils/UnityCachesFinder.cs new file mode 100644 index 0000000000..14d774510e --- /dev/null +++ b/resharper/resharper-unity/src/Utils/UnityCachesFinder.cs @@ -0,0 +1,52 @@ +using System; +using JetBrains.Annotations; +using JetBrains.Util; + +namespace JetBrains.ReSharper.Plugins.Unity.Utils +{ + public class UnityCachesFinder + { + [CanBeNull] + public static FileSystemPath GetPackagesCacheFolder(string registry) + { + const string defaultRegistryHost = "packages.unity.com"; + + var cacheRoot = GetPackagesCacheRoot(); + + var registryHost = defaultRegistryHost; + if (Uri.TryCreate(registry, UriKind.Absolute, out var registryUri)) + { + registryHost = registryUri.Host; + } + + var cacheFolder = cacheRoot.Combine(registryHost); + if (!cacheFolder.ExistsDirectory) + cacheFolder = cacheRoot.Combine(defaultRegistryHost); + + return cacheFolder.ExistsDirectory ? cacheFolder : null; + } + + [NotNull] + private static FileSystemPath GetPackagesCacheRoot() + { + var upmCachePath = Environment.GetEnvironmentVariable("UPM_CACHE_PATH"); + if (!string.IsNullOrEmpty(upmCachePath)) + return FileSystemPath.Parse(upmCachePath); + + switch (PlatformUtil.RuntimePlatform) + { + case PlatformUtil.Platform.Windows: + return Environment.SpecialFolder.LocalApplicationData.ToFileSystemPath().Combine("Unity/cache/packages"); + case PlatformUtil.Platform.MacOsX: + return Environment.SpecialFolder.Personal.ToFileSystemPath().Combine("Library/Unity/cache/packages"); + case PlatformUtil.Platform.Linux: + // This will check $XDG_CONFIG_HOME, if it exists, and fall back to ~/.config + // TODO: Check this works + return Environment.SpecialFolder.ApplicationData.ToFileSystemPath().Combine("unity3d/cache/packages"); + default: + throw new ArgumentOutOfRangeException(); + } + } + } + +} \ No newline at end of file diff --git a/resharper/resharper-unity/src/resharper-unity.csproj b/resharper/resharper-unity/src/resharper-unity.csproj index 037439448e..c12fa739ac 100644 --- a/resharper/resharper-unity/src/resharper-unity.csproj +++ b/resharper/resharper-unity/src/resharper-unity.csproj @@ -165,7 +165,6 @@ - diff --git a/resharper/resharper-unity/test/src/Rider/FrontendBackendHostStub.cs b/resharper/resharper-unity/test/src/Rider/FrontendBackendHostStub.cs index 05bfb5a813..de7db8ae17 100644 --- a/resharper/resharper-unity/test/src/Rider/FrontendBackendHostStub.cs +++ b/resharper/resharper-unity/test/src/Rider/FrontendBackendHostStub.cs @@ -2,6 +2,7 @@ using JetBrains.Lifetimes; using JetBrains.ProjectModel; using JetBrains.ReSharper.Plugins.Unity.Feature.Caches; +using JetBrains.ReSharper.Plugins.Unity.Packages; using JetBrains.ReSharper.Plugins.Unity.Rider.Protocol; namespace JetBrains.ReSharper.Plugins.Unity.Tests.Rider @@ -10,8 +11,9 @@ namespace JetBrains.ReSharper.Plugins.Unity.Tests.Rider public class FrontendBackendHostStub : FrontendBackendHost { public FrontendBackendHostStub(Lifetime lifetime, ISolution solution, IShellLocks shellLocks, + PackageManager packageManager, DeferredCacheController deferredCacheController) - : base(lifetime, solution, shellLocks, deferredCacheController, true) + : base(lifetime, solution, shellLocks, packageManager, deferredCacheController, true) { } } diff --git a/rider/.idea/libraries-with-intellij-classes.xml b/rider/.idea/libraries-with-intellij-classes.xml new file mode 100644 index 0000000000..9fa31567f1 --- /dev/null +++ b/rider/.idea/libraries-with-intellij-classes.xml @@ -0,0 +1,65 @@ + + + + + + \ No newline at end of file diff --git a/rider/.idea/runConfigurations/runIde__quick_.xml b/rider/.idea/runConfigurations/runIde__quick_.xml index a902fc3bc9..142162bf77 100644 --- a/rider/.idea/runConfigurations/runIde__quick_.xml +++ b/rider/.idea/runConfigurations/runIde__quick_.xml @@ -4,7 +4,7 @@ @@ -13,6 +13,9 @@