diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4f6ec7a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,137 @@ +name: Build & Release Rider Plugin + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: + contents: write + +jobs: + build: + runs-on: ubuntu-latest + name: Build Plugin (PR / Main) + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build plugin + run: ./gradlew buildPlugin + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: plugin-build + path: build/distributions/*.zip + + release: + needs: build + runs-on: ubuntu-latest + name: Release on Main + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Determine new version and changelog + id: changelog + uses: release-please/action@v4 + with: + release-type: simple + package-name: rider-plugin + changelog-types: | + [ + {"type":"feat","section":"โœจ Features","hidden":false}, + {"type":"fix","section":"๐Ÿ› Fixes","hidden":false}, + {"type":"chore","section":"๐Ÿงน Chores","hidden":false}, + {"type":"docs","section":"๐Ÿ“š Docs","hidden":false} + ] + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Stop if no new release + if: ${{ steps.changelog.outputs.release_created != 'true' }} + run: echo "No new version to release." + + - name: Update gradle.properties version + if: ${{ steps.changelog.outputs.release_created == 'true' }} + run: | + TAG_NAME="${{ steps.changelog.outputs.tag_name }}" + VERSION="${TAG_NAME#v}" + sed -i.bak "s/^version=.*/version=${VERSION}/" gradle.properties + echo "โœ… Updated gradle.properties to version ${VERSION}" + + - name: Update plugin.xml version + if: ${{ steps.changelog.outputs.release_created == 'true' }} + run: | + TAG_NAME="${{ steps.changelog.outputs.tag_name }}" + VERSION="${TAG_NAME#v}" + XML_FILE="src/rider/main/resources/META-INF/plugin.xml" + echo "๐Ÿงฉ Updating ${XML_FILE} to version ${VERSION}" + + awk -v ver="$VERSION" ' + // { in_comment=0 } + !in_comment { + gsub(/[^<]+<\/version>/, "" ver "") + } + { print } + ' "$XML_FILE" > "$XML_FILE.tmp" && mv "$XML_FILE.tmp" "$XML_FILE" + + grep "" "$XML_FILE" || echo "(none found)" + + - name: Build plugin + if: ${{ steps.changelog.outputs.release_created == 'true' }} + run: ./gradlew buildPlugin + + - name: Commit updated version and changelog + if: ${{ steps.changelog.outputs.release_created == 'true' }} + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add gradle.properties CHANGELOG.md src/rider/main/resources/META-INF/plugin.xml + git commit -m "chore(release): update version to ${{ steps.changelog.outputs.tag_name }}" + git push + + - name: Create GitHub Release + if: ${{ steps.changelog.outputs.release_created == 'true' }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.changelog.outputs.tag_name }} + name: Release ${{ steps.changelog.outputs.tag_name }} + body: ${{ steps.changelog.outputs.release_notes }} + files: build/distributions/*.zip + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/gradle.properties b/gradle.properties index 1daed19..3bb1766 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ DotnetPluginId=ReSharperPlugin.SerializeReferenceDropdownIntegration DotnetSolution=ReSharperPlugin.SerializeReferenceDropdownIntegration.sln RiderPluginId=com.jetbrains.rider.plugins.serializereferencedropdownintegration -PluginVersion=1.1.1 +PluginVersion=1.1.2 BuildConfiguration=Debug diff --git a/src/dotnet/Plugin.props b/src/dotnet/Plugin.props index eccd83d..ae8bc4f 100644 --- a/src/dotnet/Plugin.props +++ b/src/dotnet/Plugin.props @@ -14,4 +14,9 @@ + + + + + diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsage/ClassUsageAnalyzer.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsage/ClassUsageAnalyzer.cs new file mode 100644 index 0000000..f58113f --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsage/ClassUsageAnalyzer.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.ReSharper.Daemon.CodeInsights; +using JetBrains.ReSharper.Feature.Services.Daemon; +using JetBrains.ReSharper.Psi.CSharp.Tree; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Util; +using ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity.SRD; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.ClassUsage; + +//TODO Move from problemAnalyzer to something else where usages works normally +[ElementProblemAnalyzer(typeof(IClassDeclaration))] +public class ClassUsageAnalyzer : ElementProblemAnalyzer +{ + private readonly ClassUsageInsightsProvider codeInsightsProvider; + private readonly UnitySrdDatabaseLoader unitySrdDatabaseLoader; + private readonly UnityProjectDetector unityProjectDetector; + + public static readonly Dictionary shortTypeToFullType = new(); + + public ClassUsageAnalyzer(ClassUsageInsightsProvider codeInsightsProvider, + UnitySrdDatabaseLoader unitySrdDatabaseLoader, UnityProjectDetector unityProjectDetector) + { + this.codeInsightsProvider = codeInsightsProvider; + this.unitySrdDatabaseLoader = unitySrdDatabaseLoader; + this.unityProjectDetector = unityProjectDetector; + } + + protected override void Run(IClassDeclaration element, ElementProblemAnalyzerData data, + IHighlightingConsumer consumer) + { + try + { + if (unityProjectDetector.IsUnityProject() == false) + { + return; + } + + unitySrdDatabaseLoader.RefreshDatabase(); + if (unitySrdDatabaseLoader.IsAvailableDatabase == false) + { + return; + } + + var nonReferenceType = element.IsStatic || element.IsAbstract; + if (nonReferenceType) + { + return; + } + + var superClassNames = element.DeclaredElement.GetAllSuperClasses().Select(t => t.GetClrName()); + var inheritedFromUnityObject = superClassNames.Any(t => t.FullName == "UnityEngine.Object"); + if (inheritedFromUnityObject) + { + return; + } + + var clrName = element.DeclaredElement.GetClrName(); + var name = clrName.FullName; + var asmName = element.GetPsiModule().ContainingProjectModule.Name; + var type = TypeExtensions.MakeType(name, asmName); + unitySrdDatabaseLoader.TypesCount.TryGetValue(type, out var usageCount); + shortTypeToFullType[clrName.ShortName] = type; + + //TODO Need check usages with MovedFrom attribute + var tooltip = $"SerializeReferenceDropdown: '{clrName.ShortName}' {usageCount} - usages in project"; + consumer.AddHighlighting( + new CodeInsightsHighlighting( + element.GetNameDocumentRange(), + displayText: $"SRD: {usageCount} usages", + tooltipText: tooltip, + moreText: String.Empty, + codeInsightsProvider, + element.DeclaredElement, null)); + } + catch (Exception e) + { + Log.DevError(e.ToString()); + } + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsageInsightsProvider.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsage/ClassUsageInsightsProvider.cs similarity index 64% rename from src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsageInsightsProvider.cs rename to src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsage/ClassUsageInsightsProvider.cs index cbe723d..d65cc5b 100644 --- a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsageInsightsProvider.cs +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsage/ClassUsageInsightsProvider.cs @@ -3,22 +3,33 @@ using JetBrains.ProjectModel; using JetBrains.ReSharper.Daemon.CodeInsights; using JetBrains.Rider.Model; +using ReSharperPlugin.SerializeReferenceDropdownIntegration.ToUnity; -namespace ReSharperPlugin.SerializeReferenceDropdownIntegration; +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.ClassUsage; -[SolutionComponent(Instantiation.ContainerAsyncPrimaryThread)] +[SolutionComponent(Instantiation.DemandAnyThreadSafe)] public class ClassUsageInsightsProvider : ICodeInsightsProvider { + private readonly ToUnitySrdPipe toUnitySrdPipe; + private readonly ToUnityWindowFocusSwitch toUnityWindowFocusSwitch; + + public ClassUsageInsightsProvider(ToUnitySrdPipe toUnitySrdPipe, + ToUnityWindowFocusSwitch toUnityWindowFocusSwitch) + { + this.toUnitySrdPipe = toUnitySrdPipe; + this.toUnityWindowFocusSwitch = toUnityWindowFocusSwitch; + } + public bool IsAvailableIn(ISolution solution) { return true; } - public void OnClick(CodeInsightHighlightInfo highlightInfo, ISolution solution,CodeInsightsClickInfo clickInfo) + public void OnClick(CodeInsightHighlightInfo highlightInfo, ISolution solution, CodeInsightsClickInfo clickInfo) { var typeName = GetFullTypeName(highlightInfo); - UnityBridge.OpenUnitySearchToolWindowWithType(typeName); - WindowFocusSwitch.SwitchToUnityApplication(); + toUnitySrdPipe.OpenUnitySearchToolWindowWithType(typeName); + toUnityWindowFocusSwitch.SwitchToUnityApplication(); } private string GetFullTypeName(CodeInsightHighlightInfo highlightInfo) diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsageAnalyzer.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsageAnalyzer.cs deleted file mode 100644 index 874e66d..0000000 --- a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ClassUsageAnalyzer.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.Application.DataContext; -using JetBrains.Application.Notifications; -using JetBrains.ProjectModel; -using JetBrains.ReSharper.Daemon.CodeInsights; -using JetBrains.ReSharper.Feature.Services.Daemon; -using JetBrains.ReSharper.Psi.CSharp.Tree; -using JetBrains.ReSharper.Psi.Tree; -using JetBrains.Lifetimes; - -namespace ReSharperPlugin.SerializeReferenceDropdownIntegration; - -[ElementProblemAnalyzer(typeof(IClassDeclaration))] -public class ClassUsageAnalyzer : ElementProblemAnalyzer -{ - private readonly ClassUsageInsightsProvider codeInsightsProvider; - private readonly UserNotifications userNotifications; - private readonly Lifetime lifetime; - private readonly DatabaseLoader databaseLoader; - - public static readonly Dictionary shortTypeToFullType = new(); - - public ClassUsageAnalyzer(ClassUsageInsightsProvider codeInsightsProvider, UserNotifications userNotifications, - Lifetime lifetime, IDataContext context, ISolution solution) - { - this.codeInsightsProvider = codeInsightsProvider; - this.userNotifications = userNotifications; - this.lifetime = lifetime; - databaseLoader = new DatabaseLoader(solution, this.lifetime); - LoadDatabase(); - } - - private async void LoadDatabase() - { - Log.DevInfo("Start load database"); - var result = await databaseLoader.LoadDatabase(); - Log.DevInfo($"End load database: {result}"); - if (result == LoadResult.NoError) - { - var body = $"Loaded - {databaseLoader.TypesCount.Count} types \n" + - $"Last refresh: {databaseLoader.LastDatabaseUpdate}"; - userNotifications.CreateNotification(lifetime, NotificationSeverity.INFO, - "SRD - Database loaded", - body, closeAfterExecution: true); - } - - if (result == LoadResult.NoDatabaseFile) - { - userNotifications.CreateNotification(lifetime, NotificationSeverity.WARNING, - "SRD - No Database File", - "Need generate database file", closeAfterExecution: true); - } - } - - - protected override void Run(IClassDeclaration element, ElementProblemAnalyzerData data, - IHighlightingConsumer consumer) - { - databaseLoader.UpdateDatabaseBackground(); - if (databaseLoader.IsAvailableDatabase == false) - { - return; - } - - var clrName = element.DeclaredElement.GetClrName(); - var name = clrName.FullName; - var asmName = element.GetPsiModule().ContainingProjectModule.Name; - var type = DatabaseLoader.MakeType(name, asmName); - databaseLoader.TypesCount.TryGetValue(type, out var usageCount); - shortTypeToFullType[clrName.ShortName] = type; - - var tooltip = $"SerializeReferenceDropdown: '{clrName.ShortName}' {usageCount} - usages in project"; - consumer.AddHighlighting( - new CodeInsightsHighlighting( - element.GetNameDocumentRange(), - displayText: $"SRD: {usageCount} usages", - tooltipText: tooltip, - moreText: String.Empty, - codeInsightsProvider, - element.DeclaredElement, null)); - } -} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Log.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Log.cs index fa090d6..6dc4bea 100644 --- a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Log.cs +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Log.cs @@ -1,5 +1,5 @@ +using System; using System.Diagnostics; -using JetBrains.Util; namespace ReSharperPlugin.SerializeReferenceDropdownIntegration; @@ -8,12 +8,12 @@ public class Log [Conditional("DEVLOG")] public static void DevInfo(string data) { - MessageBox.ShowInfo(data, "SRD DEV"); + Console.WriteLine($"SRD DEV: {data}"); } [Conditional("DEVLOG")] public static void DevError(string data) { - MessageBox.ShowInfo(data, "SRD DEV"); + Console.WriteLine($"SRD DEV ERROR: {data}"); } } \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromAtomicRename.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromAtomicRename.cs new file mode 100644 index 0000000..409590c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromAtomicRename.cs @@ -0,0 +1,186 @@ +using System.Collections.Generic; +using JetBrains.Application.Progress; +using JetBrains.Diagnostics; +using JetBrains.ReSharper.Feature.Services.Refactorings; +using JetBrains.ReSharper.Feature.Services.Refactorings.Specific.Rename; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.CSharp.Impl; +using JetBrains.ReSharper.Psi.CSharp.Tree; +using JetBrains.ReSharper.Psi.Pointers; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Refactorings.Rename; +using JetBrains.Util; +using JetBrains.Util.dataStructures; +using ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Refactorings.Rename; + +public class MovedFromAtomicRename : AtomicRenameBase +{ + private readonly KnownTypesCache myKnownTypesCache; + private readonly IDeclaredElementPointer myPointer; + private readonly MovedFromRenameModel myModel; + + public MovedFromAtomicRename(IDeclaredElement declaredElement, string newName, + KnownTypesCache knownTypesCache) + { + myKnownTypesCache = knownTypesCache; + NewName = newName; + OldName = declaredElement.ShortName; + myPointer = declaredElement.CreateElementPointer(); + myModel = new MovedFromRenameModel(); + } + + public override IRefactoringPage CreateRenamesConfirmationPage(IRenameWorkflow renameWorkflow, + IProgressIndicator pi) + { + // hide confirmation page only, refactoring should update shared document too otherwise + // we will get inconsistent change modification message box + if (myModel.MovedFromRefactoringBehavior + is MovedFromRefactoringBehavior.AddAndRemember + or MovedFromRefactoringBehavior.DontAddAndRemember) + return null; + + return new MovedFromRefactoringPage( + ((RefactoringWorkflowBase)renameWorkflow).WorkflowExecuterLifetime, myModel, OldName); + } + + public override void Rename(IRenameRefactoring executer, IProgressIndicator pi, bool hasConflictsWithDeclarations, + IRefactoringDriver driver, PreviousAtomicRenames previousAtomicRenames) + { + if (myModel.MovedFromRefactoringBehavior + is MovedFromRefactoringBehavior.DontAdd + or MovedFromRefactoringBehavior.DontAddAndRemember) + return; + + var classMemberDeclaration = GetDeclaration(myPointer.FindDeclaredElement() as ITypeMember); + if (classMemberDeclaration == null) + return; + + //TODO Ask about remove old attribute? We can't use together two or more MovedFrom attributes + // RemoveExistingAttributesWithNewName(classMemberDeclaration); + + if (HasExistingMovedFromAttribute(classMemberDeclaration)) + { + // Make sure textual occurrence rename doesn't rename the existing attribute parameter + RemoveFromTextualOccurrences(executer, classMemberDeclaration); + return; + } + + //TODO Make rename source namespaces? + var attribute = CreateMovedFromAttribute(classMemberDeclaration, oldClassName: OldName); + if (attribute != null) + classMemberDeclaration.AddAttributeAfter(attribute, null); + } + + private void RemoveExistingAttributesWithNewName(IClassMemberDeclaration classMemberDeclaration) + { + var attributes = GetExistingFormerlySerializedAsAttributes(classMemberDeclaration, NewName); + foreach (var attribute in attributes) + classMemberDeclaration.RemoveAttribute(attribute); + } + + private static IClassMemberDeclaration? GetDeclaration(ITypeMember? typeMember) + { + var declarations = typeMember?.GetDeclarations(); + if (declarations?.Count == 1) + return declarations[0] as IClassMemberDeclaration; + return null; + } + + private bool HasExistingMovedFromAttribute(IClassMemberDeclaration classMemberDeclaration) + { + var attributes = GetExistingFormerlySerializedAsAttributes(classMemberDeclaration, OldName); + return attributes.Count > 0; + } + + private FrugalLocalList GetExistingFormerlySerializedAsAttributes( + IClassMemberDeclaration fieldDeclaration, string nameArgument) + { + var list = new FrugalLocalList(); + foreach (var attribute in fieldDeclaration.AttributesEnumerable) + { + var attributeTypeElement = attribute.TypeReference?.Resolve().DeclaredElement as ITypeElement; + if (attributeTypeElement == null) + continue; + + if (Equals(attributeTypeElement.GetClrName(), KnownTypes.MovedFromAttribute)) + { + var attributeInstance = attribute.GetAttributeInstance(); + var nameParameter = attributeInstance.PositionParameter(0); + if (nameParameter.IsConstant && nameParameter.ConstantValue.IsString(out var stringValue) && + stringValue == nameArgument) + { + list.Add(attribute); + } + } + } + + return list; + } + + private void RemoveFromTextualOccurrences(IRenameRefactoring executor, IClassMemberDeclaration fieldDeclaration) + { + if (executor.Workflow is not RenameWorkflow workflow) + return; + + var attributes = fieldDeclaration.Attributes; + if (attributes.Count == 0) + return; + + var attribute = attributes[0]; + var attributeSectionList = AttributeSectionListNavigator.GetByAttribute(attribute); + if (attributeSectionList == null) + return; + + var attributesRange = attributeSectionList.GetDocumentRange(); + + foreach (var occurrence in workflow.DataModel.ActualOccurrences ?? + EmptyList.InstanceList) + { + if (!occurrence.Included) + continue; + + + var occurrenceRange = occurrence.Marker.DocumentRange; + if (attributesRange.Contains(occurrenceRange)) + { + occurrence.Included = false; + break; + } + } + } + + private IAttribute? CreateMovedFromAttribute(IClassMemberDeclaration owningNode, + string oldClassName = null, string oldNamespace = null) + { + var module = owningNode.GetPsiModule(); + var elementFactory = CSharpElementFactory.GetInstance(owningNode); + var attributeType = myKnownTypesCache.GetByClrTypeName(KnownTypes.MovedFromAttribute, module); + var attributeTypeElement = attributeType.GetTypeElement(); + if (attributeTypeElement == null) + return null; + + var movedFromArguments = new AttributeValue[] + { + new(ConstantValue.Bool(true, module)), + new(ConstantValue.String(oldNamespace, module)), + new(ConstantValue.String(null, module)), + new(ConstantValue.String(oldClassName, module)) + }; + + + var movedFromAttribute = elementFactory.CreateAttribute(attributeTypeElement, + movedFromArguments, + EmptyArray>.Instance); + + return movedFromAttribute; + } + + public override IDeclaredElement NewDeclaredElement => myPointer.FindDeclaredElement().NotNull(); + public override string NewName { get; } + public override string OldName { get; } + public override IDeclaredElement PrimaryDeclaredElement => myPointer.FindDeclaredElement().NotNull(); + public override IList SecondaryDeclaredElements => null; +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromAtomicRenameFactory.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromAtomicRenameFactory.cs new file mode 100644 index 0000000..1339692 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromAtomicRenameFactory.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using JetBrains.Application; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Feature.Services.Refactorings.Specific.Rename; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.VB.Util; +using ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity; +using ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity.SRD; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Refactorings.Rename; + +[ShellFeaturePart] +public class MovedFromAtomicRenameFactory : IAtomicRenameFactory +{ + //TODO HACK I don't know why IsApplicable() called twice( + private static List _renameDeclaredElements = new(); + + public bool IsApplicable(IDeclaredElement declaredElement) + { + _renameDeclaredElements.Clear(); + + //TODO Support rename namespaces? + var isClass = declaredElement.IsClass(); + var isUnityProject = UnityProjectDetector.Instance.IsUnityProject(); + return isClass && isUnityProject; + } + + public RenameAvailabilityCheckResult CheckRenameAvailability(IDeclaredElement element) + { + return RenameAvailabilityCheckResult.CanBeRenamed; + } + + public IEnumerable CreateAtomicRenames(IDeclaredElement declaredElement, string newName, + bool doNotAddBindingConflicts) + { + if (_renameDeclaredElements.Contains(declaredElement.ShortName)) + { + return []; + } + + _renameDeclaredElements.Add(declaredElement.ShortName); + var knownTypesCache = declaredElement.GetSolution().GetComponent(); + return [new MovedFromAtomicRename(declaredElement, newName, knownTypesCache)]; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromRefactoringPage.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromRefactoringPage.cs new file mode 100644 index 0000000..d1dbcd4 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromRefactoringPage.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using JetBrains.DataFlow; +using JetBrains.IDE.UI.Extensions; +using JetBrains.Lifetimes; +using JetBrains.ReSharper.Feature.Services.Refactorings; +using JetBrains.Rider.Model.UIAutomation; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Refactorings.Rename; + +public class MovedFromRefactoringPage : SingleBeRefactoringPage +{ + private readonly MovedFromRenameModel myModel; + private readonly BeGrid myContent; + private readonly IProperty myShouldAddFormerlySerializedAs; + private readonly IProperty myRememberSelectedOptionAndNeverShowPopup; + + // TODO Replace strings with resourceManager? + public MovedFromRefactoringPage(Lifetime lifetime, + MovedFromRenameModel model, string oldName) : base(lifetime) + { + myModel = model; + + myShouldAddFormerlySerializedAs = new Property("Should add attribute action", + model.MovedFromRefactoringBehavior is MovedFromRefactoringBehavior.Add + or MovedFromRefactoringBehavior.AddAndRemember); + + + myContent = myShouldAddFormerlySerializedAs.GetBeRadioGroup(lifetime, + $"Add attribute MovedFrom to class: {oldName}", + new List { true, false }, + present: (settings, properties) => settings ? "Add" : "Don't Add", + horizontal: false + ).InAutoGrid(); + + + myRememberSelectedOptionAndNeverShowPopup = new Property("Save settings for this session", + model.MovedFromRefactoringBehavior + is MovedFromRefactoringBehavior.AddAndRemember + or MovedFromRefactoringBehavior.DontAddAndRemember); + myContent.AddElement(new BeSpacer()); + myContent.AddElement(myRememberSelectedOptionAndNeverShowPopup.GetBeCheckBox(lifetime, "Remember settings")); + } + + public override BeControl GetPageContent() => myContent; + + public override void Commit() + { + var shouldAdd = myShouldAddFormerlySerializedAs.Value; + var rememberSelectedOption = myRememberSelectedOptionAndNeverShowPopup?.Value ?? false; + + myModel.Commit(shouldAdd, rememberSelectedOption); + } + + public override string Title => "Rename with MovedFrom unity Attribute"; + public override string Description => "Renaming a class can break serialize references to this class"; +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromRenameModel.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromRenameModel.cs new file mode 100644 index 0000000..92796f7 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Refactorings/Rename/MovedFromRenameModel.cs @@ -0,0 +1,51 @@ +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Refactorings.Rename; + +public enum MovedFromRefactoringBehavior +{ + Add, + DontAdd, + AddAndRemember, + DontAddAndRemember, +} + +public enum MovedFromRefactoringSettings +{ + ShowPopup, + AlwaysAdd, + NeverAdd, +} + +public class MovedFromRenameModel +{ + //TODO Setup settings? + private static MovedFromRefactoringSettings ShowPopupSettings; + public MovedFromRefactoringBehavior MovedFromRefactoringBehavior { get; private set; } + + public MovedFromRenameModel() + { + MovedFromRefactoringBehavior = ShowPopupSettings switch + { + MovedFromRefactoringSettings.AlwaysAdd => MovedFromRefactoringBehavior.AddAndRemember, + MovedFromRefactoringSettings.NeverAdd => MovedFromRefactoringBehavior.DontAddAndRemember, + _ => MovedFromRefactoringBehavior.Add + }; + } + + public void Commit(bool shouldAddFormerlySerializedAs, bool rememberSelectedOptionAndNeverShowPopup) + { + MovedFromRefactoringBehavior = shouldAddFormerlySerializedAs + ? rememberSelectedOptionAndNeverShowPopup + ? MovedFromRefactoringBehavior.AddAndRemember + : MovedFromRefactoringBehavior.Add + : rememberSelectedOptionAndNeverShowPopup + ? MovedFromRefactoringBehavior.DontAddAndRemember + : MovedFromRefactoringBehavior.DontAdd; + + ShowPopupSettings = MovedFromRefactoringBehavior switch + { + MovedFromRefactoringBehavior.AddAndRemember => MovedFromRefactoringSettings.AlwaysAdd, + MovedFromRefactoringBehavior.DontAddAndRemember => MovedFromRefactoringSettings.NeverAdd, + _ => MovedFromRefactoringSettings.ShowPopup + }; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/UnityBridge.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ToUnity/ToUnitySrdPipe.cs similarity index 56% rename from src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/UnityBridge.cs rename to src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ToUnity/ToUnitySrdPipe.cs index efe3003..7817b56 100644 --- a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/UnityBridge.cs +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ToUnity/ToUnitySrdPipe.cs @@ -1,20 +1,23 @@ using System; using System.IO.Pipes; using System.Text; +using JetBrains.Application.Parts; +using JetBrains.ProjectModel; using JetBrains.Util; -namespace ReSharperPlugin.SerializeReferenceDropdownIntegration; +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.ToUnity; -public static class UnityBridge +[SolutionComponent(Instantiation.DemandAnyThreadSafe)] +public class ToUnitySrdPipe { - private const string pipeName = "SerializeReferenceDropdownIntegration"; - private static bool showOnce; + private const string PipeName = "SerializeReferenceDropdownIntegration"; + private bool showOnce; - public static void OpenUnitySearchToolWindowWithType(string typeName) + public void OpenUnitySearchToolWindowWithType(string typeName) { try { - using var client = new NamedPipeClientStream(".", pipeName, PipeDirection.Out); + using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); client.Connect(); var command = $"ShowSearchTypeWindow-{typeName}"; Log.DevInfo($"Send message: {command}"); @@ -23,14 +26,13 @@ public static void OpenUnitySearchToolWindowWithType(string typeName) client.Flush(); if (showOnce == false) { - MessageBox.ShowInfo("Check Unity app :)", "SRD DEV"); + MessageBox.ShowInfo("Check Unity window:)", "SRD DEV"); showOnce = true; } } catch (Exception e) { Log.DevError($"Send message failed: {e}"); - Console.WriteLine(e); throw; } } diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ToUnity/ToUnityWindowFocusSwitch.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ToUnity/ToUnityWindowFocusSwitch.cs new file mode 100644 index 0000000..f697534 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/ToUnity/ToUnityWindowFocusSwitch.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using JetBrains.Application.Parts; +using JetBrains.ProjectModel; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.ToUnity; + +[SolutionComponent(Instantiation.DemandAnyThreadSafe)] +public class ToUnityWindowFocusSwitch +{ + public void SwitchToUnityApplication() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + Process.Start("osascript", "-e \"tell application \\\"Unity\\\" to activate\""); + } + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/TypeExtensions.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/TypeExtensions.cs new file mode 100644 index 0000000..3a94c7b --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/TypeExtensions.cs @@ -0,0 +1,9 @@ +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration; + +public static class TypeExtensions +{ + public static string MakeType(string typeName, string asmName) + { + return $"{typeName},{asmName}".Replace(" ", ""); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/KnownTypes.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/KnownTypes.cs new file mode 100644 index 0000000..47763c0 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/KnownTypes.cs @@ -0,0 +1,11 @@ +using JetBrains.Metadata.Reader.API; +using JetBrains.Metadata.Reader.Impl; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity; + +public class KnownTypes +{ + // UnityEngine.Serialization + public static readonly IClrTypeName MovedFromAttribute = + new ClrTypeName("UnityEngine.Scripting.APIUpdating.MovedFromAttribute"); +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/KnownTypesCache.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/KnownTypesCache.cs new file mode 100644 index 0000000..09ca34e --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/KnownTypesCache.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using JetBrains.Application.Parts; +using JetBrains.Metadata.Reader.API; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Modules; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity; + +[SolutionComponent(Instantiation.DemandAnyThreadSafe)] +public class KnownTypesCache +{ + private readonly ConcurrentDictionary myTypes = new(); + + public IDeclaredType GetByClrTypeName(IClrTypeName typeName, IPsiModule module) + { + // TODO: If/when Unity support nullability, add this as a parameter, and as a key to the cache + const NullableAnnotation nullableAnnotation = NullableAnnotation.Unknown; + + var type = module.GetPredefinedType().TryGetType(typeName, nullableAnnotation); + if (type != null) + return type; + + // Make sure the type is still valid before handing it out. It might be invalid if the module used to create + // it has been changed + type = myTypes.AddOrUpdate(typeName, name => TypeFactory.CreateTypeByCLRName(name, nullableAnnotation, module), + (name, existingValue) => existingValue.Module.IsValid() + ? existingValue + : TypeFactory.CreateTypeByCLRName(name, nullableAnnotation, module)); + return type; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/SRD/UnityProjectDetector.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/SRD/UnityProjectDetector.cs new file mode 100644 index 0000000..5bebc6d --- /dev/null +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/SRD/UnityProjectDetector.cs @@ -0,0 +1,52 @@ +using System; +using System.Linq; +using JetBrains.Application.Parts; +using JetBrains.ProjectModel; + +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity.SRD; + +[SolutionComponent(Instantiation.DemandAnyThreadSafe)] +public class UnityProjectDetector +{ + private readonly ISolution solution; + + //Can we update projects,assemblies, etc at runtime in current solution? + private bool? isUnityProject; + + public static UnityProjectDetector Instance { get; private set; } + + public UnityProjectDetector(ISolution solution) + { + this.solution = solution; + Instance = this; + } + + public bool IsUnityProject() + { + if (isUnityProject != null) + { + return isUnityProject.Value; + } + + try + { + foreach (var project in solution.GetAllProjects()) + { + var references = project.GetAllReferencedAssemblies().Select(r => r.Name); + if (references.Any(r => r.Contains("UnityEngine") || r.Contains("UnityEditor"))) + { + isUnityProject = true; + return true; + } + } + + isUnityProject = false; + } + catch (Exception e) + { + Log.DevError(e.ToString()); + } + + return false; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/DatabaseLoader.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/SRD/UnitySrdDatabaseLoader.cs similarity index 63% rename from src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/DatabaseLoader.cs rename to src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/SRD/UnitySrdDatabaseLoader.cs index 8f377a0..d184233 100644 --- a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/DatabaseLoader.cs +++ b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/Unity/SRD/UnitySrdDatabaseLoader.cs @@ -3,43 +3,93 @@ using System.Collections.Generic; using System.IO; using System.Threading.Tasks; +using JetBrains.Application.Notifications; +using JetBrains.Application.Parts; using JetBrains.Lifetimes; using JetBrains.ProjectModel; using JetBrains.Util; using Newtonsoft.Json.Linq; -namespace ReSharperPlugin.SerializeReferenceDropdownIntegration; +namespace ReSharperPlugin.SerializeReferenceDropdownIntegration.Unity.SRD; -public enum LoadResult +[SolutionComponent(Instantiation.DemandAnyThreadSafe)] +public class UnitySrdDatabaseLoader { - NoError, - NoDatabaseFile, - NoSRDPackage, - ErrorLoading -} + private enum LoadResult + { + NoError, + NoDatabaseFile, + NoSRDPackage, + ErrorLoading + } + + private const string DatabaseJsonName = "SerializeReference_ToolSearch_DataCacheFile.json"; -public class DatabaseLoader -{ - private readonly string databaseJsonName = "SerializeReference_ToolSearch_DataCacheFile.json"; - private readonly ISolution solution; + private readonly UserNotifications userNotifications; private readonly Lifetime lifetime; + private readonly ISolution solution; private readonly ConcurrentDictionary typesCount = new(); - private static bool isRunningUpdate; private DateTime lastDatabaseUpdate; + private DateTime lastDatabaseWriteTime; + private bool isRunningUpdate; - public DatabaseLoader(ISolution solution, Lifetime lifetime) + public IReadOnlyDictionary TypesCount => typesCount; + public bool IsAvailableDatabase { get; private set; } + + public UnitySrdDatabaseLoader(UserNotifications userNotifications, Lifetime lifetime, ISolution solution) { - this.solution = solution; + this.userNotifications = userNotifications; this.lifetime = lifetime; + this.solution = solution; } - public IReadOnlyDictionary TypesCount => typesCount; - public bool IsAvailableDatabase { get; private set; } - public DateTime LastDatabaseUpdate => lastDatabaseUpdate; + private string GetDatabaseJsonPath() + { + var jsonPath = Path.Combine(solution.SolutionDirectory.FullPath, "Library", DatabaseJsonName); + return jsonPath; + } - public async Task LoadDatabase() + private string GetPackagesJsonPath() + { + var jsonPath = Path.Combine(solution.SolutionDirectory.FullPath, "Packages", "packages-lock.json"); + return jsonPath; + } + + private async void LoadDatabase() + { + Log.DevInfo("Start load database"); + var result = await LoadDatabaseImpl(); + if (result == LoadResult.NoError) + { + var body = $"Loaded - {TypesCount.Count} types \n" + + $"Last refresh: {lastDatabaseWriteTime}"; + + userNotifications.CreateNotification(lifetime, NotificationSeverity.INFO, + "SRD - Database loaded", + body, closeAfterExecution: true); + + if ((DateTime.Now - lastDatabaseWriteTime).Days > 1) + { + userNotifications.CreateNotification(lifetime, NotificationSeverity.WARNING, + "SRD - Database need refresh?", + body, closeAfterExecution: true); + } + } + + if (result == LoadResult.NoDatabaseFile) + { + userNotifications.CreateNotification(lifetime, NotificationSeverity.WARNING, + "SRD - No Database File", + "Need generate database file", closeAfterExecution: true); + } + + Log.DevInfo($"End load database: {result}"); + } + + + private async Task LoadDatabaseImpl() { var jsonPath = GetDatabaseJsonPath(); if (File.Exists(jsonPath) == false) @@ -75,18 +125,13 @@ private void FillTypesFromPath(string path) foreach (var allType in allTypes) { var array = allType.Split(','); - var type = MakeType(array[0], array[1]); + var type = TypeExtensions.MakeType(array[0], array[1]); typesCount.TryGetValue(type, out var value); value++; typesCount[type] = value; } } - public static string MakeType(string typeName, string asmName) - { - return $"{typeName},{asmName}".Replace(" ", ""); - } - private async Task UpdateDatabaseImpl(string jsonPath) { isRunningUpdate = true; @@ -94,6 +139,7 @@ private async Task UpdateDatabaseImpl(string jsonPath) try { lastDatabaseUpdate = DateTime.Now; + lastDatabaseWriteTime = File.GetLastWriteTime(jsonPath); await Task.Run(() => FillTypesFromPath(jsonPath), lifetime); ok = true; } @@ -108,7 +154,7 @@ private async Task UpdateDatabaseImpl(string jsonPath) //TODO: make better bg update - public async void UpdateDatabaseBackground() + public async void RefreshDatabase() { var jsonPath = GetDatabaseJsonPath(); if (File.Exists(jsonPath)) @@ -126,7 +172,7 @@ public async void UpdateDatabaseBackground() } } - private static void FindObjectTypes(JToken token, string propertyName, ref List values) + private void FindObjectTypes(JToken token, string propertyName, ref List values) { if (token.Type == JTokenType.Object) { @@ -136,6 +182,7 @@ private static void FindObjectTypes(JToken token, string propertyName, ref List< { values.Add(prop.Value.ToString()); } + FindObjectTypes(prop.Value, propertyName, ref values); } } @@ -147,16 +194,4 @@ private static void FindObjectTypes(JToken token, string propertyName, ref List< } } } - - private string GetDatabaseJsonPath() - { - var jsonPath = Path.Combine(solution.SolutionDirectory.FullPath, "Library", databaseJsonName); - return jsonPath; - } - - private string GetPackagesJsonPath() - { - var jsonPath = Path.Combine(solution.SolutionDirectory.FullPath, "Packages", "packages-lock.json"); - return jsonPath; - } } \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/WindowFocusSwitch.cs b/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/WindowFocusSwitch.cs deleted file mode 100644 index a542867..0000000 --- a/src/dotnet/ReSharperPlugin.SerializeReferenceDropdownIntegration/WindowFocusSwitch.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Diagnostics; - -namespace ReSharperPlugin.SerializeReferenceDropdownIntegration; - -public static class WindowFocusSwitch -{ - public static void SwitchToUnityApplication() - { - SwitchOnMacOS(); - } - - private static void SwitchOnMacOS() - { - Process.Start("osascript", "-e \"tell application \\\"Unity\\\" to activate\""); - } -} \ No newline at end of file diff --git a/src/rider/main/resources/META-INF/plugin.xml b/src/rider/main/resources/META-INF/plugin.xml index a50f872..0660add 100644 --- a/src/rider/main/resources/META-INF/plugin.xml +++ b/src/rider/main/resources/META-INF/plugin.xml @@ -1,12 +1,10 @@ - + com.jetbrains.rider.plugins.serializereferencedropdownintegration SerializeReferenceDropdownIntegration - 1.1.1 + 1.1.2 Author Alexey Taranov com.intellij.modules.rider - Integration for unity package: SerializeReferenceDropdown

]]>
-