Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion example-mod/.idea/.idea.AshAndDust/.idea/.name

This file was deleted.

4 changes: 0 additions & 4 deletions example-mod/.idea/.idea.AshAndDust/.idea/encodings.xml

This file was deleted.

8 changes: 0 additions & 8 deletions example-mod/.idea/.idea.AshAndDust/.idea/indexLayout.xml

This file was deleted.

6 changes: 0 additions & 6 deletions example-mod/.idea/.idea.AshAndDust/.idea/vcs.xml

This file was deleted.

4 changes: 2 additions & 2 deletions example-mod/Source/AshAndDust.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@
<Private>False</Private>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>D:\SteamLibrary\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\Assembly-CSharp.dll</HintPath>
<HintPath>..\..\..\RimWorldWin64_Data\Managed\Assembly-CSharp.dll</HintPath>
<Private>False</Private>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
<HintPath>D:\SteamLibrary\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
<HintPath>..\..\..\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using JetBrains.Application.DataContext;
using JetBrains.Application.Help;
using JetBrains.Application.Progress;
using JetBrains.Application.UI.Actions.ActionManager;
using JetBrains.DataFlow;
using JetBrains.IDE.UI;
using JetBrains.IDE.UI.Extensions;
using JetBrains.Lifetimes;
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Feature.Services.ContextActions;
using JetBrains.ReSharper.Feature.Services.CSharp.ContextActions;
using JetBrains.ReSharper.Feature.Services.Refactorings;
using JetBrains.ReSharper.Feature.Services.UI.Validation;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree;
using JetBrains.ReSharper.Psi.Files;
using JetBrains.ReSharper.Psi.Paths;
using JetBrains.ReSharper.Psi.Tree;
using JetBrains.ReSharper.Psi.Util;
using JetBrains.ReSharper.Psi.Xml.Impl.Tree;
using JetBrains.ReSharper.Psi.Xml.Parsing;
using JetBrains.ReSharper.Psi.Xml.Tree;
using JetBrains.Rider.Model.UIAutomation;
using JetBrains.TextControl;
using JetBrains.Util;

namespace ReSharperPlugin.RimworldDev.ContextActions;

[ContextAction(Description = "Move to Keyed Translation", GroupType = typeof (CSharpContextActions), Name = "StringToKeyedTranslation",
Priority = 1)]
public class StringToKeyedTranslationAction(ICSharpContextActionDataProvider provider) : ContextActionBase
{
public override string Text => "Move to Keyed Translation";

protected override Action<ITextControl> ExecutePsiTransaction(ISolution solution, IProgressIndicator progress)
{
var selectedElement = provider.GetSelectedElement<IStringLiteralOwner>();

return (_) =>
{
using var lifetimeDefinition = Lifetime.Define(Lifetime.Eternal);
var withDataRules = JetBrains.ReSharper.Resources.Shell.Shell.Instance.GetComponent<IActionManager>()
.DataContexts.CreateWithDataRules(lifetimeDefinition.Lifetime);

var workflow = new StringToKeyedTranslationWorkflow(solution, null, selectedElement);

RefactoringActionUtil.ExecuteRefactoring(withDataRules, workflow);
};
}

public override bool IsAvailable(IUserDataHolder cache)
{
if (!ScopeHelper.IsRimworldProject())
return false;

var selectedElement = provider.GetSelectedElement<IStringLiteralOwner>();

if (selectedElement is null) return false;
if (selectedElement.Parent is IReferenceExpression
{
LastChild: IIdentifier { Name: "Translate" }
}) return false;

if (selectedElement is IInterpolatedStringExpression interpolatedStringExpression)
{
return !interpolatedStringExpression.Inserts.Any(insert => insert.Expression is not IReferenceExpression);
}

return selectedElement is ICSharpLiteralExpression;
}
}

public class StringToKeyedTranslationWorkflow([NotNull] ISolution solution, [CanBeNull] string actionId, ITreeNode selectedElement)
: DrivenRefactoringWorkflow(solution, actionId)

Check warning on line 80 in src/dotnet/ReSharperPlugin.RimworldDev/ContextActions/StringToKeyedTranslationAction.cs

View workflow job for this annotation

GitHub Actions / Build

Parameter 'ISolution solution' is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well.
{
public StringToKeyedTranslationDataModel DataModel;
public StringToKeyedTranslationDataProvider DataProvider = new ("");

public override bool Initialize(IDataContext context)
{
return true;
}

public override bool IsAvailable(IDataContext context)
{
if (!ScopeHelper.IsRimworldProject())
return false;

return true;
}

public override HelpId HelpKeyword => HelpId.Empty;

public override IRefactoringPage FirstPendingRefactoringPage => new StringToKeyedTranslationRefactoringPage(this);

public override bool MightModifyManyDocuments => true;
public override string Title => "Move to Keyed Translation";
public override RefactoringActionGroup ActionGroup => RefactoringActionGroup.Convert;

public override IRefactoringExecuter CreateRefactoring(IRefactoringDriver driver)
{
DataModel = new StringToKeyedTranslationDataModel(selectedElement);

return new StringToKeyedTranslationRefactoring(this, solution, driver);
}
}

public class StringToKeyedTranslationDataModel(ITreeNode stringExpression) : IDataModel
{
public ITreeNode StringExpression { get; } = stringExpression;
}

public class StringToKeyedTranslationDataProvider(string name) : IDataProvider
{
public string Name = name;

public bool NonInteractive => true;
}

public class StringToKeyedTranslationRefactoringPage : SingleBeRefactoringPage
{
private readonly BeGrid content;
private StringToKeyedTranslationWorkflow workflow;
public IProperty<string> Name { get; }

public StringToKeyedTranslationRefactoringPage(StringToKeyedTranslationWorkflow workflow) : base(workflow.WorkflowExecuterLifetime)
{
this.workflow = workflow;

Name = new Property<string>( "StringToKeyedTranslation.Name", "");

var component = workflow.GetComponent<IconHostBase>();
var nameControl = Name.GetBeTextBox(Lifetime).WithTextNotEmpty(
Lifetime,
ValidationStates.validationError.GetIcon(component)
);

content = new BeControl[1]
{
nameControl.WithDescription("Key Name", Lifetime)
}.GetGrid();
}

public override BeControl GetPageContent() => content;

public override void Commit()
{
workflow.DataProvider = new StringToKeyedTranslationDataProvider(Name.Value);
}
}

// TODO: If the file isn't referencing Verse yet, we need to insert that reference
// TODO: Allow the user to select the file they want to add the key to
// TODO: Add an option to insert the XML into all languages defined, not just the default language
public class StringToKeyedTranslationRefactoring(
[NotNull] StringToKeyedTranslationWorkflow workflow,
[NotNull] ISolution solution,
[NotNull] IRefactoringDriver driver)
: DrivenRefactoringBase<StringToKeyedTranslationWorkflow>(workflow, solution, driver)
{
public override bool Execute(IProgressIndicator pi)
{
var selectedElement = Workflow.DataModel.StringExpression;
if (selectedElement is not ICSharpExpression csharpExpression) return false;

var textToTranslate = "";
var arguments = new List<string>();

if (selectedElement is ICSharpLiteralExpression literalExpression)
{
textToTranslate = literalExpression.GetUnquotedText();
}

if (selectedElement is IInterpolatedStringExpression interpolatedStringExpression)
{
if (!Regex.IsMatch(interpolatedStringExpression.GetUnquotedText(), "^\\$\".*"))
return false;

textToTranslate = Regex.Replace(interpolatedStringExpression.GetUnquotedText(), "^\\$\"(.*)$", "$1");

arguments = interpolatedStringExpression
.Inserts
.Select(insert =>
{
if (insert.Expression is not IReferenceExpression referenceExpression)
return "";

return insert.GetText();
})
.Where(argument => !string.IsNullOrEmpty(argument))
.Distinct()
.ToList();
}

var translationKey = Workflow.DataProvider.Name;

var invocation = CSharpElementFactory
.GetInstance(csharpExpression)
.CreateExpression($"\"$0\".Translate({String.Join(", ", arguments)})", Workflow.DataProvider.Name);

var rimworldProject = Solution
.GetTopLevelProjects()
.FirstOrDefault(project => project.ProjectFileLocation.FullPath.EndsWith("About.xml"));

if (rimworldProject == null)
return false;

var languagesFolder = rimworldProject.GetSubItems().FirstOrDefault(item => item.Name == "Languages");
if (languagesFolder == null)
return false;

var translationFiles = rimworldProject
.GetAllProjectFiles()
.Where(file => file.Location.FullPath.StartsWith(languagesFolder.Location.FullPath));

var firstFile = translationFiles.FirstOrDefault();
if (firstFile == null)
return false;

var psiFile = rimworldProject.GetPsiSourceFileInProject(firstFile.Location).GetPrimaryPsiFile();
if (psiFile is not XmlFile xmlFile)
return false;

var languageDataTag = xmlFile.GetNestedTags<IXmlTag>("LanguageData").FirstOrDefault();
if (languageDataTag == null)
return false;

var lastTag = languageDataTag.Children().Last(tag => tag is IXmlTag);

for (var index = 0; index < arguments.Count; index++)
{
var argument = arguments[index];

textToTranslate = textToTranslate.FullReplace("{" + argument + "}", "{" + index + "}");
}

var newTag = XmlElementFactory.GetInstance(languageDataTag).CreateTagForTag(languageDataTag, $"<{translationKey}>{textToTranslate}</{translationKey}>");

csharpExpression.ReplaceBy(invocation);
ModificationUtil.AddChildAfter(languageDataTag, lastTag, newTag);

return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@

public override bool IsAvailable(IDataContext dataContext)
{
if (!ScopeHelper.IsRimworldProject()) return false;

var solution = dataContext.GetData(ProjectModelDataConstants.SOLUTION);
if (solution == null)
return false;
Expand All @@ -57,7 +59,7 @@
}
public override bool IsEnabled(ITreeNode context)
{
return true;
return ScopeHelper.IsRimworldProject();
}

public override bool IsEmptyInputAllowed(IGeneratorContext context)
Expand All @@ -71,6 +73,8 @@
{
protected override bool IsAvailable(GeneratorContextBase context)
{
if (!ScopeHelper.IsRimworldProject()) return false;

var anchor = context.Anchor;

if (anchor is not XmlWhitespaceToken) return false;
Expand All @@ -86,7 +90,7 @@
return true;
}

protected override void Process(GeneratorContextBase context)

Check warning on line 93 in src/dotnet/ReSharperPlugin.RimworldDev/Generator/DefGenerator.cs

View workflow job for this annotation

GitHub Actions / Build

Member 'DefPropertiesGeneratorBuilderXml.Process(GeneratorContextBase)' overrides obsolete member 'GeneratorBuilderBase<GeneratorContextBase>.Process(GeneratorContextBase)'. Add the Obsolete attribute to 'DefPropertiesGeneratorBuilderXml.Process(GeneratorContextBase)'.
{
var anchor = context.Anchor;
if (anchor is not ITreeNode) return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure;
using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure.LookupItems;
using JetBrains.ReSharper.Feature.Services.CSharp.CodeCompletion.Infrastructure;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.CSharp.Impl.Tree;
using JetBrains.ReSharper.Psi.CSharp.Parsing;
using JetBrains.ReSharper.Psi.CSharp.Tree;
using JetBrains.ReSharper.Psi.Tree;
using ReSharperPlugin.RimworldDev.SymbolScope;
using ReSharperPlugin.RimworldDev.TypeDeclaration;

namespace ReSharperPlugin.RimworldDev.ItemCompletion;

[Language(typeof(CSharpLanguage))]
public class RimworldKeyedTranslationsCSharpItemProvider: ItemsProviderOfSpecificContext<CSharpCodeCompletionContext>
{
private static RimworldCSharpLookupFactory LookupFactory = new();

protected override bool IsAvailable(CSharpCodeCompletionContext context)
{
var node = context.NodeInFile;
if (!node.Language.IsLanguage(CSharpLanguage.Instance)) return false;
if (node is not CSharpGenericToken) return false;
if (node.NodeType != CSharpTokenType.STRING_LITERAL_REGULAR) return false;

return true;
}

protected override bool AddLookupItems(CSharpCodeCompletionContext context, IItemsCollector collector)
{
var node = context.NodeInFile;

if (node.Parent?.NextSibling?.NextSibling is not ICSharpIdentifier identifier ||
identifier.GetText() != "Translate")
return false;

var xmlSymbolTable = context.NodeInFile.GetSolution().GetComponent<RimworldKeyedTranslationSymbolScope>();

foreach (var key in xmlSymbolTable.GetKeys())
{
var nullableKeyTag = xmlSymbolTable.GetTranslationKey(key);
if (!nullableKeyTag.HasValue) continue;
var keyTag = nullableKeyTag.Value;

var lookup = LookupFactory.CreateDeclaredElementLookupItem(
context,
key,
new DeclaredElementInstance(new XMLTagDeclaredElement(keyTag.Tag, $"{keyTag.Language}/{keyTag.KeyName}", false))
);

collector.Add(lookup);
}

return base.AddLookupItems(context, collector);
}

}
Loading
Loading