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
7 changes: 7 additions & 0 deletions CherryPick_Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,11 @@ public partial class CherryPick : ResoniteMod

[AutoRegisterConfigKey]
public static ModConfigurationKey<bool> ClearFocus = new("Clear focus", "When checked, the search buttons will clear the focus of the search bar.", () => true);

[AutoRegisterConfigKey]
public static ModConfigurationKey<string> UserExcludedCategories = new("Excluded categories", "Excludes specific categories from being searched into by path. Separate entries by semicolon.", () => "/ProtoFlux, /Example/Test");

[AutoRegisterConfigKey]
public static ModConfigurationKey<float> SearchRefreshDelay = new("Search refresh delay", "Time to wait after search input change before refreshing the results. 0 to always refresh.", () => .4f);

}
174 changes: 102 additions & 72 deletions CherryPicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FrooxEngine.UIX;
using static CherryPick.CherryPick;
using System.Runtime.CompilerServices;
using System;

namespace CherryPick;

Expand Down Expand Up @@ -67,7 +68,9 @@ public void PerformMatch(string query, int resultCount = 10)

string[] splitQuery = query.Split(' ');


string userexcludedcategories = Config!.GetValue(CherryPick.UserExcludedCategories).ToLower();
string[] UserExcludedCategories = userexcludedcategories.Replace(" ", null).Split(',');


// The for loops are a bit hot and can cause minor
// hitches if care isn't taken. Avoiding branch logic if possible
Expand All @@ -78,12 +81,15 @@ public void PerformMatch(string query, int resultCount = 10)
for (int i = 0; i < workerCount; i++)
{
WorkerDetails worker = details[i];
float ratio = MatchRatioInsensitive(worker.LowerName, splitQuery);
if (!UserExcludedCategories.Any(worker.Path.ToLower().Contains) || userexcludedcategories == "")
{
float ratio = MatchRatioInsensitive(worker.LowerName, splitQuery);

_results.Add(ratio, worker);
int detailCount = _results.Count;
_results.Add(ratio, worker);
int detailCount = _results.Count;

_results.RemoveAt(detailCount - 1);
_results.RemoveAt(detailCount - 1);
}
}
}
else
Expand All @@ -92,12 +98,16 @@ public void PerformMatch(string query, int resultCount = 10)
for (int i = 0; i < workerCount; i++)
{
WorkerDetails worker = details[i];
float ratio = worker.Path.StartsWith(searchScope) ? MatchRatioInsensitive(worker.LowerName, splitQuery) : 0f;

_results.Add(ratio, worker);
int detailCount = _results.Count;
if (!UserExcludedCategories.Any(worker.Path.ToLower().Replace("/protoflux", "").Contains) || userexcludedcategories == "")
{
float ratio = worker.Path.StartsWith(searchScope) ? MatchRatioInsensitive(worker.LowerName, splitQuery) : 0f;

_results.Add(ratio, worker);
int detailCount = _results.Count;

_results.RemoveAt(detailCount - 1);
_results.RemoveAt(detailCount - 1);
}
}
}

Expand Down Expand Up @@ -188,95 +198,115 @@ public void ForceEditFinished(TextEditor editor)
EditFinished(editor);
}



int searchDelayToken = 0;

public void EditChanged(TextEditor editor)
{
if (searchRoot == null ||
componentUIRoot == null ||
editor == null ||
onGenericPressed == null ||
onAddPressed == null ||
searchBuilder == null ||
!IsReady) // You can't search until the cache is built! This is fine in most cases, but if you end up searching before then, too bad!
return;
int SearchRefreshDelay = (int)(1000 * Config!.GetValue(CherryPick.SearchRefreshDelay));


string txt = editor.Text.Target.Text;
if (txt == null)
return;
editor.StartTask(async () =>
{
searchDelayToken++;
int LocalSearchDelayToken = searchDelayToken;

if (SearchRefreshDelay > 0)
{
await default(ToBackground);
await Task.Delay(SearchRefreshDelay);
await default(NextUpdate);
}

int genericStart = txt.IndexOf('<');
string? matchTxt = null;
string? genericType = null;
// Only refresh UI with search results if there was no further update immediately following it
if (searchDelayToken!= LocalSearchDelayToken)
return;

if (genericStart > 0)
{
matchTxt = txt.Substring(0, genericStart);
genericType = txt.Substring(genericStart);
}
else
{
matchTxt = txt;
}
if (searchRoot == null ||
componentUIRoot == null ||
editor == null ||
onGenericPressed == null ||
onAddPressed == null ||
searchBuilder == null ||
!IsReady) // You can't search until the cache is built! This is fine in most cases, but if you end up searching before then, too bad!
return;


string txt = editor.Text.Target.Text;
if (txt == null)
return;

searchRoot.DestroyChildren();
int resultCount = Config!.GetValue(ResultCount);
resultCount = MathX.Min(resultCount, MAX_RESULT_COUNT);

int genericStart = txt.IndexOf('<');
string? matchTxt = null;
string? genericType = null;

PerformMatch(matchTxt, resultCount);
foreach (var result in _results.Values)
{
bool isGenType = result.Type.IsGenericTypeDefinition;
string arg = "";

try
if (genericStart > 0)
{
arg = isGenType ? Path.Combine(result.Path, result.Type.AssemblyQualifiedName) : searchRoot.World.Types().EncodeType(result.Type);
matchTxt = txt.Substring(0, genericStart);
genericType = txt.Substring(genericStart);
}
catch (ArgumentException)
else
{
CherryPick.Warn($"Tried to encode a non-data model type: {result.Type}");
continue;
matchTxt = txt;
}

var pressed = isGenType ? onGenericPressed : onAddPressed;
CreateButton(result, pressed, arg, searchBuilder, editor, RadiantUI_Constants.Sub.CYAN);
}

searchRoot.DestroyChildren();
int resultCount = Config!.GetValue(ResultCount);
resultCount = MathX.Min(resultCount, MAX_RESULT_COUNT);

try
{
WorkerDetails firstGeneric = _results.Values.First(w => w.Type.IsGenericTypeDefinition);
if (genericType != null)

PerformMatch(matchTxt, resultCount);
foreach (var result in _results.Values)
{
string typeName = firstGeneric.Type.FullName;
typeName = typeName.Substring(0, typeName.IndexOf("`")) + genericType;
Type? constructed = NiceTypeParser.TryParse(typeName);
bool isGenType = result.Type.IsGenericTypeDefinition;
string arg = "";

try
{
arg = isGenType ? Path.Combine(result.Path, result.Type.AssemblyQualifiedName) : searchRoot.World.Types().EncodeType(result.Type);
}
catch (ArgumentException)
{
CherryPick.Warn($"Tried to encode a non-data model type: {result.Type}");
continue;
}

var pressed = isGenType ? onGenericPressed : onAddPressed;
CreateButton(result, pressed, arg, searchBuilder, editor, RadiantUI_Constants.Sub.CYAN);
}


if (constructed != null)
try
{
WorkerDetails firstGeneric = _results.Values.First(w => w.Type.IsGenericTypeDefinition);
if (genericType != null)
{
try
{
string arg = searchRoot.World.Types().EncodeType(constructed);
}
catch (ArgumentException)
string typeName = firstGeneric.Type.FullName;
typeName = typeName.Substring(0, typeName.IndexOf("`")) + genericType;
Type? constructed = NiceTypeParser.TryParse(typeName);


if (constructed != null)
{
CherryPick.Warn($"Tried to encode a non-data model type: {constructed}");
return;
try
{
string arg = searchRoot.World.Types().EncodeType(constructed);
}
catch (ArgumentException)
{
CherryPick.Warn($"Tried to encode a non-data model type: {constructed}");
return;
}

WorkerDetails detail = new(constructed.GetNiceName(), firstGeneric.Path, constructed);
Button typeButton = CreateButton(detail, onAddPressed, searchRoot.World.Types().EncodeType(constructed), searchBuilder, editor, RadiantUI_Constants.Sub.ORANGE);
typeButton.Slot.OrderOffset = -1024;
}

WorkerDetails detail = new(constructed.GetNiceName(), firstGeneric.Path, constructed);
Button typeButton = CreateButton(detail, onAddPressed, searchRoot.World.Types().EncodeType(constructed), searchBuilder, editor, RadiantUI_Constants.Sub.ORANGE);
typeButton.Slot.OrderOffset = -1024;
}
}
}
catch (InvalidOperationException) { } // Swallow this exception in particular because First() will throw if nothing satisfies the lambda condition
catch (InvalidOperationException) { } // Swallow this exception in particular because First() will throw if nothing satisfies the lambda condition
});
}


Expand Down