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 @@
+ true
+ true
+ false
\ No newline at end of file
diff --git a/rider/protocol/src/main/kotlin/model/frontendBackend/FrontendBackendModel.kt b/rider/protocol/src/main/kotlin/model/frontendBackend/FrontendBackendModel.kt
index 731541a1ab..d7c62239f3 100644
--- a/rider/protocol/src/main/kotlin/model/frontendBackend/FrontendBackendModel.kt
+++ b/rider/protocol/src/main/kotlin/model/frontendBackend/FrontendBackendModel.kt
@@ -17,6 +17,36 @@ import model.lib.Library
// Callback is an RPC method (with return value) that is implemented by the frontend/called by the backend
@Suppress("unused")
object FrontendBackendModel : Ext(SolutionModel.Solution) {
+
+ private val UnityPackageSource = enum {
+ +"Unknown"
+ +"BuiltIn"
+ +"Registry"
+ +"Embedded"
+ +"Local"
+ +"LocalTarball"
+ +"Git"
+ }
+
+ private val UnityPackage = structdef {
+ field("id", string)
+ field("version", string)
+ field("packageFolderPath", string.nullable)
+ field("source", UnityPackageSource)
+ field("displayName", string)
+ field("description", string.nullable)
+ field("dependencies", array(structdef("unityPackageDependency") {
+ field("id", string)
+ field("version", string)
+ }))
+ field("tarballLocation", string.nullable)
+ field("gitDetails", structdef("unityGitDetails") {
+ field("url", string)
+ field("hash", string.nullable)
+ field("revision", string.nullable)
+ }.nullable)
+ }
+
private val UnitTestLaunchPreference = enum {
+"NUnit"
+"EditMode"
@@ -25,10 +55,8 @@ object FrontendBackendModel : Ext(SolutionModel.Solution) {
}
private val shaderInternScope = internScope()
-
private val shaderContextDataBase = baseclass {}
private val autoShaderContextData = classdef extends shaderContextDataBase {}
-
private val shaderContextData = classdef extends shaderContextDataBase {
field("path", string.interned(shaderInternScope))
field("name", string.interned(shaderInternScope))
@@ -43,6 +71,8 @@ object FrontendBackendModel : Ext(SolutionModel.Solution) {
setting(Kotlin11Generator.Namespace, "com.jetbrains.rider.model.unity.frontendBackend")
setting(CSharp50Generator.Namespace, "JetBrains.Rider.Model.Unity.FrontendBackend")
+ property("hasUnityReference", bool).documentation = "True when the current project is a Unity project. Either full Unity project or class library"
+
// Connection to Unity editor
property("unityEditorConnected", bool).documentation = "Is the backend/Unity protocol connected?"
property("unityEditorState", Library.UnityEditorState)
@@ -61,13 +91,11 @@ object FrontendBackendModel : Ext(SolutionModel.Solution) {
property("mergeParameters", string)
})
- // Misc backend/fronted context
- property("hasUnityReference", bool).documentation = "True when the current project is a Unity project. Either full Unity project or class library"
- property("externalDocContext", string).documentation = "Fully qualified type or method name at the location of the text caret. Used for external help URL"
-
field("playControls", Library.PlayControls)
field("consoleLogging", Library.ConsoleLogging)
+ map("packages", string, UnityPackage)
+
// Unit testing
property("unitTestPreference", UnitTestLaunchPreference.nullable).documentation = "Selected unit testing mode. Everything is handled by the backend, but this setting is from a frontend combobox"
@@ -103,6 +131,9 @@ object FrontendBackendModel : Ext(SolutionModel.Solution) {
callback("attachDebuggerToUnityEditor", void, bool).documentation = "Tell the frontend to attach the debugger to the Unity editor. Used for debugging unit tests"
callback("allowSetForegroundWindow", void, bool).documentation = "Tell the frontend to call AllowSetForegroundWindow for the current Unity editor process ID. Called before the backend tells Unity to show itself"
+ // Misc backend/fronted context
+ property("externalDocContext", string).documentation = "Fully qualified type or method name at the location of the text caret. Used for external help URL"
+
// Only used in integration tests
property("riderFrontendTests", bool)
call("runMethodInUnity", Library.RunMethodData, Library.RunMethodResult)
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/FrontendBackendHost.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/FrontendBackendHost.kt
index 9f5b54871c..40d8d3e7d1 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/FrontendBackendHost.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/FrontendBackendHost.kt
@@ -19,6 +19,7 @@ import com.jetbrains.rider.debugger.RiderDebugActiveDotNetSessionsTracker
import com.jetbrains.rider.model.unity.LogEvent
import com.jetbrains.rider.model.unity.frontendBackend.frontendBackendModel
import com.jetbrains.rider.plugins.unity.actions.StartUnityAction
+import com.jetbrains.rider.plugins.unity.packageManager.PackageManager
import com.jetbrains.rider.plugins.unity.run.DefaultRunConfigurationGenerator
import com.jetbrains.rider.plugins.unity.run.configurations.UnityAttachToEditorRunConfiguration
import com.jetbrains.rider.plugins.unity.run.configurations.UnityDebugConfigurationType
@@ -95,6 +96,14 @@ class FrontendBackendHost(project: Project) : ProtocolSubscribedProjectComponent
task
}
+
+ model.packages.adviseAddRemove(projectComponentLifetime) { action, id, p ->
+ val packageManager = PackageManager.getInstance(project)
+ when (action) {
+ AddRemove.Add -> packageManager.addPackage(id, p)
+ AddRemove.Remove -> packageManager.removePackage(id)
+ }
+ }
}
companion object {
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/AssetsRoot.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/AssetsRoot.kt
deleted file mode 100644
index 9e3797e9c8..0000000000
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/AssetsRoot.kt
+++ /dev/null
@@ -1,140 +0,0 @@
-package com.jetbrains.rider.plugins.unity.explorer
-
-import com.intellij.ide.projectView.PresentationData
-import com.intellij.ide.util.treeView.AbstractTreeNode
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.util.text.StringUtil
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.ui.SimpleTextAttributes
-import com.intellij.workspaceModel.ide.WorkspaceModel
-import com.intellij.workspaceModel.ide.impl.virtualFile
-import com.jetbrains.rd.util.getOrCreate
-import com.jetbrains.rider.model.*
-import com.jetbrains.rider.projectView.ProjectModelStatuses
-import com.jetbrains.rider.projectView.views.addAdditionalText
-import com.jetbrains.rider.projectView.views.presentSyncNode
-import com.jetbrains.rider.projectView.workspace.*
-import icons.UnityIcons
-
-class AssetsRoot(project: Project, virtualFile: VirtualFile)
- : UnityExplorerNode(project, virtualFile, listOf(), AncestorNodeType.Assets) {
-
- private val referenceRoot = ReferenceRoot(project)
-
- override fun update(presentation: PresentationData) {
- if (!virtualFile.isValid) return
- presentation.addText("Assets", SimpleTextAttributes.REGULAR_ATTRIBUTES)
- presentation.setIcon(UnityIcons.Explorer.AssetsRoot)
-
- val solutionEntity = WorkspaceModel.getInstance(myProject).getSolutionEntity() ?: return
- val descriptor = solutionEntity.descriptor as? RdSolutionDescriptor ?: return
-
- if (isSolutionOrProjectsSync()) {
- presentation.presentSyncNode()
- } else {
- when (descriptor.state) {
- RdSolutionState.Default -> {
- if (descriptor.projectsCount.failed + descriptor.projectsCount.unloaded > 0) {
- presentProjectsCount(presentation, descriptor.projectsCount, true)
- }
- }
- RdSolutionState.WithErrors -> presentation.addAdditionalText("load failed")
- RdSolutionState.WithWarnings -> presentProjectsCount(presentation, descriptor.projectsCount, true)
- }
- }
- }
-
- private fun isSolutionOrProjectsSync(): Boolean {
- val projectModelStatuses = ProjectModelStatuses.getInstance(myProject)
- if (projectModelStatuses.isSolutionInSync()) return true
-
- val projects = WorkspaceModel.getInstance(myProject).findProjects()
- return projects.any { project ->
- projectModelStatuses.getProjectStatus(project) != null
- }
- }
-
- private fun presentProjectsCount(presentation: PresentationData, count: RdProjectsCount, showZero: Boolean) {
- if (count.total == 0 && !showZero) return
-
- var text = "${count.total} ${StringUtil.pluralize("project", count.total)}"
- val unloadedCount = count.failed + count.unloaded
- if (unloadedCount > 0) {
- text += ", $unloadedCount unloaded"
- }
- presentation.addAdditionalText(text)
- }
-
- override fun isAlwaysExpand() = true
-
- override fun calculateChildren(): MutableList> {
- val result = super.calculateChildren()
- result.add(0, referenceRoot)
- return result
- }
-}
-
-class ReferenceRoot(project: Project) : AbstractTreeNode(project, key) {
-
- companion object {
- val key = Any()
- }
-
- override fun update(presentation: PresentationData) {
- presentation.presentableText = "References"
- presentation.setIcon(UnityIcons.Explorer.ReferencesRoot)
- }
-
- override fun getChildren(): MutableCollection> {
- val referenceNames = hashMapOf()
- val visitor = object : ProjectModelEntityVisitor() {
- override fun visitReference(entity: ProjectModelEntity): Result {
- if (entity.isAssemblyReference()) {
- val virtualFile = entity.url?.virtualFile
- if (virtualFile != null) {
- val item = referenceNames.getOrCreate(entity.descriptor.location.toString(), {
- ReferenceItemNode(project!!, entity.descriptor.name, virtualFile, arrayListOf())
- })
- item.entityReferences.add(entity.toReference())
- }
- }
- return Result.Stop
- }
- }
- visitor.visit(project!!)
-
- val children = arrayListOf>()
- for ((_, item) in referenceNames) {
- children.add(item)
- }
- return children
- }
-}
-
-class ReferenceItemNode(
- project: Project,
- private val referenceName: String,
- virtualFile: VirtualFile,
- override val entityReferences: ArrayList
-) : UnityExplorerNode(project, virtualFile, listOf(), AncestorNodeType.Assets) {
-
- override fun isAlwaysLeaf() = true
-
- override fun update(presentation: PresentationData) {
- presentation.presentableText = referenceName
- presentation.setIcon(UnityIcons.Explorer.Reference)
- }
-
- override fun navigate(requestFocus: Boolean) {
- // the same VirtualFile may be added as a file inside Assets folder, so simple click on the reference would jump to that file
- }
-
- // Allows View In Assembly Explorer and Properties actions to work
- override val entities: List
- get() = entityReferences.mapNotNull { it.getEntity(project!!) }
-
- override val entity: ProjectModelEntity?
- get() = entities.firstOrNull()
- override val entityReference: ProjectModelEntityReference?
- get() = entityReferences.firstOrNull()
-}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/AssetsRootNode.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/AssetsRootNode.kt
new file mode 100644
index 0000000000..c14460cd23
--- /dev/null
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/AssetsRootNode.kt
@@ -0,0 +1,77 @@
+package com.jetbrains.rider.plugins.unity.explorer
+
+import com.intellij.ide.projectView.PresentationData
+import com.intellij.ide.util.treeView.AbstractTreeNode
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.ui.SimpleTextAttributes
+import com.intellij.workspaceModel.ide.WorkspaceModel
+import com.jetbrains.rider.model.RdProjectsCount
+import com.jetbrains.rider.model.RdSolutionDescriptor
+import com.jetbrains.rider.model.RdSolutionState
+import com.jetbrains.rider.projectView.ProjectModelStatuses
+import com.jetbrains.rider.projectView.views.addAdditionalText
+import com.jetbrains.rider.projectView.views.presentSyncNode
+import com.jetbrains.rider.projectView.workspace.findProjects
+import com.jetbrains.rider.projectView.workspace.getSolutionEntity
+import icons.UnityIcons
+
+@Suppress("UnstableApiUsage")
+class AssetsRootNode(project: Project, virtualFile: VirtualFile)
+ : UnityExplorerFileSystemNode(project, virtualFile, emptyList(), AncestorNodeType.Assets) {
+
+ private val referenceRoot = ReferenceRootNode(project)
+
+ override fun update(presentation: PresentationData) {
+ if (!virtualFile.isValid) return
+ presentation.addText("Assets", SimpleTextAttributes.REGULAR_ATTRIBUTES)
+ presentation.setIcon(UnityIcons.Explorer.AssetsRoot)
+
+ val solutionEntity = WorkspaceModel.getInstance(myProject).getSolutionEntity() ?: return
+ val descriptor = solutionEntity.descriptor as? RdSolutionDescriptor ?: return
+
+ if (isSolutionOrProjectsSync()) {
+ presentation.presentSyncNode()
+ } else {
+ when (descriptor.state) {
+ RdSolutionState.Default -> {
+ if (descriptor.projectsCount.failed + descriptor.projectsCount.unloaded > 0) {
+ presentProjectsCount(presentation, descriptor.projectsCount, true)
+ }
+ }
+ RdSolutionState.WithErrors -> presentation.addAdditionalText("load failed")
+ RdSolutionState.WithWarnings -> presentProjectsCount(presentation, descriptor.projectsCount, true)
+ }
+ }
+ }
+
+ private fun isSolutionOrProjectsSync(): Boolean {
+ val projectModelStatuses = ProjectModelStatuses.getInstance(myProject)
+ if (projectModelStatuses.isSolutionInSync()) return true
+
+ val projects = WorkspaceModel.getInstance(myProject).findProjects()
+ return projects.any { project ->
+ projectModelStatuses.getProjectStatus(project) != null
+ }
+ }
+
+ private fun presentProjectsCount(presentation: PresentationData, count: RdProjectsCount, showZero: Boolean) {
+ if (count.total == 0 && !showZero) return
+
+ var text = "${count.total} ${StringUtil.pluralize("project", count.total)}"
+ val unloadedCount = count.failed + count.unloaded
+ if (unloadedCount > 0) {
+ text += ", $unloadedCount unloaded"
+ }
+ presentation.addAdditionalText(text)
+ }
+
+ override fun isAlwaysExpand() = true
+
+ override fun calculateChildren(): MutableList> {
+ val result = super.calculateChildren()
+ result.add(0, referenceRoot)
+ return result
+ }
+}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/PackageNodes.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/PackageNodes.kt
new file mode 100644
index 0000000000..783c22e117
--- /dev/null
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/PackageNodes.kt
@@ -0,0 +1,381 @@
+package com.jetbrains.rider.plugins.unity.explorer
+
+import com.intellij.ide.projectView.PresentationData
+import com.intellij.ide.util.treeView.AbstractTreeNode
+import com.intellij.openapi.fileEditor.OpenFileDescriptor
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.text.StringUtil
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.ui.SimpleTextAttributes
+import com.jetbrains.rider.plugins.unity.packageManager.PackageData
+import com.jetbrains.rider.plugins.unity.packageManager.PackageManager
+import com.jetbrains.rider.plugins.unity.packageManager.PackageSource
+import com.jetbrains.rider.projectView.views.*
+import com.jetbrains.rider.projectView.views.solutionExplorer.SolutionExplorerViewPane
+import icons.UnityIcons
+
+class PackagesRootNode(project: Project, packagesFolder: VirtualFile)
+ : UnityExplorerFileSystemNode(project, packagesFolder, emptyList(), AncestorNodeType.FileSystem) {
+
+ private val packageManager = PackageManager.getInstance(myProject)
+
+ override fun update(presentation: PresentationData) {
+ if (!virtualFile.isValid) return
+ presentation.presentableText = "Packages"
+ presentation.setIcon(UnityIcons.Explorer.PackagesRoot)
+ }
+
+ override fun calculateChildren(): MutableList> {
+
+ // Add file system children, which will include embedded packages
+ val children = super.calculateChildren()
+
+ val allPackages = packageManager.getPackages()
+
+ // Add the "Read Only" node for modules and referenced packages. Don't add the node if we haven't loaded
+ // packages yet
+ if (allPackages.any { it.source.isReadOnly() }) {
+ children.add(0, ReadOnlyPackagesRootNode(myProject, packageManager))
+ }
+
+ // Also include any local (file: based) packages, plus all unresolved packages
+ allPackages.filter { it.source == PackageSource.Local }.forEach { addPackageNode(children, it) }
+ allPackages.filter { it.source == PackageSource.Unknown }.forEach { addPackageNode(children, it) }
+
+ return children
+ }
+
+ override fun createNode(virtualFile: VirtualFile, nestedFiles: List>): FileSystemNodeBase {
+ // If the child folder is an embedded package, add it as a package node
+ if (virtualFile.isDirectory) {
+ packageManager.tryGetPackage(virtualFile)?.let {
+ return PackageNode(myProject, packageManager, virtualFile, it)
+ }
+ }
+ return super.createNode(virtualFile, nestedFiles)
+ }
+
+ // Required for "Locate in Solution Explorer". Treat as "can contain". Returning false stops the visitor. If we
+ // return true, which is "maybe", then the child nodes are expanded as the visitor keeps looking
+ override fun contains(file: VirtualFile): Boolean {
+ return children.any { (it as? SolutionViewNode)?.contains(file) == true }
+ }
+
+ private fun addPackageNode(children: MutableList>, thePackage: PackageData) {
+ if (thePackage.packageFolder != null) {
+ children.add(PackageNode(myProject, packageManager, thePackage.packageFolder, thePackage))
+ }
+ else {
+ children.add(UnknownPackageNode(myProject, thePackage))
+ }
+ }
+}
+
+class PackageNode(project: Project, private val packageManager: PackageManager, packageFolder: VirtualFile, private val packageData: PackageData)
+ : UnityExplorerFileSystemNode(project, packageFolder, emptyList(), AncestorNodeType.fromPackageData(packageData)), Comparable> {
+
+ init {
+ icon = when (packageData.source) {
+ PackageSource.Registry -> UnityIcons.Explorer.ReferencedPackage
+ PackageSource.Embedded -> UnityIcons.Explorer.EmbeddedPackage
+ PackageSource.Local -> UnityIcons.Explorer.LocalPackage
+ PackageSource.LocalTarball -> UnityIcons.Explorer.LocalTarballPackage
+ PackageSource.BuiltIn -> UnityIcons.Explorer.BuiltInPackage
+ PackageSource.Git -> UnityIcons.Explorer.GitPackage
+ PackageSource.Unknown -> UnityIcons.Explorer.UnknownPackage
+ }
+ }
+
+ override fun getName() = packageData.displayName
+
+ override fun update(presentation: PresentationData) {
+ presentation.addText(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
+ presentation.setIcon(icon)
+ presentation.addNonIndexedMark(myProject, virtualFile)
+
+ // Note that this might also set the tooltip if we have too many projects underneath
+ if (UnityExplorer.getInstance(myProject).showProjectNames) {
+ addProjects(presentation)
+ }
+
+ val existingTooltip = presentation.tooltip ?: ""
+
+ var tooltip = "" + getPackageTooltip(name, packageData)
+ tooltip += when (packageData.source) {
+ PackageSource.Embedded -> if (virtualFile.name != name) "
Tarball location: ${packageData.tarballLocation}"
+ PackageSource.Git -> {
+ var text = "
"
+ text += if (!packageData.gitUrl.isNullOrEmpty()) {
+ "Git URL: ${packageData.gitUrl}"
+ } else {
+ "Unknown Git URL"
+ }
+ if (!packageData.gitHash.isNullOrEmpty()) {
+ text += " Hash: ${packageData.gitHash}"
+ }
+ if (!packageData.gitRevision.isNullOrEmpty()) {
+ text += " Revision: ${packageData.gitRevision}"
+ }
+ text
+ }
+ else -> ""
+ }
+ if (existingTooltip.isNotEmpty()) {
+ tooltip += "
$existingTooltip"
+ }
+ tooltip += ""
+ presentation.tooltip = tooltip
+ }
+
+ override fun calculateChildren(): MutableList> {
+ val children = super.calculateChildren()
+
+ if (packageData.dependencies.isNotEmpty()) {
+ children.add(0, PackageDependenciesRoot(myProject, packageManager, packageData))
+ }
+
+ return children
+ }
+
+ override fun compareTo(other: AbstractTreeNode<*>): Int {
+ // Compare by display name, rather than the default file name
+ return String.CASE_INSENSITIVE_ORDER.compare(name, other.name)
+ }
+}
+
+class PackageDependenciesRoot(project: Project, private val packageManager: PackageManager, private val packageData: PackageData)
+ : SolutionViewNode(project, packageData) {
+
+ override fun update(presentation: PresentationData) {
+ presentation.presentableText = "Dependencies"
+ presentation.setIcon(UnityIcons.Explorer.DependenciesRoot)
+ }
+
+ override fun calculateChildren(): MutableList> {
+ val children = mutableListOf>()
+ for ((name, version) in packageData.dependencies) {
+ children.add(PackageDependencyItemNode(myProject, packageManager, name, version))
+ }
+ return children
+ }
+
+ override fun contains(file: VirtualFile) = false
+}
+
+class PackageDependencyItemNode(project: Project, private val packageManager: PackageManager, private val packageId: String, version: String)
+ : SolutionViewNode(project, "$packageId@$version") {
+
+ init {
+ myName = "$packageId@$version"
+ }
+
+ override fun calculateChildren(): MutableList> = mutableListOf()
+ override fun isAlwaysLeaf() = true
+ override fun contains(file: VirtualFile) = false
+
+ override fun update(presentation: PresentationData) {
+ presentation.presentableText = name
+ presentation.setIcon(UnityIcons.Explorer.PackageDependency)
+ }
+
+ override fun canNavigate() = packageManager.tryGetPackage(packageId)?.packageFolder != null
+ override fun navigate(requestFocus: Boolean) {
+ val packageFolder = packageManager.tryGetPackage(packageId)?.packageFolder ?: return
+ myProject.navigateToSolutionView(packageFolder, requestFocus)
+ }
+}
+
+class ReadOnlyPackagesRootNode(project: Project, private val packageManager: PackageManager)
+ : SolutionViewNode(project, key) {
+
+ companion object {
+ val key = Any()
+ }
+
+ override fun update(presentation: PresentationData) {
+ presentation.presentableText = "Read only"
+ presentation.setIcon(UnityIcons.Explorer.ReadOnlyPackagesRoot)
+ }
+
+ override fun calculateChildren(): MutableList> {
+ val children = mutableListOf>()
+
+ // If we have any packages, we'll have modules
+ children.add(BuiltinPackagesRootNode(myProject, packageManager))
+
+ for (packageData in packageManager.getPackages().filter { it.source.isReadOnly() && it.source != PackageSource.BuiltIn }) {
+ if (packageData.packageFolder == null) {
+ children.add(UnknownPackageNode(myProject, packageData))
+ }
+ else {
+ children.add(PackageNode(myProject, packageManager, packageData.packageFolder, packageData))
+ }
+ }
+ return children
+ }
+
+ // Treat as "can contain". Returning false stops the visitor. If we return true, which is "maybe", then the child
+ // nodes are expanded as the visitor keeps looking
+ override fun contains(file: VirtualFile): Boolean {
+ return children.any { (it as? SolutionViewNode)?.contains(file) == true }
+ }
+}
+
+class BuiltinPackagesRootNode(project: Project, private val packageManager: PackageManager)
+ : SolutionViewNode(project, key) {
+
+ companion object {
+ val key = Any()
+ }
+
+ override fun update(presentation: PresentationData) {
+ presentation.presentableText = "Modules"
+ presentation.setIcon(UnityIcons.Explorer.BuiltInPackagesRoot)
+ }
+
+ override fun calculateChildren(): MutableList> {
+ val children = mutableListOf>()
+ for (packageData in packageManager.getPackages().filter { it.source == PackageSource.BuiltIn }) {
+
+ // All modules should have a package folder, or it means we haven't been able to resolve it
+ if (packageData.packageFolder == null) {
+ children.add(UnknownPackageNode(myProject, packageData))
+ }
+ else {
+ children.add(BuiltinPackageNode(myProject, packageData))
+ }
+ }
+ return children
+ }
+
+ // Required for "Locate in Solution Explorer". Treat as "can contain". Returning false stops the visitor. If we
+ // return true, which is "maybe", then the child nodes are expanded as the visitor keeps looking
+ override fun contains(file: VirtualFile): Boolean {
+ return children.any { (it as? SolutionViewNode)?.contains(file) == true }
+ }
+}
+
+// Represents a module, built in part of the Unity product. We show it as a single node with no children, unless we have
+// "show hidden items" enabled, in which case we show the package folder, including the package.json.
+// Note that a module can have dependencies. Perhaps we want to always show this as a folder, including the Dependencies
+// node?
+class BuiltinPackageNode(project: Project, private val packageData: PackageData)
+ : UnityExplorerFileSystemNode(project, packageData.packageFolder!!, emptyList(), AncestorNodeType.ReadOnlyPackage), Comparable> {
+
+ override fun calculateChildren(): MutableList> {
+
+ if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
+ return super.calculateChildren()
+ }
+
+ // Show children if there's anything interesting to show. If it's just package.json or .icon.png, or their
+ // meta files, pretend there's no children. We'll show them when show hidden items is enabled
+ val children = super.calculateChildren()
+ if (children.all { it.name?.startsWith("package.json") == true
+ || it.name?.startsWith(".icon.png") == true
+ || it.name?.startsWith("package.ModuleCompilationTrigger") == true }) {
+ return mutableListOf()
+ }
+ return super.calculateChildren()
+ }
+
+ override fun createNode(virtualFile: VirtualFile, nestedFiles: List>): FileSystemNodeBase {
+ return UnityExplorerFileSystemNode(myProject, virtualFile, nestedFiles, descendentOf)
+ }
+
+ override fun canNavigateToSource(): Boolean {
+ if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
+ return super.canNavigateToSource()
+ }
+ return true
+ }
+
+ override fun navigate(requestFocus: Boolean) {
+ if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
+ return super.navigate(requestFocus)
+ }
+
+ val packageJson = virtualFile.findChild("package.json")
+ if (packageJson != null) {
+ OpenFileDescriptor(myProject, packageJson).navigate(requestFocus)
+ }
+ }
+
+ override fun getName() = packageData.displayName
+
+ override fun update(presentation: PresentationData) {
+ presentation.addText(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
+ presentation.setIcon(UnityIcons.Explorer.BuiltInPackage)
+ if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
+ presentation.addNonIndexedMark(myProject, virtualFile)
+ }
+
+ val tooltip = getPackageTooltip(name, packageData)
+ if (tooltip != name) {
+ presentation.tooltip = tooltip
+ }
+ }
+
+ override fun compareTo(other: AbstractTreeNode<*>): Int {
+ // Compare by display name, rather than the default file name
+ return String.CASE_INSENSITIVE_ORDER.compare(name, other.name)
+ }
+}
+
+// Note that this might get a PackageData with source == PackageSource.BuiltIn
+class UnknownPackageNode(project: Project, private val packageData: PackageData)
+ : AbstractTreeNode(project, packageData) {
+
+ init {
+ icon = when (packageData.source) {
+ PackageSource.BuiltIn -> UnityIcons.Explorer.BuiltInPackage
+ else -> UnityIcons.Explorer.UnknownPackage
+ }
+ }
+
+ override fun getName() = packageData.displayName
+ override fun getChildren(): MutableCollection> = arrayListOf()
+ override fun isAlwaysLeaf() = true
+
+ override fun update(presentation: PresentationData) {
+ presentation.addText(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
+ presentation.setIcon(icon)
+
+ // Description can be an error message
+ if (packageData.description?.isNotEmpty() == true) {
+ presentation.tooltip = formatDescription(packageData.description)
+ }
+ }
+}
+
+private fun getPackageTooltip(displayName: String, packageData: PackageData): String {
+ var tooltip = displayName
+ if (packageData.version.isNotEmpty()) {
+ tooltip += " ${packageData.version}"
+ }
+ if (displayName != packageData.id) {
+ tooltip += " ${packageData.id}"
+ }
+ if (packageData.description?.isNotEmpty() == true) {
+ tooltip += "
" + formatDescription(packageData.description)
+ }
+ return tooltip
+}
+
+private fun formatDescription(description: String): String {
+ val text = description.replace("\n", " ").let {
+ StringUtil.shortenTextWithEllipsis(it, 600, 0, true)
+ }
+
+ // Very crude. This should really be measured by font + pixels, not characters.
+ // Hopefully we can replace all of this with QuickDoc though, which has smarter wrapping.
+ val shouldWrap = StringUtil.splitByLines(text).any { it.length > 50 }
+ return if (shouldWrap) {
+ "
$text
"
+ }
+ else {
+ text
+ }
+}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/PackagesRoot.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/PackagesRoot.kt
deleted file mode 100644
index 1b0099c7d1..0000000000
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/PackagesRoot.kt
+++ /dev/null
@@ -1,420 +0,0 @@
-package com.jetbrains.rider.plugins.unity.explorer
-
-import com.intellij.ide.projectView.PresentationData
-import com.intellij.ide.util.treeView.AbstractTreeNode
-import com.intellij.openapi.fileEditor.OpenFileDescriptor
-import com.intellij.openapi.project.Project
-import com.intellij.openapi.util.text.StringUtil
-import com.intellij.openapi.vfs.VfsUtil
-import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.ui.SimpleTextAttributes
-import com.jetbrains.rider.plugins.unity.packageManager.PackageData
-import com.jetbrains.rider.plugins.unity.packageManager.PackageManager
-import com.jetbrains.rider.plugins.unity.packageManager.PackageSource
-import com.jetbrains.rider.plugins.unity.util.UnityInstallationFinder
-import com.jetbrains.rider.projectView.views.*
-import com.jetbrains.rider.projectView.views.solutionExplorer.SolutionExplorerViewPane
-import icons.UnityIcons
-
-// Packages are included in a project by listing in the "dependencies" node of Packages/manifest.json. Packages can
-// contain assets, such as source, resources, and .dlls. They can also include .asmdef files
-// a) Built-in (modules). These are pseudo-package that ship built in to Unity. They can be resolved by manifests in the
-// application directory. Modules are cached in an application specific folder, so can't be found without knowing the
-// path to the currently running instance of Unity. Path is: Editor/Data/Resources/PackageManager/BuiltInPackages for
-// Windows and Unity.app/Contents/Resources/PackageManager/BuiltInPackages on OSX
-// b) Referenced. These are cached in a per-user and per-registry location, and are read only. Any .asmdef files in the
-// package will be used to compile an assembly, saved to Library/ScriptAssemblies and added as a binary reference to
-// the project. A referenced package can be copied into the Packages folder to convert it into a writable embedded
-// package. Cache folder is %LOCALAPPDATA%\Unity\cache\packages on Windows and ~/Library/Unity/cache/packages on OSX
-// c) Embedded. This is a package that lives inside the Packages folder, and is read-write. Any .asmdef files are used
-// to create C# projects and added to the generated solution.
-// d) Local. These are read-write packages that live outside of the Packages folder. Any .asmdef files are used to
-// generate C# projects. The version in Packages/manifest.json begins with `file:` and is a path to the package,
-// either relative to the project root, or fully qualified
-// e) Git. Currently undocumented. Unity will check out a git repo to a cache folder and treat it as a read-only package
-// f) Excluded. A package can have a version of "excluded" in the manifest.json. It is simply ignored
-//
-// If there is a Packages/manifest.json file, show the Packages node in the Unity Explorer. The Packages node will show
-// all editable packages at the root, with a child node for read only packages. It will show all files and folders under
-// the Packages folder, with embedded packages being highlighted and sorted to the top. Local packages will also be
-// listed here. All other packages will be listed under "Read only", with source packages listed at the top as folders,
-// followed by modules and other/unresolved packages
-//
-// Potential actions:
-// a) If a folder has a package.json, but isn't listed in manifest.json, highlight with an error, and offer a right
-// click action to add to manifest.json
-// b) Right click on a referenced package to convert to embedded - simply copy into the project's Packages folder
-
-class PackagesRoot(project: Project, private val packageManager: PackageManager)
- : UnityExplorerNode(project, packageManager.packagesFolder, listOf(), AncestorNodeType.FileSystem) {
-
- override fun update(presentation: PresentationData) {
- if (!virtualFile.isValid) return
- presentation.presentableText = "Packages"
- presentation.setIcon(UnityIcons.Explorer.PackagesRoot)
- }
-
- override fun isAlwaysExpand() = true
-
- override fun calculateChildren(): MutableList> {
- val children = super.calculateChildren()
-
- // We want the children to be file system folders and editable packages, which means embedded packages and local
- // packages. We've already added the embedded packages by including file system folders
- packageManager.localPackages.forEach { addPackage(children, it) }
- packageManager.unknownPackages.forEach { addPackage(children, it) }
-
- if (packageManager.immutablePackages.any())
- children.add(0, ReadOnlyPackagesRoot(project!!, packageManager))
-
- return children
- }
-
- override fun createNode(virtualFile: VirtualFile, nestedFiles: List>): FileSystemNodeBase {
- if (virtualFile.isDirectory) {
- packageManager.getPackageData(virtualFile)?.let {
- val embeddedPackageData = PackageData(it.name, virtualFile, it.details, PackageSource.Embedded)
- return PackageNode(project!!, packageManager, virtualFile, embeddedPackageData)
- }
- }
- return super.createNode(virtualFile, nestedFiles)
- }
-
- // Required for "Locate in Solution Explorer" to work. If we return false, the solution view visitor stops walking.
- // True is effectively "maybe"
- override fun contains(file: VirtualFile) = true
-
- private fun addPackage(children: MutableList>, thePackage: PackageData) {
- if (thePackage.packageFolder != null) {
- children.add(PackageNode(project!!, packageManager, thePackage.packageFolder, thePackage))
- }
- else {
- children.add(UnknownPackageNode(project!!, thePackage))
- }
- }
-}
-
-class PackageNode(project: Project, private val packageManager: PackageManager, packageFolder: VirtualFile, private val packageData: PackageData)
- : UnityExplorerNode(project, packageFolder, listOf(), AncestorNodeType.fromPackageData(packageData)), Comparable> {
-
- init {
- icon = when (packageData.source) {
- PackageSource.Registry -> UnityIcons.Explorer.ReferencedPackage
- PackageSource.Embedded -> UnityIcons.Explorer.EmbeddedPackage
- PackageSource.Local -> UnityIcons.Explorer.LocalPackage
- PackageSource.LocalTarball -> UnityIcons.Explorer.LocalTarballPackage
- PackageSource.BuiltIn -> UnityIcons.Explorer.BuiltInPackage
- PackageSource.Git -> UnityIcons.Explorer.GitPackage
- PackageSource.Unknown -> UnityIcons.Explorer.UnknownPackage
- }
- }
-
- override fun getName() = packageData.details.displayName
-
- override fun update(presentation: PresentationData) {
- presentation.addText(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
- presentation.setIcon(icon)
- presentation.addNonIndexedMark(myProject, virtualFile)
-
- // Note that this might also set the tooltip if we have too many projects underneath
- if (UnityExplorer.getInstance(myProject).showProjectNames)
- addProjects(presentation)
-
- val existingTooltip = presentation.tooltip ?: ""
-
- var tooltip = "" + getPackageTooltip(name, packageData)
- tooltip += when (packageData.source) {
- PackageSource.Embedded -> if (virtualFile.name != name) "
Tarball location: ${packageData.tarballLocation}"
- PackageSource.Git -> {
- var text = "
Git URL: ${packageData.gitDetails?.url}"
- if (!packageData.gitDetails?.hash.isNullOrEmpty()) {
- text += " Hash: ${packageData.gitDetails?.hash}"
- }
- if (!packageData.gitDetails?.revision.isNullOrEmpty()) {
- text += " Revision: ${packageData.gitDetails?.revision}"
- }
- text
- }
- else -> ""
- }
- if (existingTooltip.isNotEmpty()) {
- tooltip += "
$existingTooltip"
- }
- tooltip += ""
- presentation.tooltip = tooltip
- }
-
- override fun calculateChildren(): MutableList> {
- val children = super.calculateChildren()
-
- if (packageData.details.dependencies.isNotEmpty()) {
- children.add(0, DependenciesRoot(project!!, packageManager, packageData))
- }
-
- return children
- }
-
- override fun compareTo(other: AbstractTreeNode<*>): Int {
- // Compare by name, rather than ID
- return String.CASE_INSENSITIVE_ORDER.compare(name, other.name)
- }
-}
-
-class DependenciesRoot(project: Project, private val packageManager: PackageManager, private val packageData: PackageData)
- : AbstractTreeNode(project, packageData) {
-
- override fun update(presentation: PresentationData) {
- presentation.presentableText = "Dependencies"
- presentation.setIcon(UnityIcons.Explorer.DependenciesRoot)
- }
-
- override fun getChildren(): MutableCollection> {
- val children = mutableListOf>()
- for ((name, version) in packageData.details.dependencies) {
- children.add(DependencyItemNode(project!!, packageManager, name, version))
- }
- return children
- }
-}
-
-class DependencyItemNode(project: Project, private val packageManager: PackageManager, private val packageName: String, version: String)
- : AbstractTreeNode(project, "$packageName@$version") {
-
- init {
- myName = "$packageName@$version"
- }
-
- override fun getChildren(): MutableCollection> = arrayListOf()
- override fun isAlwaysLeaf() = true
-
- override fun update(presentation: PresentationData) {
- presentation.presentableText = name
- presentation.setIcon(UnityIcons.Explorer.PackageDependency)
- }
-
- override fun canNavigate(): Boolean {
- return packageManager.getPackageData(packageName) != null
- }
-
- override fun navigate(requestFocus: Boolean) {
- val packageData = packageManager.getPackageData(packageName)
- if (packageData?.packageFolder == null) return
- project!!.navigateToSolutionView(packageData.packageFolder, requestFocus)
- }
-}
-
-abstract class CompositeFolderRoot(project: Project, key: Any)
- : SolutionViewNode(project, key) {
-
- private val packageFolders = mutableSetOf()
-
- protected fun addPackageFolder(packageFolder: VirtualFile) {
- packageFolders.add(packageFolder)
- }
-
- // Note that this requires the children to have been expanded first. The SolutionViewVisitor will ensure this happens
- override fun contains(file: VirtualFile): Boolean {
- if (packageFolders.contains(file)) return true
- for (packageFolder in packageFolders) {
- if (VfsUtil.isAncestor(packageFolder, file, false)) return true
- }
- return false
- }
-}
-
-class ReadOnlyPackagesRoot(project: Project, private val packageManager: PackageManager)
- : CompositeFolderRoot(project, key) {
-
- companion object {
- val key = Any()
- }
-
- override fun update(presentation: PresentationData) {
- presentation.presentableText = "Read only"
- presentation.setIcon(UnityIcons.Explorer.ReadOnlyPackagesRoot)
- }
-
- override fun calculateChildren(): MutableList> {
- val children = mutableListOf>()
-
- if (packageManager.hasBuiltInPackages) {
- children.add(BuiltinPackagesRoot(project!!, packageManager))
-
- // Add the builtin packages root folder to the list of folders we know are under this node. This lets us
- // correctly handle `contains()` for built in packages, which in turn means we can navigate to a built in
- // package by folder (e.g. by double clicking a dependency node)
- try {
- UnityInstallationFinder.getInstance(myProject).getBuiltInPackagesRoot()?.let {
- VfsUtil.findFile(it, true)?.let { vfile ->
- addPackageFolder(vfile)
- }
- }
- } catch (throwable: Throwable) {
- // Do nothing. It just means navigation to built in packages from dependency nodes won't work
- }
- }
-
- for (packageData in packageManager.immutablePackages) {
- if (packageData.source == PackageSource.BuiltIn) continue
-
- if (packageData.packageFolder == null) {
- children.add(UnknownPackageNode(project!!, packageData))
- }
- else {
- children.add(PackageNode(project!!, packageManager, packageData.packageFolder, packageData))
- addPackageFolder(packageData.packageFolder)
- }
- }
- return children
- }
-}
-
-class BuiltinPackagesRoot(project: Project, private val packageManager: PackageManager)
- : CompositeFolderRoot(project, key) {
-
- companion object {
- val key = Any()
- }
-
- override fun update(presentation: PresentationData) {
- presentation.presentableText = "Modules"
- presentation.setIcon(UnityIcons.Explorer.BuiltInPackagesRoot)
- }
-
- override fun calculateChildren(): MutableList> {
- val children = mutableListOf>()
- for (packageData in packageManager.immutablePackages) {
- if (packageData.source != PackageSource.BuiltIn) continue
-
- if (packageData.packageFolder == null) {
- children.add(UnknownPackageNode(project!!, packageData))
- }
- else {
- children.add(BuiltinPackageNode(project!!, packageData))
- addPackageFolder(packageData.packageFolder)
- }
- }
- return children
- }
-}
-
-// Represents a module, built in part of the Unity product. We show it as a single node with no children, unless we have
-// "show hidden items" enabled, in which case we show the package folder, including the package.json.
-// Note that a module can have dependencies. Perhaps we want to always show this as a folder, including the Dependencies
-// node?
-class BuiltinPackageNode(project: Project, private val packageData: PackageData)
- : UnityExplorerNode(project, packageData.packageFolder!!, listOf(), AncestorNodeType.ReadOnlyPackage) {
-
- override fun calculateChildren(): MutableList> {
-
- if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
- return super.calculateChildren()
- }
-
- // Show children if there's anything interesting to show. If it's just package.json or .icon.png, or their
- // meta files, pretend there's no children. We'll show them when show hidden items is enabled
- val children = super.calculateChildren()
- if (children.all { it.name?.startsWith("package.json") == true
- || it.name?.startsWith(".icon.png") == true
- || it.name?.startsWith("package.ModuleCompilationTrigger") == true }) {
- return mutableListOf()
- }
- return super.calculateChildren()
- }
-
- override fun createNode(virtualFile: VirtualFile, nestedFiles: List>): FileSystemNodeBase {
- return UnityExplorerNode(project!!, virtualFile, nestedFiles, descendentOf)
- }
-
- override fun canNavigateToSource(): Boolean {
- if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
- return super.canNavigateToSource()
- }
- return true
- }
-
- override fun navigate(requestFocus: Boolean) {
- if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
- return super.navigate(requestFocus)
- }
-
- val packageJson = virtualFile.findChild("package.json")
- if (packageJson != null) {
- OpenFileDescriptor(project!!, packageJson).navigate(requestFocus)
- }
- }
-
- override fun getName() = packageData.details.displayName
-
- override fun update(presentation: PresentationData) {
- presentation.addText(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
- presentation.setIcon(UnityIcons.Explorer.BuiltInPackage)
- if (SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
- presentation.addNonIndexedMark(myProject, virtualFile)
- }
-
- val tooltip = getPackageTooltip(name, packageData)
- if (tooltip != name) {
- presentation.tooltip = tooltip
- }
- }
-}
-
-// Note that this might get a PackageData with source == PackageSource.Unknown
-class UnknownPackageNode(project: Project, private val packageData: PackageData)
- : AbstractTreeNode(project, packageData) {
-
- init {
- icon = when (packageData.source) {
- PackageSource.BuiltIn -> UnityIcons.Explorer.BuiltInPackage
- else -> UnityIcons.Explorer.UnknownPackage
- }
- }
-
- override fun getName() = packageData.details.displayName
- override fun getChildren(): MutableCollection> = arrayListOf()
- override fun isAlwaysLeaf() = true
-
- override fun update(presentation: PresentationData) {
- presentation.addText(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
- presentation.setIcon(icon)
- if (packageData.details.description.isNotEmpty()) {
- presentation.tooltip = formatDescription(packageData)
- }
- }
-}
-
-private fun getPackageTooltip(name: String, packageData: PackageData): String {
- var tooltip = name
- if (packageData.details.version.isNotEmpty()) {
- tooltip += " ${packageData.details.version}"
- }
- if (name != packageData.details.canonicalName) {
- tooltip += " ${packageData.details.canonicalName}"
- }
- if (packageData.details.author.isNotEmpty()) {
- tooltip += " ${packageData.details.author}"
- }
- if (packageData.details.description.isNotEmpty()) {
- tooltip += "
" + formatDescription(packageData)
- }
- return tooltip
-}
-
-private fun formatDescription(packageData: PackageData): String {
- val description = packageData.details.description.replace("\n", " ").let {
- StringUtil.shortenTextWithEllipsis(it, 600, 0, true)
- }
-
- // Very crude. This should really be measured by font + pixels, not characters.
- // Hopefully we can replace all of this with QuickDoc though, which has smarter wrapping.
- val shouldWrap = StringUtil.splitByLines(packageData.details.description).any { it.length > 50 }
- return if (shouldWrap) {
- "
$description
"
- }
- else {
- description
- }
-}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/ReferenceNodes.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/ReferenceNodes.kt
new file mode 100644
index 0000000000..8dddf60dfd
--- /dev/null
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/ReferenceNodes.kt
@@ -0,0 +1,78 @@
+package com.jetbrains.rider.plugins.unity.explorer
+
+import com.intellij.ide.projectView.PresentationData
+import com.intellij.ide.util.treeView.AbstractTreeNode
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vcs.FileStatus
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.workspaceModel.ide.impl.virtualFile
+import com.jetbrains.rd.util.getOrCreate
+import com.jetbrains.rider.projectView.workspace.*
+import icons.UnityIcons
+
+class ReferenceRootNode(project: Project) : AbstractTreeNode(project, key) {
+
+ companion object {
+ val key = Any()
+ }
+
+ override fun update(presentation: PresentationData) {
+ presentation.presentableText = "References"
+ presentation.setIcon(UnityIcons.Explorer.ReferencesRoot)
+ }
+
+ override fun getChildren(): MutableCollection> {
+ val referenceNames = hashMapOf()
+ val visitor = object : ProjectModelEntityVisitor() {
+ override fun visitReference(entity: ProjectModelEntity): Result {
+ if (entity.isAssemblyReference()) {
+ val virtualFile = entity.url?.virtualFile
+ if (virtualFile != null) {
+ val item = referenceNames.getOrCreate(entity.descriptor.location.toString()) {
+ ReferenceItemNode(myProject, entity.descriptor.name, virtualFile, arrayListOf())
+ }
+ item.entityReferences.add(entity.toReference())
+ }
+ }
+ return Result.Stop
+ }
+ }
+ visitor.visit(myProject)
+
+ val children = arrayListOf>()
+ for ((_, item) in referenceNames) {
+ children.add(item)
+ }
+ return children
+ }
+}
+
+class ReferenceItemNode(
+ project: Project,
+ private val referenceName: String,
+ virtualFile: VirtualFile,
+ override val entityReferences: ArrayList
+) : UnityExplorerFileSystemNode(project, virtualFile, emptyList(), AncestorNodeType.References) {
+
+ override fun isAlwaysLeaf() = true
+
+ override fun update(presentation: PresentationData) {
+ presentation.presentableText = referenceName
+ presentation.setIcon(UnityIcons.Explorer.Reference)
+ }
+
+ override fun navigate(requestFocus: Boolean) {
+ // the same VirtualFile may be added as a file inside Assets folder, so simple click on the reference would jump to that file
+ }
+
+ override val entities: List
+ get() = entityReferences.mapNotNull { it.getEntity(myProject) }
+ override val entity: ProjectModelEntity?
+ get() = entities.firstOrNull()
+ override val entityReference: ProjectModelEntityReference?
+ get() = entityReferences.firstOrNull()
+
+ // Don't show references with weird file statuses. They are files, and some will be in ignored folders
+ // (e.g. Library/PackageCache)
+ override fun getFileStatus(): FileStatus = FileStatus.NOT_CHANGED
+}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorer.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorer.kt
index c5ce08d3b8..a9e6b4a6ef 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorer.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorer.kt
@@ -8,7 +8,6 @@ import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.JDOMExternalizerUtil
import com.jetbrains.rider.isUnityProject
-import com.jetbrains.rider.plugins.unity.packageManager.PackageManager
import com.jetbrains.rider.projectView.views.SolutionViewPaneBase
import com.jetbrains.rider.projectView.views.actions.ConfigureScratchesAction
import com.jetbrains.rider.projectView.views.actions.SolutionViewToggleAction
@@ -17,7 +16,7 @@ import com.jetbrains.rider.projectView.views.solutionExplorer.SolutionExplorerVi
import icons.UnityIcons
import org.jdom.Element
-class UnityExplorer(project: Project) : SolutionViewPaneBase(project, UnityExplorerRootNode(project, PackageManager.getInstance(project))) {
+class UnityExplorer(project: Project) : SolutionViewPaneBase(project, createRootNode(project)) {
companion object {
const val ID = "UnityExplorer"
@@ -35,6 +34,10 @@ class UnityExplorer(project: Project) : SolutionViewPaneBase(project, UnityExplo
fun tryGetInstance(project: Project): UnityExplorer? {
return ProjectView.getInstance(project).getProjectViewPaneById(ID) as? UnityExplorer
}
+
+ private fun createRootNode(project: Project): UnityExplorerRootNode {
+ return UnityExplorerRootNode(project)
+ }
}
var showTildeFolders = true
@@ -53,7 +56,7 @@ class UnityExplorer(project: Project) : SolutionViewPaneBase(project, UnityExplo
val root = tree.model.root
val count = tree.model.getChildCount(root)
for (i in 0..count) {
- if (tree.model.getChild(root, i) is PackagesRoot) {
+ if (tree.model.getChild(root, i) is PackagesRootNode) {
return true
}
}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerNode.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerFileSystemNode.kt
similarity index 78%
rename from rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerNode.kt
rename to rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerFileSystemNode.kt
index cb2c0df46e..c6bb3e5a79 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerNode.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerFileSystemNode.kt
@@ -1,7 +1,7 @@
package com.jetbrains.rider.plugins.unity.explorer
import com.intellij.ide.projectView.PresentationData
-import com.intellij.ide.util.treeView.AbstractTreeNode
+import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vcs.FileStatus
@@ -10,81 +10,27 @@ import com.intellij.ui.SimpleTextAttributes
import com.intellij.workspaceModel.ide.WorkspaceModel
import com.intellij.workspaceModel.ide.impl.virtualFile
import com.jetbrains.rider.plugins.unity.packageManager.PackageData
-import com.jetbrains.rider.plugins.unity.packageManager.PackageManager
-import com.jetbrains.rider.projectDir
import com.jetbrains.rider.projectView.calculateFileSystemIcon
-import com.jetbrains.rider.projectView.ideaInterop.RiderScratchProjectViewPane
-import com.jetbrains.rider.projectView.nodes.*
import com.jetbrains.rider.projectView.views.FileSystemNodeBase
import com.jetbrains.rider.projectView.views.NestingNode
-import com.jetbrains.rider.projectView.views.SolutionViewRootNodeBase
-import com.jetbrains.rider.projectView.views.actions.ConfigureScratchesAction
+import com.jetbrains.rider.projectView.views.SolutionViewPaneBase
import com.jetbrains.rider.projectView.views.fileSystemExplorer.FileSystemExplorerCustomization
import com.jetbrains.rider.projectView.views.solutionExplorer.SolutionExplorerViewPane
-import com.jetbrains.rider.projectView.workspace.*
+import com.jetbrains.rider.projectView.workspace.ProjectModelEntity
+import com.jetbrains.rider.projectView.workspace.containingProjectEntity
+import com.jetbrains.rider.projectView.workspace.getProjectModelEntities
import com.jetbrains.rider.projectView.workspace.impl.WorkspaceEntityErrorsSupport
+import com.jetbrains.rider.projectView.workspace.isProject
import icons.UnityIcons
import java.awt.Color
import javax.swing.Icon
-class UnityExplorerRootNode(project: Project, private val packageManager: PackageManager)
- : SolutionViewRootNodeBase(project) {
-
- override fun calculateChildren(): MutableList> {
- val assetsFolder = myProject.projectDir.findChild("Assets")!!
- val assetsNode = AssetsRoot(myProject, assetsFolder)
-
- val nodes = mutableListOf>(assetsNode)
-
- if (packageManager.hasPackages) {
- nodes.add(PackagesRoot(myProject, packageManager))
- }
-
- if (ConfigureScratchesAction.showScratchesInExplorer(myProject)) {
- nodes.add(RiderScratchProjectViewPane.createNode(myProject))
- }
-
- return nodes
- }
-
- override fun createComparator(): Comparator> {
- val comparator = super.createComparator()
- return Comparator { node1, node2 ->
- val sortKey1 = getSortKey(node1)
- val sortKey2 = getSortKey(node2)
-
- if (sortKey1 != sortKey2) {
- return@Comparator sortKey1.compareTo(sortKey2)
- }
-
- comparator.compare(node1, node2)
- }
- }
-
- private fun getSortKey(node: AbstractTreeNode<*>): Int {
- // Nodes of the same type should be sorted as the same. Different types should be in this order (although some
- // are in different levels of the hierarchy)
- return when (node) {
- is AssetsRoot -> 1
- is PackagesRoot -> 2
- is ReferenceRoot -> 3
- is ReadOnlyPackagesRoot -> 4
- is BuiltinPackagesRoot -> 5
- is PackageNode -> 6
- is DependenciesRoot -> 7
- is DependencyItemNode -> 8
- is BuiltinPackageNode -> 9
- is UnknownPackageNode -> 100
- is UnityExplorerNode -> 1000
- else -> 10000
- }
- }
-}
-
enum class AncestorNodeType {
Assets,
UserEditablePackage,
ReadOnlyPackage,
+ IgnoredFolder,
+ References,
FileSystem; // A folder in Packages that isn't a package. Gets no special treatment
companion object {
@@ -94,10 +40,11 @@ enum class AncestorNodeType {
}
}
-open class UnityExplorerNode(project: Project,
- virtualFile: VirtualFile,
- nestedFiles: List>,
- protected val descendentOf: AncestorNodeType)
+@Suppress("UnstableApiUsage")
+open class UnityExplorerFileSystemNode(project: Project,
+ virtualFile: VirtualFile,
+ nestedFiles: List>,
+ protected val descendentOf: AncestorNodeType)
: FileSystemNodeBase(project, virtualFile, nestedFiles) {
override val entities: List
@@ -114,6 +61,7 @@ open class UnityExplorerNode(project: Project,
override fun update(presentation: PresentationData) {
if (!virtualFile.isValid) return
+
presentation.addText(name, SimpleTextAttributes.REGULAR_ATTRIBUTES)
presentation.setIcon(calculateIcon())
@@ -121,6 +69,18 @@ open class UnityExplorerNode(project: Project,
it.updateNode(presentation, file, this)
}
+ // Mark ignored file types. This is mainly so we can highlight that hidden folders are completely ignored. We
+ // have lots of non-indexed files in Assets and Packages, but they still appear in Find in Files when we check
+ // 'non-solution items'. Ignored file types are completely ignored, and don't show up at all. The default `*~`
+ // pattern matches Unity's hidden folder pattern, but can be used for e.g. `Samples~` and `Documentation~`.
+ // Make it clear that the folder is ignored/excluded/not indexed
+ val ignored = FileTypeManager.getInstance().isFileIgnored(virtualFile)
+ if (ignored || descendentOf == AncestorNodeType.IgnoredFolder) {
+ // TODO: Consider wording
+ // We can usually still search for a file that is not indexed. An ignored file is completely excluded
+ presentation.addText(" ${SolutionViewPaneBase.TextSeparator} ignored ${SolutionViewPaneBase.TextSeparator} no index", SimpleTextAttributes.GRAYED_ATTRIBUTES)
+ }
+
// Add additional info for directories
val unityExplorer = UnityExplorer.getInstance(myProject)
if (virtualFile.isDirectory && unityExplorer.showProjectNames) {
@@ -128,14 +88,19 @@ open class UnityExplorerNode(project: Project,
}
// Add tooltip for non-imported folders (anything ending with tilde). Also, show the full name if we're hiding
- // the tilde suffix
+ // the tilde suffix.
if (isHiddenFolder(virtualFile)) {
- var tooltip = if (presentation.tooltip.isNullOrEmpty()) "" else " "
+ var tooltip = if (presentation.tooltip.isNullOrEmpty()) "" else presentation.tooltip + " "
if (!SolutionExplorerViewPane.getInstance(myProject).myShowAllFiles) {
tooltip += virtualFile.name + " "
}
presentation.tooltip = tooltip + "This folder is not imported into the asset database"
}
+
+ if (ignored) {
+ val tooltip = if (presentation.tooltip.isNullOrEmpty()) "" else presentation.tooltip + " "
+ presentation.tooltip = tooltip + "This folder matches an Ignored File and Folders pattern"
+ }
}
override fun getName(): String {
@@ -147,9 +112,14 @@ open class UnityExplorerNode(project: Project,
return super.getName()
}
+ // Hidden from Unity's asset database
private fun isHiddenFolder(file: VirtualFile)
= descendentOf != AncestorNodeType.FileSystem && file.isDirectory && file.name.endsWith("~")
+ // Ignored by IDE
+ private fun isIgnoredFolder(file: VirtualFile)
+ = file.isDirectory && FileTypeManager.getInstance().isFileIgnored(virtualFile)
+
protected fun addProjects(presentation: PresentationData) {
val projectNames = entities // One node for each project that this directory is part of
.mapNotNull { containingProjectNode(it) }
@@ -225,8 +195,9 @@ open class UnityExplorerNode(project: Project,
private fun forEachAncestor(root: FileSystemNodeBase?, action: FileSystemNodeBase.() -> Boolean): FileSystemNodeBase? {
var node: FileSystemNodeBase? = root
while (node != null) {
- if (node.action())
+ if (node.action()) {
return node
+ }
node = node.parent as? FileSystemNodeBase
}
return null
@@ -259,6 +230,10 @@ open class UnityExplorerNode(project: Project,
}
private fun calculateIcon(): Icon? {
+ if (isIgnoredFolder(virtualFile) || (virtualFile.isDirectory && descendentOf == AncestorNodeType.IgnoredFolder)) {
+ return UnityIcons.Explorer.UnloadedFolder
+ }
+
if (descendentOf != AncestorNodeType.FileSystem) {
// Under Packages, the only special folder is "Resources". As per Maxime @ Unity:
// "Resources folders work the same in packages as under Assets, but that's mostly it. Editor folders have no
@@ -272,7 +247,7 @@ open class UnityExplorerNode(project: Project,
return UnityIcons.Explorer.EditorFolder
}
- if (parent is AssetsRoot) {
+ if (parent is AssetsRootNode) {
val rootSpecialIcon = when (name.toLowerCase()) {
"editor default resources" -> UnityIcons.Explorer.EditorDefaultResourcesFolder
"gizmos" -> UnityIcons.Explorer.GizmosFolder
@@ -303,7 +278,12 @@ open class UnityExplorerNode(project: Project,
}
override fun createNode(virtualFile: VirtualFile, nestedFiles: List>): FileSystemNodeBase {
- return UnityExplorerNode(project!!, virtualFile, nestedFiles, descendentOf)
+ val desc = if (isIgnoredFolder(virtualFile) || (!virtualFile.isDirectory && isIgnoredFolder(virtualFile.parent))) {
+ AncestorNodeType.IgnoredFolder
+ } else {
+ descendentOf
+ }
+ return UnityExplorerFileSystemNode(myProject, virtualFile, nestedFiles, desc)
}
override fun getVirtualFileChildren(): List {
@@ -327,15 +307,19 @@ open class UnityExplorerNode(project: Project,
}
/* Files and folders ending with '~' are ignored by the asset importer. Files with '~' are usually backup files,
- but Unity uses folders that end with '~' as a way of distributing files that are not to be imported. This is
- usually `Documentation~` inside packages (https://docs.unity3d.com/Manual/cus-layout.html), but it can also
- be used for distributing code, too. This code will not be treated as assets by Unity, but will still be added
- to the generated .csproj files to allow for use as e.g. command line tools
+ so should be hidden. Unity uses folders that end with '~' as a way of distributing files that are not to be
+ imported. This is usually `Documentation~` inside packages (https://docs.unity3d.com/Manual/cus-layout.html),
+ but it can also be used for distributing code, too (e.g. `Samples~`). This code will not be treated as assets
+ by Unity, but will still be added to the generated .csproj files to allow for use as e.g. command line tools
*/
if (isHiddenFolder(file)) {
return UnityExplorer.getInstance(myProject).showTildeFolders
}
+ if (!file.isDirectory && file.name.endsWith("~")) {
+ return false
+ }
+
return true
}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerProjectModelViewUpdater.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerProjectModelViewUpdater.kt
index 0072869cbb..2fa40624d2 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerProjectModelViewUpdater.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerProjectModelViewUpdater.kt
@@ -82,7 +82,7 @@ class UnityExplorerProjectModelViewUpdater(project: Project) : ProjectModelViewU
// Note that refresh will never refresh the root node, only its children
pane?.refresh(object : SolutionViewVisitor() {
override fun visit(node: AbstractTreeNode<*>): TreeVisitor.Action {
- if (node is PackagesRoot) {
+ if (node is PackagesRootNode) {
return TreeVisitor.Action.INTERRUPT
}
return TreeVisitor.Action.SKIP_CHILDREN
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerRootNode.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerRootNode.kt
new file mode 100644
index 0000000000..a836e993ba
--- /dev/null
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityExplorerRootNode.kt
@@ -0,0 +1,64 @@
+package com.jetbrains.rider.plugins.unity.explorer
+
+import com.intellij.ide.util.treeView.AbstractTreeNode
+import com.intellij.openapi.project.Project
+import com.jetbrains.rider.projectDir
+import com.jetbrains.rider.projectView.ideaInterop.RiderScratchProjectViewPane
+import com.jetbrains.rider.projectView.views.SolutionViewRootNodeBase
+import com.jetbrains.rider.projectView.views.actions.ConfigureScratchesAction
+
+class UnityExplorerRootNode(project: Project)
+ : SolutionViewRootNodeBase(project) {
+
+ override fun calculateChildren(): MutableList> {
+ val nodes = mutableListOf>()
+
+ val assetsFolder = myProject.projectDir.findChild("Assets")!!
+ nodes.add(AssetsRootNode(myProject, assetsFolder))
+
+ // Older Unity versions won't have a packages folder
+ val packagesFolder = myProject.projectDir.findChild("Packages")
+ if (packagesFolder?.exists() == true) {
+ nodes.add(PackagesRootNode(myProject, packagesFolder))
+ }
+
+ if (ConfigureScratchesAction.showScratchesInExplorer(myProject)) {
+ nodes.add(RiderScratchProjectViewPane.createNode(myProject))
+ }
+
+ return nodes
+ }
+
+ override fun createComparator(): Comparator> {
+ val comparator = super.createComparator()
+ return Comparator { node1, node2 ->
+ val sortKey1 = getSortKey(node1)
+ val sortKey2 = getSortKey(node2)
+
+ if (sortKey1 != sortKey2) {
+ return@Comparator sortKey1.compareTo(sortKey2)
+ }
+
+ comparator.compare(node1, node2)
+ }
+ }
+
+ private fun getSortKey(node: AbstractTreeNode<*>): Int {
+ // Nodes of the same type should be sorted as the same. Different types should be in this order (although some
+ // are in different levels of the hierarchy)
+ return when (node) {
+ is AssetsRootNode -> 1
+ is PackagesRootNode -> 2
+ is ReferenceRootNode -> 3
+ is ReadOnlyPackagesRootNode -> 4
+ is BuiltinPackagesRootNode -> 5
+ is PackageNode -> 6
+ is PackageDependenciesRoot -> 7
+ is PackageDependencyItemNode -> 8
+ is BuiltinPackageNode -> 9
+ is UnknownPackageNode -> 100
+ is UnityExplorerFileSystemNode -> 1000
+ else -> 10000
+ }
+ }
+}
\ No newline at end of file
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityProjectModelViewExtensions.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityProjectModelViewExtensions.kt
index e21714166b..998194fc2e 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityProjectModelViewExtensions.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/explorer/UnityProjectModelViewExtensions.kt
@@ -10,6 +10,7 @@ import com.jetbrains.rider.projectView.ProjectEntityView
import com.jetbrains.rider.projectView.ProjectModelViewExtensions
import com.jetbrains.rider.projectView.workspace.*
+@Suppress("UnstableApiUsage")
class UnityProjectModelViewExtensions(project: Project) : ProjectModelViewExtensions(project) {
// this is called for rename, we should filter .Player projects and return node itself
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/notifications/OpenUnityProjectAsFolderNotification.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/notifications/OpenUnityProjectAsFolderNotification.kt
index 41399a94ac..7e5f1627df 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/notifications/OpenUnityProjectAsFolderNotification.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/notifications/OpenUnityProjectAsFolderNotification.kt
@@ -28,6 +28,7 @@ import com.jetbrains.rider.projectView.SolutionManager
import com.jetbrains.rider.projectView.solution
import com.jetbrains.rider.projectView.solutionDescription
import com.jetbrains.rider.util.*
+import kotlinx.coroutines.CoroutineStart
import javax.swing.event.HyperlinkEvent
class OpenUnityProjectAsFolderNotification(project: Project) : ProtocolSubscribedProjectComponent(project) {
@@ -63,8 +64,12 @@ class OpenUnityProjectAsFolderNotification(project: Project) : ProtocolSubscribe
else if (solutionDescription is RdVirtualSolution) { // opened as folder
val adviceText = " Please close and reopen through the Unity editor, or by opening a .sln file."
val contentWoSolution =
- // todo: hasPackage is unreliable, when PackageManager is still in progress, revisit this after PackageManager is moved to backend
- if (UnityInstallationFinder.getInstance(project).requiresRiderPackage() && !PackageManager.getInstance(project).hasPackage("com.unity.ide.rider")){
+ // todo: hasPackage is unreliable, when PackageManager is still in progress
+ // Revisit this after PackageManager is moved to backend
+ // MTE: There is an inherent race condition here. Packages can be updated at any time, so we can't
+ // be sure that PackageManager is fully loaded at this time.
+ if (UnityInstallationFinder.getInstance(project).requiresRiderPackage()
+ && !PackageManager.getInstance(project).hasPackage("com.unity.ide.rider")) {
content
}
else if (solutionDescription.projectFilePaths.isEmpty()) {
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageData.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageData.kt
index c269a6d71e..c79f34ea1b 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageData.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageData.kt
@@ -1,6 +1,11 @@
package com.jetbrains.rider.plugins.unity.packageManager
+import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
+import com.jetbrains.rider.model.unity.frontendBackend.UnityPackage
+import com.jetbrains.rider.model.unity.frontendBackend.UnityPackageDependency
+import com.jetbrains.rider.model.unity.frontendBackend.UnityPackageSource
+import java.nio.file.Paths
enum class PackageSource {
Unknown,
@@ -14,73 +19,56 @@ enum class PackageSource {
fun isEditable(): Boolean {
return this == Embedded || this == Local
}
-}
-data class PackageData(val name: String, val packageFolder: VirtualFile?, val details: PackageDetails,
- val source: PackageSource, val gitDetails: GitDetails? = null, val tarballLocation: String? = null) {
- companion object {
- fun unknown(name: String, version: String, source: PackageSource = PackageSource.Unknown): PackageData {
- return PackageData(name, null, PackageDetails(name, "$name@$version", version,
- "Cannot resolve package '$name' with version '$version'", "", mapOf()), source)
- }
+ fun isReadOnly(): Boolean {
+ return !isEditable() && this != Unknown
}
}
-// Canonical name is the name from package.json, or the package's folder name if missing
-// Display name is the display name from package.json, falling back to package.json name and then folder name
-// For unresolved packages, name is the name from manifest.json and display name is name@version from manifest.json
-data class PackageDetails(val canonicalName: String, val displayName: String, val version: String,
- val description: String, val author: String, val dependencies: Map) {
+data class PackageData(val id: String,
+ val version: String,
+ val packageFolder: VirtualFile?,
+ val source: PackageSource,
+ val displayName: String,
+ val description: String?,
+ val dependencies: Map,
+ val tarballLocation: String?,
+ val gitUrl: String?,
+ val gitHash: String?,
+ val gitRevision: String?) {
companion object {
- fun fromPackageJson(packageFolder: VirtualFile, packageJson: PackageJson?): PackageDetails? {
- if (packageJson == null) return null
- val name = packageJson.name ?: packageFolder.name
- return PackageDetails(name, packageJson.displayName
- ?: name, packageJson.version ?: "",
- packageJson.description ?: "", getAuthor(packageJson.author), packageJson.dependencies ?: mapOf())
+ fun fromUnityPackage(unityPackage: UnityPackage): PackageData {
+ return PackageData(unityPackage.id,
+ unityPackage.version,
+ getPackagesFolder(unityPackage.packageFolderPath),
+ toPackageSource(unityPackage.source),
+ unityPackage.displayName,
+ unityPackage.description,
+ getDependencies(unityPackage.dependencies),
+ unityPackage.tarballLocation,
+ unityPackage.gitDetails?.url,
+ unityPackage.gitDetails?.hash,
+ unityPackage.gitDetails?.revision)
}
- private fun getAuthor(author: Any?): String {
- if (author == null)
- return ""
-
- if (author is String)
- return author
-
+ private fun getPackagesFolder(path: String?): VirtualFile? {
+ if (path == null) return null
+ return VfsUtil.findFile(Paths.get(path), true)
+ }
- if (author is Map<*, *>) {
- return author["name"] as String? ?: "";
+ private fun toPackageSource(unityPackageSource: UnityPackageSource): PackageSource {
+ return when (unityPackageSource) {
+ UnityPackageSource.Unknown -> PackageSource.Unknown
+ UnityPackageSource.BuiltIn -> PackageSource.BuiltIn
+ UnityPackageSource.Registry -> PackageSource.Registry
+ UnityPackageSource.Embedded -> PackageSource.Embedded
+ UnityPackageSource.Local -> PackageSource.Local
+ UnityPackageSource.LocalTarball -> PackageSource.LocalTarball
+ UnityPackageSource.Git -> PackageSource.Git
}
-
- return "";
}
+
+ private fun getDependencies(dependencies: Array) =
+ dependencies.associate { it.id to it.version }
}
}
-
-data class GitDetails(val url: String, val hash: String, val revision: String?)
-
-// Git lock details have moved to packages-lock.json in Unity 2019.4+
-class LockDetails(val hash: String?, val revision: String?)
-class ManifestJson(val dependencies: Map, val testables: Array?, val registry: String?, val lock: Map?)
-
-
-// Other properties are available: category, keywords, unity (supported version)
-data class PackageJson(val name: String?, val displayName: String?, val version: String?, val description: String?,
- val author: Any?, val dependencies: Map?)
-
-
-// packages-lock.json (note the 's', this isn't NPM's package-lock.json)
-// This was introduced in Unity 2019.4 and appears to be a full list of packages, dependencies and transitive
-// dependencies. It also contains the git hash for git based packages.
-// By observation:
-// * `source` can be `builtin`, `registry`, `embedded`, `git`. Likely also includes other members of PackageSource, such
-// as local and local tarball
-// * `version` is a semver value for `builtin`, `registry`, a `file:` url for `embedded` and a url for `git`
-// * `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 could 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
-class PackagesLockDependency(val version: String, val depth: Int?, val source: String?, val dependencies: Map, val url: String?, val hash: String?)
-class PackagesLockJson(val dependencies: Map)
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageManager.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageManager.kt
index 5a53100bbe..c9b6c35a1c 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageManager.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/packageManager/PackageManager.kt
@@ -1,39 +1,14 @@
package com.jetbrains.rider.plugins.unity.packageManager
-import com.google.gson.Gson
-import com.intellij.openapi.application.ModalityState
-import com.intellij.openapi.application.ReadAction
-import com.intellij.openapi.diagnostic.ControlFlowException
import com.intellij.openapi.diagnostic.Logger
-import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
-import com.intellij.openapi.vfs.AsyncFileListener
-import com.intellij.openapi.vfs.AsyncFileListener.ChangeApplier
-import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.vfs.VirtualFile
-import com.intellij.openapi.vfs.VirtualFileManager
-import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.util.EventDispatcher
-import com.intellij.util.concurrency.NonUrgentExecutor
-import com.intellij.util.io.inputStream
-import com.intellij.util.io.isDirectory
import com.intellij.util.pooledThreadSingleAlarm
+import com.jetbrains.rd.platform.util.application
import com.jetbrains.rd.platform.util.idea.getOrCreateUserData
-import com.jetbrains.rd.platform.util.lifetime
-import com.jetbrains.rider.debugger.util.isExistingFile
-import com.jetbrains.rider.model.unity.frontendBackend.frontendBackendModel
-import com.jetbrains.rider.plugins.unity.util.SemVer
-import com.jetbrains.rider.plugins.unity.util.UnityCachesFinder
-import com.jetbrains.rider.plugins.unity.util.UnityInstallationFinder
-import com.jetbrains.rider.plugins.unity.util.findFile
-import com.jetbrains.rider.projectDir
-import com.jetbrains.rider.projectView.solution
-import java.lang.Integer.min
-import java.nio.charset.Charset
-import java.nio.file.Path
-import java.nio.file.Paths
-import java.security.MessageDigest
+import com.jetbrains.rider.model.unity.frontendBackend.UnityPackage
import java.util.*
interface PackageManagerListener : EventListener {
@@ -41,6 +16,13 @@ interface PackageManagerListener : EventListener {
fun onPackagesUpdated()
}
+// There is chance of a race condition here. We don't know if the backend has started or completed the first read. We
+// also don't know if we're in the middle of an update. If someone calls `hasPackage`, how confident can they be that
+// a) we've got the initial list of packages and b) we've not just deleted the package because we're about to add a new
+// version?
+// (See OpenUnityProjectAFolderNotification)
+// Should we include an "updating" flag, and only send the notification once it's been reset? That doesn't help with
+// hasPackage - what if someone calls hasPackage and we're in the middle of updating?
class PackageManager(private val project: Project) {
companion object {
@@ -50,513 +32,53 @@ class PackageManager(private val project: Project) {
fun getInstance(project: Project) = project.getOrCreateUserData(KEY) { PackageManager(project) }
private const val MILLISECONDS_BEFORE_REFRESH = 1000
- private const val DEFAULT_REGISTRY_URL = "https://packages.unity.com"
}
- private val gson = Gson()
- private val listeners = EventDispatcher.create(PackageManagerListener::class.java)
- private val alarm = pooledThreadSingleAlarm(MILLISECONDS_BEFORE_REFRESH, project, ::refreshAndNotify)
-
- // The manifest of packages that ship with the editor. Used during package resolve to ensure that builtin packages
- // have a known minimum version
- private var lastReadGlobalManifestPath: String? = null
- private var globalManifest: EditorManifestJson? = null
-
- private var packagesByCanonicalName: Map = mutableMapOf()
- private var packagesByFolderPath: Map = mutableMapOf()
-
- private data class Packages(val packagesByCanonicalName: Map, val packagesByFolderPath: Map)
-
- init {
- val listener = PackagesAsyncFileListener()
- VirtualFileManager.getInstance().addAsyncFileListener(listener, project)
-
- val lifetime = project.lifetime
-
- // The application path affects the module packages. This comes from the backend, so will be up to date with
- // changes from the Editor via protocol, or changes to the project files via heuristics
- project.solution.frontendBackendModel.unityApplicationData.advise(lifetime) { scheduleRefreshAndNotify() }
-
- scheduleRefreshAndNotify()
- }
+ private val packages = mutableMapOf()
+ private val packagesByFolder = mutableMapOf()
- val packagesFolder: VirtualFile
- get() = project.projectDir.findChild("Packages")!!
-
- val hasPackages: Boolean
- get() = packagesByCanonicalName.isNotEmpty()
-
- val allPackages: List
- get() = packagesByCanonicalName.values.toList()
-
- val localPackages: List
- get() = filterPackagesBySource(PackageSource.Local).toList()
-
- val immutablePackages: List
- get() = packagesByCanonicalName.filterValues { !it.source.isEditable() && it.source != PackageSource.Unknown }.values.toList()
+ // We don't support a granular add/remove notification, just a batched "changed" message
+ // Our only subscriber for events right now is the Unity Explorer, and that works best by refreshing a parent node
+ // and getting all packages
+ private val listeners = EventDispatcher.create(PackageManagerListener::class.java)
+ private val alarm = pooledThreadSingleAlarm(MILLISECONDS_BEFORE_REFRESH, project, ::notifyPackagesChanged)
- val unknownPackages: List
- get() = filterPackagesBySource(PackageSource.Unknown).toList()
+ // TODO: Threading issues adding/removing and accessing?
- val hasBuiltInPackages: Boolean
- get() = filterPackagesBySource(PackageSource.BuiltIn).any()
+ fun getPackages(): List = packages.values.toList()
- fun hasPackage(id:String):Boolean {
- return allPackages.any { it.name == id }
- }
+ fun hasPackage(id: String) = packages.containsKey(id)
+ fun tryGetPackage(id: String): PackageData? = packages[id]
+ fun tryGetPackage(packageFolder: VirtualFile): PackageData? = packagesByFolder[packageFolder]
- fun getPackageData(packageFolder: VirtualFile): PackageData? {
- return packagesByFolderPath[packageFolder.path]
+ fun addPackage(id: String, pack: UnityPackage) {
+ logger.trace("Adding Unity package: $id")
+ val packageData = PackageData.fromUnityPackage(pack)
+ packages[id] = packageData
+ packageData.packageFolder?.let { packagesByFolder[it] = packageData }
+ scheduleNotifyPackagesChanged()
}
- fun getPackageData(canonicalName: String): PackageData? {
- return packagesByCanonicalName[canonicalName]
+ fun removePackage(id: String) {
+ logger.trace("Removing Unity package: $id")
+ packages.remove(id)?.let { packagesByFolder.remove(it.packageFolder) }
+ scheduleNotifyPackagesChanged()
}
+ // Listeners are called back on a pooled thread!
fun addListener(listener: PackageManagerListener) {
// Automatically scoped to project lifetime
listeners.addListener(listener, project)
}
- private fun scheduleRefreshAndNotify() {
+ private fun scheduleNotifyPackagesChanged() {
alarm.cancelAndRequest()
}
- private fun refreshAndNotify() {
- // Get package data on a background thread
- ReadAction.nonBlocking { getPackages() }
- .expireWith(project)
- .finishOnUiThread(ModalityState.any()) {
- // Updated without locks. Each reference is updated atomically, and they are not used together, so there
- // are no tearing issues
- packagesByCanonicalName = it.packagesByCanonicalName
- packagesByFolderPath = it.packagesByFolderPath
- listeners.multicaster.onPackagesUpdated()
- }
- .submit(NonUrgentExecutor.getInstance())
- }
-
- private data class EditorPackageDetails(val introduced: String?, val minimumVersion: String?, val version: String?)
- private data class EditorManifestJson(val recommended: Map?, val defaultDependencies: Map?, val packages: Map?)
-
- private fun getPackages(): Packages {
- logger.debug("Refreshing packages manager")
- return getPackagesFromPackagesLockJson() ?: getPackagesFromManifestJson()
- }
-
- // 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
- private fun getPackagesFromPackagesLockJson(): Packages? {
- val packagesLockJsonFile = getPackagesLockJsonFile() ?: return null
-
- logger.debug("Getting packages from packages-lock.json")
-
- val builtInPackagesFolder = UnityInstallationFinder.getInstance(project).getBuiltInPackagesRoot()
-
- val byCanonicalName: MutableMap = mutableMapOf()
- val byFolderPath: MutableMap = mutableMapOf()
-
- // This file contains all packages, including transitive dependencies and folders
- val packagesLock = readPackagesLockFile(packagesLockJsonFile) ?: return null
-
- for ((name, details) in packagesLock.dependencies) {
-
- // Note that packages-lock.json doesn't seem to use a version of "exclude" to indicate that a package has
- // been disabled, unlike older manifest.json versions. It just removes it from the manifest + lock files
-
- val packageData = getPackageData(packagesFolder, name, details, builtInPackagesFolder)
- byCanonicalName[name] = packageData
- if (packageData.packageFolder != null) {
- byFolderPath[packageData.packageFolder.path] = packageData
- }
- }
-
- return Packages(byCanonicalName, byFolderPath)
- }
-
- private fun getPackagesFromManifestJson(): Packages {
-
- logger.debug("Getting packages from manifest.json")
-
- val byCanonicalName: MutableMap = mutableMapOf()
- val byFolderPath: MutableMap = mutableMapOf()
-
- val builtInPackagesFolder = UnityInstallationFinder.getInstance(project).getBuiltInPackagesRoot()
- val manifestJsonFile = getManifestJsonFile() ?: return Packages(byCanonicalName, byFolderPath)
-
- val globalManifestPath = UnityInstallationFinder.getInstance(project).getPackageManagerDefaultManifest()
- if (globalManifestPath != null && globalManifestPath.toString() != lastReadGlobalManifestPath) {
- globalManifest = readGlobalManifestFile(globalManifestPath)
- }
-
- val projectManifest = readProjectManifestFile(manifestJsonFile)
-
- val registry = projectManifest.registry ?: DEFAULT_REGISTRY_URL
- for ((name, version) in projectManifest.dependencies) {
- if (version.equals("exclude", true)) continue
-
- val lockDetails = projectManifest.lock?.get(name)
- val packageData = getPackageData(packagesFolder, name, version, registry, builtInPackagesFolder, lockDetails)
- byCanonicalName[name] = packageData
- if (packageData.packageFolder != null) {
- byFolderPath[packageData.packageFolder.path] = packageData
- }
- }
-
- // 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
- for (child in packagesFolder.children) {
- val packageData = getPackageDataFromFolder(child.name, child, PackageSource.Embedded)
- if (packageData != null) {
- byCanonicalName[packageData.details.canonicalName] = packageData
- byFolderPath[child.path] = packageData
- }
- }
-
- // Calculate the transitive dependencies. This is all based on observation
- var packagesToProcess: Collection = byCanonicalName.values
- while (packagesToProcess.isNotEmpty()) {
-
- // This can't get stuck in an infinite loop. We look up each package in resolvedPackages - if it's already
- // there, it doesn't get processed any further, and we update resolvedPackages (well packagesByCanonicalName)
- // after every loop
- packagesToProcess = getPackagesFromDependencies(packagesFolder, registry, builtInPackagesFolder, byCanonicalName, packagesToProcess)
- for (newPackage in packagesToProcess) {
- byCanonicalName[newPackage.details.canonicalName] = newPackage
- newPackage.packageFolder?.let { byFolderPath[it.path] = newPackage }
- }
- }
-
- return Packages(byCanonicalName, byFolderPath)
- }
-
- private fun readGlobalManifestFile(editorManifestPath: Path): EditorManifestJson {
- lastReadGlobalManifestPath = editorManifestPath.toString()
- return try {
- gson.fromJson(editorManifestPath.inputStream().reader(), EditorManifestJson::class.java)
- } catch (e: Throwable) {
- if (e is ControlFlowException) {
- // Leave this null so we'll try and load again next time
- lastReadGlobalManifestPath = null
- } else {
- logger.error("Error deserializing Resources/PackageManager/Editor/manifest.json")
- }
- EditorManifestJson(emptyMap(), emptyMap(), emptyMap())
+ private fun notifyPackagesChanged() {
+ // Make sure to call back on the main thread
+ application.invokeLater {
+ listeners.multicaster.onPackagesUpdated()
}
}
-
- private fun readProjectManifestFile(manifestFile: VirtualFile): ManifestJson {
- return try {
- gson.fromJson(manifestFile.inputStream.reader(), ManifestJson::class.java)
- } catch (e: Throwable) {
- if (e !is ControlFlowException) {
- logger.error("Error deserializing Packages/manifest.json", e)
- }
- ManifestJson(emptyMap(), emptyArray(), null, emptyMap())
- }
- }
-
- private fun readPackagesLockFile(packagesLockFile: VirtualFile): PackagesLockJson? = try {
- gson.fromJson(packagesLockFile.inputStream.reader(), PackagesLockJson::class.java)
- } catch (e: Throwable) {
- if (e !is ControlFlowException) {
- logger.error("Error deserializeing Packages/packages-lock.json", e)
- }
- null
- }
-
- private fun getPackagesLockJsonFile(): VirtualFile? {
- // Only exists in Unity 2019.4+
- return project.findFile("Packages/packages-lock.json")
- }
-
- private fun getManifestJsonFile(): VirtualFile? {
- return project.findFile("Packages/manifest.json")
- }
-
- private fun getPackagesFromDependencies(packagesFolder: VirtualFile, registry: String, builtInPackagesFolder: Path?,
- resolvedPackages: Map,
- packages: Collection)
- : Collection {
-
- val dependencies = mutableMapOf()
-
- // Find the highest requested version of each dependency of each package being processed
- for (packageData in packages) {
-
- for ((name, version) in packageData.details.dependencies) {
-
- // If it's been previously resolved, there's nothing more to do. Note that skipping it here means it's
- // not processed further, including dependencies
- if (resolvedPackages.containsKey(name)) continue
-
- val lastVersion = dependencies[name]
- val thisVersion = SemVer.parse(version)
- if (thisVersion == null || (lastVersion != null && lastVersion >= thisVersion)) continue
-
- val minimumVersion = SemVer.parse(globalManifest?.packages?.get(name)?.minimumVersion ?: "")
- dependencies[name] = if (minimumVersion != null && minimumVersion > thisVersion) {
- minimumVersion
- }
- else {
- thisVersion
- }
- }
- }
-
- // Now find all of the packages for all of these dependencies
- val newPackages = mutableListOf()
- for ((name, version) in dependencies) {
- newPackages.add(getPackageData(packagesFolder, name, version.toString(), registry, builtInPackagesFolder, null))
- }
- return newPackages
- }
-
- private fun getPackageData(packagesFolder: VirtualFile,
- name: String,
- version: String,
- registry: String,
- builtInPackagesFolder: Path?,
- lockDetails: LockDetails?)
- : PackageData {
-
- // 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.
- return try {
- getEmbeddedPackage(packagesFolder, name, name)
- ?: getRegistryPackage(name, version, registry)
- ?: getGitPackage(name, version, lockDetails?.hash, lockDetails?.revision)
- ?: getLocalPackage(packagesFolder, name, version)
- ?: getLocalTarballPackage(packagesFolder, name, version)
- ?: getBuiltInPackage(name, version, builtInPackagesFolder)
- ?: PackageData.unknown(name, version)
- }
- catch (throwable: Throwable) {
- if (throwable !is ControlFlowException) {
- logger.error("Error resolving package $name", throwable)
- }
- PackageData.unknown(name, version)
- }
- }
-
- private fun getPackageData(packagesFolder: VirtualFile,
- name: String,
- details: PackagesLockDependency,
- builtInPackagesFolder: Path?)
- : PackageData {
-
- // Embedded packages are listed in packages-lock.json with the name of the package, and the version is a file:
- // spec pointing to the embedded folder in Packages
- return try {
- when (details.source) {
- "embedded" -> getEmbeddedPackage(packagesFolder, name, details.version)
- "registry" -> getRegistryPackage(name, details.version, details.url ?: DEFAULT_REGISTRY_URL)
- "builtin" -> getBuiltInPackage(name, details.version, builtInPackagesFolder)
- "git" -> getGitPackage(name, details.version, details.hash)
- "local" -> getLocalPackage(packagesFolder, name, details.version)
- "local-tarball" -> getLocalTarballPackage(packagesFolder, name, details.version)
- else -> null
- } ?: PackageData.unknown(name, details.version)
- } catch (throwable: Throwable) {
- if (throwable !is ControlFlowException) {
- logger.error("Error resolving package $name", throwable)
- }
- PackageData.unknown(name, details.version)
- }
- }
-
- private fun getLocalPackage(packagesFolder: VirtualFile, name: String, version: String): PackageData? {
-
- if (!version.startsWith("file:")) {
- return null
- }
-
- return try {
- val path = version.substring(5)
- val packagesPath = Paths.get(packagesFolder.path)
- val filePath = packagesPath.resolve(path).normalize()
- val packageFolder = VfsUtil.findFile(filePath, false)
- when (packageFolder?.isDirectory) {
- true -> getPackageDataFromFolder(name, packageFolder, PackageSource.Local)
- else -> null
- }
- } catch (throwable: Throwable) {
- logger.error("Error resolving local package", throwable)
- null
- }
- }
-
- private fun getLocalTarballPackage(packagesFolder: VirtualFile, name: String, version: String): PackageData? {
- 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-creation-in-epoch-ms}
- return try {
- val path = version.substring(5)
- val packagesPath = Paths.get(packagesFolder.path)
- val tarballPath = packagesPath.resolve(path).normalize()
- val packageFile = VfsUtil.findFile(tarballPath, true)
- when {
- packageFile?.isExistingFile() == true -> {
-
- // 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.
- val createdTimestamp = packageFile.timeStamp
- val hash = getMd5OfString(tarballPath.toString()).substring(0, 12)
-
- val packageCacheFolder = project.findFile("Library/PackageCache")
- val packageFolder = packageCacheFolder?.findChild("$name@$hash-$createdTimestamp")
- val tarballLocation = if (tarballPath.startsWith(project.projectDir.toNioPath())) {
- // If the package lives under the solution root, it looks better to include the solution root name
- // E.g. MyUnityProject/package.tgz, rather than just package.tgz
- project.projectDir.parent.toNioPath().relativize(tarballPath)
- } else {
- tarballPath
- }
- getPackageDataFromFolder(name, packageFolder, PackageSource.LocalTarball, tarballLocation = tarballLocation.toString())
- }
- else -> null
- }
- }
- catch (throwable: Throwable) {
- logger.error("Error resolving local package", throwable)
- null
- }
- }
-
- private fun getMd5OfString(value: String): String {
- var result = ""
- val instance = MessageDigest.getInstance("MD5")
- if (instance != null) {
- val digest = instance.digest(value.toByteArray(Charset.forName("UTF-8")))
- result = digest.joinToString("") { String.format("%02x", it) }
- }
-
- return result.padStart(32, '0')
- }
-
- private fun getGitPackage(name: String, version: String, hash: String?, revision: String? = null): PackageData? {
- if (hash == null) return null
-
- // If we have lockDetails, we know this is a git based package, so always return something
- return try {
- // 2019.3 changed the format of the cached folder to only use the first 10 characters of the hash
- val packageFolder = project.findFile("Library/PackageCache/$name@${hash}")
- ?: project.findFile("Library/PackageCache/$name@${hash.substring(0, min(hash.length, 10))}")
- getPackageDataFromFolder(name, packageFolder, PackageSource.Git, GitDetails(version, hash, revision))
- }
- catch (throwable: Throwable) {
- logger.error("Error resolving git package", throwable)
- PackageData.unknown(name, version)
- }
- }
-
- private fun getEmbeddedPackage(packagesFolder: VirtualFile, name: String, filePath: String): PackageData? {
- // filePath will have a file: prefix for packages-lock.json, but will be a simple package name for manifest.json
- val packageFolder = packagesFolder.findChild(filePath.removePrefix("file:"))
- return getPackageDataFromFolder(name, packageFolder, PackageSource.Embedded)
- }
-
- private fun getRegistryPackage(name: String, version: String, registry: String): PackageData? {
- // 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 per-user cache
- // NOTE: We use findChild here because name/version might contain illegal chars, e.g. "https://" which will
- // throw in refreshAndFindFile on Windows
- val packageCacheFolder = project.findFile("Library/PackageCache")
- var packageFolder = packageCacheFolder?.findChild("$name@$version")
- val packageData = getPackageDataFromFolder(name, packageFolder, PackageSource.Registry)
- if (packageData != null) return packageData
-
- val registryRoot = UnityCachesFinder.getPackagesCacheFolder(registry)
- if (registryRoot == null || !registryRoot.isDirectory) return null
-
- packageFolder = registryRoot.findChild("$name@$version")
- return getPackageDataFromFolder(name, packageFolder, PackageSource.Registry)
- }
-
- private fun getBuiltInPackage(name: String, version: String, builtInPackagesFolder: Path?): PackageData? {
-
- // If we can identify the module root of the current project, use it to look up the module
- if (builtInPackagesFolder?.isDirectory() == true) {
- val packageFolder = VfsUtil.findFile(builtInPackagesFolder.resolve(name), false)
- return getPackageDataFromFolder(name, packageFolder, PackageSource.BuiltIn)
- }
-
- // Simple heuristic to identify modules when we can't look them up in the correct location. Unity will control
- // the namespace of their registries, which makes it highly unlikely anyone else will create a package that
- // begins with `com.unity.modules.` And if they do, they only have themselves to blame.
- if (name.startsWith("com.unity.modules.")) {
- return PackageData.unknown(name, version, PackageSource.BuiltIn)
- }
-
- return null
- }
-
- private fun getPackageDataFromFolder(name: String, packageFolder: VirtualFile?, source: PackageSource,
- gitDetails: GitDetails? = null, tarballLocation: String? = null): PackageData? {
- packageFolder?.let {
- if (packageFolder.isDirectory) {
- val packageDetails = readPackagesJson(packageFolder)
- if (packageDetails != null) {
- return PackageData(name, packageFolder, packageDetails, source, gitDetails, tarballLocation)
- }
- }
- }
- return null
- }
-
- private fun readPackagesJson(packageFolder: VirtualFile): PackageDetails? {
- val packageFile = packageFolder.findChild("package.json")
- if (packageFile?.exists() == true && !packageFile.isDirectory) {
- try {
- val packageJson = gson.fromJson(packageFile.inputStream.reader(), PackageJson::class.java)
- return PackageDetails.fromPackageJson(packageFolder, packageJson!!)
- }
- catch (t: Throwable) {
- if (t !is ControlFlowException) {
- logger.error("Error reading package.json", t)
- }
- }
- }
- return null
- }
-
- private fun filterPackagesBySource(source: PackageSource): List {
- return packagesByCanonicalName.filterValues { it.source == source }.values.toList()
- }
-
- private inner class PackagesAsyncFileListener : AsyncFileListener {
- override fun prepareChange(events: MutableList): ChangeApplier? {
- var refreshPackages = false
-
- events.forEach {
- ProgressManager.checkCanceled()
-
- // Update on any kind of change/creation/deletion of the main manifest.json, any package.json or the
- // deletion/creation of the Packages folder
- val path = it.path
- if (path.endsWith("/Packages/manifest.json", true)
- || path.endsWith("/package.json", true)
- || path.endsWith("/Packages/packages-lock.json", true)
- || path.endsWith("/Packages", true)) {
-
- refreshPackages = true
- }
- }
-
- if (!refreshPackages) return null
-
- return object: ChangeApplier {
- override fun afterVfsChange() = scheduleRefreshAndNotify()
- }
- }
- }
-}
\ No newline at end of file
+}
diff --git a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/workspace/UnityWorkspaceModelUpdater.kt b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/workspace/UnityWorkspaceModelUpdater.kt
index 57af03f5b2..0ae5000078 100644
--- a/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/workspace/UnityWorkspaceModelUpdater.kt
+++ b/rider/src/main/kotlin/com/jetbrains/rider/plugins/unity/workspace/UnityWorkspaceModelUpdater.kt
@@ -27,7 +27,7 @@ class UnityWorkspaceModelUpdater(private val project: Project) {
application.invokeLater {
rebuildWorkspaceModel()
- // Listen for external packages that we should index
+ // Listen for external packages that we should index. Called on the UI thread
PackageManager.getInstance(project).addListener(object : PackageManagerListener {
override fun onPackagesUpdated() {
rebuildWorkspaceModel()
@@ -46,8 +46,8 @@ class UnityWorkspaceModelUpdater(private val project: Project) {
val packagesModuleEntity = builder.addRiderModuleEntity(PackagesModuleName, RiderUnityEntitySource)
// TODO: WORKSPACEMODEL
- // We want to include list of specifal files (by extensions comes from unity editor)
- // in the content mode. It is better to do it on backed via backend PackageManager
+ // We want to include list of special files (by extensions comes from unity editor)
+ // in the content model. It is better to do it on backed via backend PackageManager
builder.addContentRootEntity(
project.solutionDirectory.resolve("Packages").toVirtualFileUrl(virtualFileUrlManager), listOf(), listOf("*.meta", "*.tmp"), packagesModuleEntity
@@ -56,11 +56,11 @@ class UnityWorkspaceModelUpdater(private val project: Project) {
project.solutionDirectory.resolve("ProjectSettings").toVirtualFileUrl(virtualFileUrlManager), listOf(), listOf("*.meta", "*.tmp"), packagesModuleEntity
)
- val packages = PackageManager.getInstance(project).allPackages
+ val packages = PackageManager.getInstance(project).getPackages()
if (packages.any()) {
for (packageData in packages) {
val packageFolder = packageData.packageFolder ?: continue
- if (packageData.source !in arrayOf(PackageSource.BuiltIn, PackageSource.Embedded, PackageSource.Unknown)) {
+ if (packageData.source !in arrayOf(PackageSource.Embedded, PackageSource.Unknown)) {
builder.addContentRootEntity(
packageFolder.toVirtualFileUrl(virtualFileUrlManager), listOf(), listOf("*.meta", "*.tmp"), packagesModuleEntity
)
diff --git a/rider/src/test/kotlin/base/ProjectModel.API.kt b/rider/src/test/kotlin/base/ProjectModel.API.kt
index 46c0070499..4020333434 100644
--- a/rider/src/test/kotlin/base/ProjectModel.API.kt
+++ b/rider/src/test/kotlin/base/ProjectModel.API.kt
@@ -10,7 +10,7 @@ import com.jetbrains.rider.model.RdProjectModelDumpFlags
import com.jetbrains.rider.model.RdProjectModelDumpParams
import com.jetbrains.rider.model.projectModelTasks
import com.jetbrains.rider.plugins.unity.explorer.UnityExplorer
-import com.jetbrains.rider.plugins.unity.explorer.UnityExplorerNode
+import com.jetbrains.rider.plugins.unity.explorer.UnityExplorerFileSystemNode
import com.jetbrains.rider.projectView.actions.renameAction.RiderRenameItemHandler
import com.jetbrains.rider.projectView.moveProviders.RiderCutProvider
import com.jetbrains.rider.projectView.moveProviders.RiderDeleteProvider
@@ -105,7 +105,7 @@ fun createDataContextFor2(project: Project, paths: Array>): DataCo
fun findReq(path: Array, project: Project): AbstractTreeNode<*> {
val viewPane = UnityExplorer.getInstance(project)
val solutionNode = viewPane.model.root
- val fileNodes = viewPane.model.root.children.filterIsInstance()
+ val fileNodes = viewPane.model.root.children.filterIsInstance()
val solutionNodeName = solutionNode.name
if (path.count() == 1) {