queryEngine | Query engine to add extended search value filters and parsers. |
Extended a query engine to work with dynamic search values.
Here's a few example of custom providers that use the extended search value when setting up a QueryEngine.
using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Search; using UnityEngine;
static class EasySearchProviderExample { enum ExampleProvider { win, material, shader, folder, component, project, res }
/// <summary> /// Search opened editor windows /// </summary> [SearchItemProvider] public static SearchProvider ExampleWindows() { return EasySearchProvider.Create(ExampleProvider.win.ToString(), _ => Resources.FindObjectsOfTypeAll<EditorWindow>()) .AddAction("select", win => win.Focus()) .AddFilter("floating", "Floating", win => !win.docked) .AddOption(ShowDetailsOptions.Inspector) .AddOption(EasyOptions.DisplayFilterValueInDescription) .AddByReflectionActions(); }
/// <summary> /// Search any loaded material and select them in the inspector. /// </summary> [SearchItemProvider] public static SearchProvider ExampleMaterials() { return EasySearchProvider.Create(_ => Resources.FindObjectsOfTypeAll<Material>()) .AddAction("select", o => Selection.activeObject = o) .AddOption(ShowDetailsOptions.Actions | ShowDetailsOptions.Inspector) .RemoveOption(ShowDetailsOptions.Description); }
/// <summary> /// Search any loaded shader. /// Select them in the inspector. /// View the shader source in the preview inspector. /// </summary> [SearchItemProvider] public static SearchProvider ExampleShaders() { static string ReadSource(Shader shader) { var shaderPath = AssetDatabase.GetAssetPath(shader); if (string.IsNullOrEmpty(shaderPath)) return ShaderUtil.GetShaderData(shader).ToString(); if (System.IO.File.Exists(shaderPath)) return $"<size=8>{System.IO.File.ReadAllText(shaderPath)}</size>"; return shaderPath; }
static string FetchShaderSource(Shader shader, SearchItemOptions options) { if ((options & SearchItemOptions.FullDescription) != 0) return ReadSource(shader) ?? shader.ToString(); return AssetDatabase.GetAssetPath(shader); }
return EasySearchProvider.Create(_ => Resources.FindObjectsOfTypeAll<Shader>()) .AddAction("select", o => Selection.activeObject = o) .AddOption(ShowDetailsOptions.Actions | ShowDetailsOptions.Inspector | ShowDetailsOptions.Description) .AddFilter("source", "Source Code", s => ReadSource(s)) .SetDescriptionHandler(FetchShaderSource); }
/// <summary> /// Search any active game object component. /// </summary> [SearchItemProvider] public static SearchProvider ExampleComponents() { return EasySearchProvider.Create(_ => Resources.FindObjectsOfTypeAll<Component>()) .SetDescriptionHandler((obj, options) => obj.GetType().FullName) .AddAction("ping", o => EditorGUIUtility.PingObject(o.gameObject)) .AddAction("select", o => Selection.activeObject = o) .AddOption(ShowDetailsOptions.Actions | ShowDetailsOptions.Inspector) .RemoveOption(ShowDetailsOptions.Description); }
/// <summary> /// Search all project folders. /// </summary> /// <returns></returns> [SearchItemProvider] public static SearchProvider ExampleFolders() { var folderIcon = EditorGUIUtility.FindTexture("Folder Icon"); return EasySearchProvider.Create(ExampleProvider.folder.ToString(), "Folders", _ => System.IO.Directory.EnumerateDirectories("Assets", "*", System.IO.SearchOption.AllDirectories).Select(d => d.Replace("
", "/"))) .SetThumbnailHandler(dir => folderIcon) .AddAction("open", dir => EditorUtility.RevealInFinder(dir)) .AddAction("select", dir => Selection.activeObject =AssetDatabase.LoadMainAssetAtPath(dir)) .AddOption(EasyOptions.DescriptionSameAsLabel | EasyOptions.SortByName); }
/// <summary> /// Search editor bundle resources. /// Note: that bundle resources are cached once. /// </summary> /// <returns></returns> [SearchItemProvider] public static SearchProvider ExampleEditorBundles() { Func<UnityEngine.Object, bool> FR = r => string.Equals(AssetDatabase.GetAssetPath(r), "Library/unity editor resources", StringComparison.Ordinal); return EasySearchProvider.Create(ExampleProvider.res.ToString(), "Resources", Resources.FindObjectsOfTypeAll<UnityEngine.Object>().Where(FR)) .SetDescriptionHandler(r => $"{r.GetType().FullName} ({r.GetInstanceID()})") .AddAction("select", o => Selection.activeObject = o) .AddAction("copy", "Copy Name", r => EditorGUIUtility.systemCopyBuffer = r.name) .AddOption(ShowDetailsOptions.Actions | ShowDetailsOptions.Inspector) .AddOption(EasyOptions.YieldAllItemsIfSearchQueryEmpty); }
[MenuItem("Window/Search/Easy")] public static void ShowProvider() { SearchService.ShowContextual(Enum.GetValues(typeof(ExampleProvider)).Cast<ExampleProvider>().Select(e => e.ToString()).ToArray()); } }
Easy search providers.
using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEditor; using UnityEditor.Search; using UnityEditor.Search.Providers; using UnityEngine;
[Flags] enum EasyOptions { None = 0,
YieldAllItemsIfSearchQueryEmpty = 1 << 5, DescriptionSameAsLabel = 1 << 6, SortByName = 1 << 7, DisplayFilterValueInDescription = 1 << 8, }
static class EasyOptionsExtensions { public static bool HasAny(this EasyOptions flags, in EasyOptions f) => (flags & f) != 0; public static bool HasAll(this EasyOptions flags, in EasyOptions all) => (flags & all) == all; }
static class EasySearchProvider { public static EasySearchProvider<T> Create<T>(string id, Func<SearchContext, IEnumerable<T>> fetchObjects) { return Create(id, null, fetchObjects); }
public static EasySearchProvider<T> Create<T>(Func<SearchContext, IEnumerable<T>> fetchObjects) { return Create(typeof(T).Name.ToLowerInvariant(), null, fetchObjects); }
public static EasySearchProvider<T> Create<T>(string id, string label, Func<SearchContext, IEnumerable<T>> fetchObjects) { return new EasySearchProvider<T>(id, label, fetchObjects); }
public static EasySearchProvider<T> Create<T>(string id, string label, IEnumerable<T> objects) { return new EasySearchProvider<T>(id, label, objects); } }
readonly struct EasyFilter { public readonly string name; public readonly string label; public readonly Func<object, object> func;
public EasyFilter(string name, string label, Func<object, object> func) { this.name = name; this.label = label; this.func = func; } }
class EasySearchProvider<T> : SearchProvider { private bool m_FiltersAdded; private EasyOptions m_Options; private QueryEngine<T> m_QueryEngine; private List<EasyFilter> m_Filters;
private T[] m_CachedObjects; private Func<SearchContext, IEnumerable<T>> m_FetchObjects;
public EasySearchProvider(string id, string displayName, IEnumerable<T> objects) : this(id, displayName, null as Func<SearchContext, IEnumerable<T>>) { m_CachedObjects = objects.ToArray(); m_FetchObjects = _ => m_CachedObjects; }
public EasySearchProvider(string id, string displayName, Func<SearchContext, IEnumerable<T>> fetchObjects) : base(id, displayName ?? ObjectNames.NicifyVariableName(typeof(T).Name)) { active = false; m_FetchObjects = fetchObjects; m_Filters = new List<EasyFilter>();
showDetails = false; showDetailsOptions = ShowDetailsOptions.None; fetchItems = (context, items, provider) => FetchItems(context); fetchPropositions = FetchPropositions; fetchDescription = FetchDescription; fetchThumbnail = FetchThumbnail; startDrag = StartDrag; toObject = ToObject;
m_QueryEngine = new QueryEngine<T>(); m_QueryEngine.SetSearchDataCallback(SearchWords, s => s.ToLowerInvariant(), StringComparison.Ordinal); SearchValue.SetupEngine(m_QueryEngine);
var dataType = typeof(T); if (!dataType.IsPrimitive && dataType != typeof(string)) { AddByReflectionFilters(); AddFilter("t", "Object Type", obj => obj.GetType().Name); } }
public EasySearchProvider<T> AddOption(in EasyOptions options) { m_Options |= options; return this; }
public EasySearchProvider<T> AddOption(ShowDetailsOptions showDetailsOptions) { this.showDetailsOptions |= showDetailsOptions; this.showDetails = this.showDetailsOptions != ShowDetailsOptions.None; return this; }
public EasySearchProvider<T> AddAction(string name, Action<T> handler) { actions.Insert(0, new SearchAction(id, name, new GUIContent(ObjectNames.NicifyVariableName(name)), (items) => ActionToDataHandler(items, handler))); return this; }
public EasySearchProvider<T> AddAction(string name, string label, Action<T> handler) { actions.Insert(Math.Min(1, actions.Count), new SearchAction(id, name, new GUIContent(label), (items) => ActionToDataHandler(items, handler))); return this; }
public EasySearchProvider<T> RemoveOption(ShowDetailsOptions showDetailsOptions) { this.showDetailsOptions &= ~showDetailsOptions; this.showDetails = this.showDetailsOptions != ShowDetailsOptions.None; return this; }
public EasySearchProvider<T> SetDescriptionHandler(Func<T, SearchItemOptions, string> handler) { fetchDescription = (item, context) => FetchObjectDescription(item, context, handler); return AddOption(ShowDetailsOptions.Description); }
public EasySearchProvider<T> SetDescriptionHandler(Func<T, string> handler) { return SetDescriptionHandler((T o, SearchItemOptions options) => handler(o)); }
public EasySearchProvider<T> SetThumbnailHandler(Func<T, Texture2D> handler) { this.fetchThumbnail = (item, context) => FetchObjectThumbnail(item, context, handler); return this; }
public EasySearchProvider<T> AddFilter<TResult>(string filter, in string label, Func<T, TResult> func) { if (m_Filters.Any(f => string.Equals(f.name, filter, StringComparison.Ordinal))) return this;
m_Filters.Add(new EasyFilter(filter, label, o => func((T)o))); m_QueryEngine.AddFilter(filter, func); return this; }
public EasySearchProvider<T> AddByReflectionActions() { foreach (var m in typeof(T).GetMethods().OrderBy(m => m.Name)) { if (m.GetParameters().Length > 0 || m.IsStatic) continue;
actions.Add(new SearchAction(id, m.Name) { handler = item => HandleMethod(item, m), closeWindowAfterExecution = false }); } return this; }
void StartDrag(SearchItem item, SearchContext context) { var data = (T)item.data; DragAndDrop.PrepareStartDrag(); if (data is UnityEngine.Object uo) DragAndDrop.objectReferences = new [] { uo }; else DragAndDrop.SetGenericData(item.data.GetType().Name, item.data); DragAndDrop.StartDrag(GetName(data)); }
string FetchObjectDescription(SearchItem item, SearchContext context, Func<T, SearchItemOptions, string> handler) { if (item.data is T obj) return handler(obj, item.options); return null; }
Texture2D FetchObjectThumbnail(SearchItem item, SearchContext context, Func<T, Texture2D> handler) { if (item.data is T obj) return handler(obj); return null; }
void ActionToDataHandler(SearchItem[] items, Action<T> handler) { foreach (var item in items) { if (item.data is T obj) handler(obj); } }
void AddByReflectionFilters() { foreach (var prop in typeof(T).GetProperties()) { var type = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; var filterName = ObjectNames.NicifyVariableName(prop.Name).Replace(" ", "").ToLowerInvariant(); AddFilter(filterName, $"C# {type.Name}", obj => FetchPropertyValue(obj, prop)); }
foreach (var prop in typeof(T).GetFields()) { var type = Nullable.GetUnderlyingType(prop.FieldType) ?? prop.FieldType; var filterName = ObjectNames.NicifyVariableName(prop.Name).Replace(" ", "").ToLowerInvariant(); AddFilter(filterName, $"C# {type.Name}", obj => FetchPropertyValue(obj, prop)); } }
IEnumerable<SearchProposition> FetchPropositions(SearchContext context, SearchPropositionOptions options) { foreach (var f in m_Filters) yield return new SearchProposition(f.name, f.name, f.label); }
IEnumerable<string> SearchWords(T obj) { yield return GetName(obj).ToLowerInvariant(); yield return obj.ToString().ToLowerInvariant(); }
string GetName(T obj) { if (TryGetProperty(obj, "name", out var objName) && objName is string ons && !string.IsNullOrEmpty(ons)) return ons.ToString(); return obj.ToString(); }
bool TryGetProperty(in T obj, string name, out object value) { var p = obj.GetType().GetProperty(name); if (p != null) { value = p.GetValue(obj); return true; }
value = null; return false; }
IEnumerable<SearchItem> FetchItems(SearchContext context) { if (string.IsNullOrEmpty(context.searchQuery)) { if (HasOption(EasyOptions.YieldAllItemsIfSearchQueryEmpty)) return SortObjects(m_FetchObjects(context)).Select((o, index) => CreateItem(context, GetName(o), index, o)); return Enumerable.Empty<SearchItem>(); }
if (!m_FiltersAdded) AddFilters(m_FetchObjects(context).FirstOrDefault());
var query = m_QueryEngine.Parse(context.searchQuery); if (!query.valid) { context.AddSearchQueryErrors(query.errors.Select(e => new SearchQueryError(e, context, this))); return Enumerable.Empty<SearchItem>(); }
return SearchItems(context, query); }
string FetchDescription(SearchItem item, SearchContext context) { if ((item.options & SearchItemOptions.Compacted) != 0 || m_Options.HasAny(EasyOptions.DescriptionSameAsLabel)) return item.GetLabel(context);
if (m_Options.HasAny(EasyOptions.DisplayFilterValueInDescription)) { var description = ""; foreach (var f in m_Filters) { if (context.searchQuery.LastIndexOf(f.name) == -1) continue;
if (description.Length != 0) description += ", "; description += $"{f.name}={f.func(item.data)}"; }
return description; }
return item.data.ToString(); }
private Texture2D FetchThumbnail(SearchItem item, SearchContext context) { if (item.data is UnityEngine.Object uo) return AssetPreview.GetMiniThumbnail(uo); return EditorGUIUtility.FindTexture(item.data.GetType().Name); }
private UnityEngine.Object ToObject(SearchItem item, Type type) { return item.data as UnityEngine.Object; }
IEnumerable<T> SortObjects(IEnumerable<T> items) { if (m_Options.HasAny(EasyOptions.SortByName)) items = items.OrderBy(e => GetName(e)); return items; }
IEnumerable<SearchItem> SearchItems(SearchContext context, Query<T> query) { int index = 0; foreach (var o in SortObjects(m_FetchObjects(context))) { if (o == null || o.Equals(default(T))) { yield return null; continue; }
if (!query.Test(o)) { yield return null; continue; }
var score = index++; var name = GetName(o); if (!m_Options.HasAny(EasyOptions.SortByName)) score = ComputeScore(name); yield return CreateItem(context, name, score, o); } }
void AddFilters(T o) { if (o == null) return;
if (o is UnityEngine.Object uo) { using (var so = new SerializedObject(uo)) { var p = so.GetIterator(); var next = p.NextVisible(true); while (next) { var propertyPath = p.propertyPath; var filterName = ObjectNames.NicifyVariableName(propertyPath).Replace(" ", "").ToLowerInvariant();
AddFilter(filterName, $"{p.displayName} ({p.propertyType})", obj => FetchPropertyValue(obj, propertyPath)); next = p.NextVisible(p.hasChildren); } } }
m_FiltersAdded = true; }
private void HandleMethod(SearchItem item, MethodInfo mi) { var result = mi.Invoke(item.data, null); var unityObject = item.data as UnityEngine.Object; if (result != null) Debug.Log(result, unityObject); else Debug.Log($"Executed {mi.DeclaringType.FullName}.{mi.Name}", unityObject); }
SearchValue FetchPropertyValue(in T obj, in FieldInfo prop) => new SearchValue(prop.GetValue(obj)); SearchValue FetchPropertyValue(in T obj, in PropertyInfo prop) => new SearchValue(prop.GetValue(obj));
SearchValue FetchPropertyValue(in T obj, in string propertyPath) { if (obj is UnityEngine.Object uo) { using (var so = new SerializedObject(uo)) { var p = so.FindProperty(propertyPath); if (p == null) return SearchValue.invalid; return SearchValue.ConvertPropertyValue(p); } }
return SearchValue.invalid; }
SearchItem CreateItem(in SearchContext context, in string name, in int score, in T o) { if (o == null) return null; return CreateItem(context, Guid.NewGuid().ToString("N"), score, name, null, null, o); }
int ComputeScore(in string name) { if (name.Length > 2) { var sp = Math.Max(0, name.LastIndexOf('/')); if (sp + 2 < name.Length) return name[sp] * 5 + name[sp + 1] * 2 + name[sp + 2]; } return 99; }
bool HasOption(in EasyOptions option) { return m_Options.HasAny(option); } }