This commit is contained in:
2021-06-13 10:28:03 +02:00
parent eb70603c85
commit df2d24cbd3
7487 changed files with 943244 additions and 0 deletions

View File

@@ -0,0 +1,312 @@
# Changelog
## [4.3.2] - 2021-02-16
- Fixed bug that causes searcher window to close when double-clicking a category [case 1302267]
- Fixed bug that causes searcher window to be offset too far when accounting for host window boundaries
## [4.3.1] - 2020-06-08
- Fix bug that cause keyboard navigation to fail. [case 1253544]
## [4.3.0] - 2020-05-27
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.3.0-preview.tgz
- Bump minor version for synonyms.
## [4.2.0] - 2020-04-30
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.2.0-preview.tgz
- Bump to minor version for API validation.
## [4.1.1] - 2020-04-30
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.1.1-preview.tgz
- Add all children is now an adapter override.
## [4.1.0] - 2020-03-20
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.1.0-preview.tgz
- Improve matching algorithm
- Add a splitter between searcher and details panel
- Fix adding all children of matching expanded categories
## [4.0.9] - 2019-10-22
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.0.9-preview.tgz
- Update ListView API
## [4.0.8] - 2019-09-16
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.0.8-preview.tgz
- Made SearcherItem Name property virtual
## [4.0.7] - 2019-08-29
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.0.7-preview.tgz
- Fix bold fonts (case 1178374)
- case 1178373 and 1071573014: minor examples tweaks
## [4.0.6] - 2019-08-01
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.0.6-preview.tgz
- Fix bug where items were selected twice when using keyboard inputs
## [4.0.5] - 2019-07-26
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.0.5-preview.tgz
- Fix searcher look to match Northstar changes
## [4.0.4] - 2019-07-23
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.0.4-preview.tgz
- Change the default size when the searcher has a details panel
## [4.0.3] - 2019-06-11
Package: https://artifactory.prd.cds.internal.unity3d.com/artifactory/upm-candidates/com.unity.searcher/-/com.unity.searcher-4.0.3-preview.tgz
- Added ability to use capital letters in a search bar
- Bugfix: Search bar focus after the escape button pressed
## [4.0.2] - 2019-05-24
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/4.0.2-preview
- API Make Match() virtual again in SearcherDatabase
## [4.0.1] - 2019-04-30
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/4.0.1-preview
- Bugfix: [MacOs] Fix issue where the searcher moves on the top left corner while resizing/moving
## [4.0.0] - 2019-04-24
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/4.0.0-preview
- Cleanup for promotion to production
## [3.0.12] - 2019-04-17
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.12
- API: Make SearcherField public again
## [3.0.11] - 2019-04-15
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.11
- Fix all issues flagged by ReSharper
## [3.0.10] - 2019-03-26
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.10
- fix CI
## [3.0.9] - 2019-03-24
Package: none
- Add Yamato CI config
## [3.0.8] - 2019-03-24
Package: none
- Bugfix: Autocomplete text was misaligned.
## [3.0.7] - 2019-02-28
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.7
- API: Remove Experimental API reference.
## [3.0.6] - 2019-09-27
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.6
- UI: Restyling
- API: Add public ctor to SearcherDatabase
## [3.0.5] - 2018-12-18
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.5
- Bugfix: Focus search text field when window is displayed
## [3.0.4] - 2018-11-30
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.4
- Trigger callback when an item is selected instead of when the details panel is displayed
## [3.0.3] - 2018-11-28
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.3
- Add alignments
## [3.0.2] - 2018-11-22
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.2
- Bugfix: Searcher autocomplete label now bold to match text input style
## [3.0.1] - 2018-11-20
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.1
- Bugfix
## [3.0.0] - 2018-11-20
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/3.0.0
- Restyling and move + resize
## [2.1.1] - 2018-11-12
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.1.1
- Fix text input filtering
## [2.1.0] - 2018-11-05
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.1.0
- UIElements compatibility update
## [2.0.6] - 2018-08-15
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.0.6-preview
- Add possibility to sort items
## [2.0.5] - 2018-08-15
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.0.5-preview
- Filtering fix
## [2.0.4] - 2018-08-08
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.0.4-preview
- Added hooks for analytics
## [2.0.3] - 2018-08-07
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.0.3-preview
- The matchFilter is now also applied at database initial setup time
## [2.0.2] - 2018-08-02
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.0.2-preview
- Added matchFilter delegate on SearcherDatabase to further control the match criteria
## [2.0.1] - 2018-07-13
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.0.1-preview
- Fixed Exception when a whitespace query is entered
## [2.0.0] - 2018-07-12
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/2.0.0-preview
- Created a base class for Databases, renamed SearcherDatabase to LuceneDatabase, add a brand new SearcherDatabase written from scratch
## [1.0.6] - 2018-06-21
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/1.0.6-preview
- hotfix for left arrow on a child that cannot be collapsed will select the parent feature
## [1.0.5] - 2018-06-21
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/1.0.5-preview
- fixed an draw issue when expanding and collapsing an item on a small list - issue #25
- pressing left arrow on a child that cannot be collapsed will select the parent
## [1.0.4] - 2018-05-16
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/1.0.4-preview
- fixed compilation error with latest trunk (around styles.flex)
- added third party notices file
## [1.0.3] - 2018-05-03
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/1.0.3-preview
- window close due to focus loss will now trigger the selection callback with null
- fixed potential null ref exception in sample code
## [1.0.2] - 2018-04-30
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/1.0.2-preview
- removed AutoCompleter in favor of a more robust top-result based approach
## [1.0.1] - 2018-04-26
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/1.0.1-preview
- now showing children of matching items - issue #19
- fixed completion scoring with multiple databases
- search results in general have been improved
## [1.0.0] - 2018-04-25
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/1.0.0-preview
- added basic tests - issue #18
- added a README and documentation
- fixed Searcher.Search() not returning anything if query contained capital letters - issue #22
## [0.1.3] - 2018-04-23
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/0.1.3-preview
- added ability to add a title to the Searcher window - feature #3
- removed Searcher arrow and moved default display point to top-right corner - related issues #2, #12, #16
- fixed lingering arrow when bring Searcher window up from Inspector - issue #2
- fixed SearcherWindow.Show() to always take world space display location - issue #17
## [0.1.2] - 2018-04-18
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/0.1.2-experimental
- fixed Searcher's list is visually cut off when closing a parent SearcherItem - issue #9
- scroll to selected item/best result
- add parents field, do not autocomplete it, search using a multi phrase query, auto create the parents path in overwritePath()
- fixed window arrow being removed AFTER the target window repaint, leaving remnant arrwos sometimes - issue #6
- fixed Null Ref Exception when getting the selected item of an empty listview. only get it if relevant
- fixed bug where child was not under parent in Searcher
## [0.1.1] - 2018-03-21
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/0.1.1-experimental
- Minor fixes for VisualScripting
## [0.1.0] - 2018-03-05
Package: https://bintray.com/unity/unity-staging/com.unity.searcher/0.1.0-experimental
### This is the first release of *Unity Package Searcher*.
General search window for use in the Editor. First target use is for GraphView node search.

View File

@@ -0,0 +1,4 @@
using System.Runtime.CompilerServices;
using UnityEngine;
[assembly: InternalsVisibleTo("Unity.Searcher.EditorSamples")]

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,231 @@
#windowMainVisualContainer {
flex: 1;
flex-direction: row;
background-color: #272727;
}
#windowMainVisualContainer * {
-unity-font: resource("FlatSkin/Font/Roboto-Regular");
}
#windowResizer {
position: absolute;
height: 20px;
width: 20px;
bottom: 0;
right: 0;
padding-left: 10px;
padding-top: 14px;
cursor: resize-up-left;
}
#splitter
{
flex-direction:row;
position: absolute;
right:0;
top:0;
left:0;
bottom:0;
}
#windowResizerIcon {
flex: 1;
background-image:resource("WindowBottomResize");
cursor: resize-up-left;
}
#searcherVisualContainer {
flex: 1;
}
#windowTitleLabel {
font-size: 14px;
color: #FFFFFF;
margin-top: 5px;
margin-left: 6px;
}
#windowSearchBoxVisualContainer {
height: 21px;
background-color: #454545;
margin-right: 3px;
margin-left: 5px;
margin-top: 0;
margin-bottom: 6px;
border-radius: 6px;
border-color: #323232;
border-width: 1px;
}
#searchIcon {
width: 32px;
height: 16px;
background-image:resource("FlatSkin/SearchSmallDownOff");
top: 2px;
left: 4px;
margin-right: 2px;
}
#searchIcon.Active {
background-image:resource("FlatSkin/SearchSmallDownOn");
}
#autoCompleteLabel {
position: absolute;
background-image: none;
top: 2px;
left: 36px;
font-size: 12px;
color: #828282;
-unity-text-align: middle-left;
}
#searchBox {
background-image: none;
position: absolute;
left: 30px;
right: 0;
bottom: 0;
margin-bottom: 2px;
margin-top: 2px;
font-size: 12px;
color: #FFFFFF;
border-color: rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
}
#searchBox .unity-base-field__input {
top: 2px;
font-size: 12px;
-unity-text-align: middle-left;
color: #FFFFFF;
background-image: none;
border-color: rgba(0, 0, 0, 0);
background-color: rgba(0, 0, 0, 0);
}
#windowSelectionVisualContainer {
flex-direction: row;
flex: 1;
}
.unity-list-view.focusableScrollView {
flex: 1;
}
.unity-list-view {
background-color: #383838;
--unity-item-height: 18;
}
.unity-list-view > * {
left: 15px;
}
.unity-list-view > * #labelsContainer {
flex-direction: row;
flex: 1;
left: 8px;
top: -2px;
}
.unity-list-view > * #labelsContainer > .unity-label {
margin-left: 0;
margin-right: 0;
padding-left: 0;
padding-right: 0;
color: #C4C4C4;
font-size: 12px;
}
.unity-list-view > * > .unity-label {
flex: 1;
}
#smartSearchItem > #itemMainVisualContainer > #labelsContainer > .unity-label.Highlighted {
color: #FFCD62;
}
.unity-list-view > * > #smartSearchItem:hover * {
background-color: #424242;
}
.unity-list-view > * > #smartSearchItem.unity-list-view__item--selected * {
background-color: #2B5D87;
}
#smartSearchItem.unity-list-view__item--selected > #itemMainVisualContainer > #labelsContainer > .unity-label {
color : #FFFFFF;
}
#smartSearchItem.unity-list-view__item--selected > #itemMainVisualContainer > #labelsContainer > .unity-label.Highlighted {
color: #FFCD00;
}
.unity-list-view > * #itemChildExpander {
width: 18px;
height: 18px;
}
.unity-list-view > * #itemChildExpander > #expanderIcon {
width: 13px;
height: 13px;
top: 1px;
left: 10px;
}
#smartSearchItem > * > #itemChildExpander > #expanderIcon.Collapsed {
background-image: resource("Builtin Skins/DarkSkin/Images/IN foldout.png");
}
#smartSearchItem > * > #itemChildExpander > #expanderIcon.Expanded {
background-image: resource("Builtin Skins/DarkSkin/Images/IN foldout on.png");
}
#smartSearchItem.unity-list-view__item--selected > * > #itemChildExpander > #expanderIcon.Collapsed {
background-image: resource("Builtin Skins/DarkSkin/Images/IN foldout act.png");
}
#smartSearchItem.unity-list-view__item--selected > * > #itemChildExpander > #expanderIcon.Expanded {
background-image: resource("Builtin Skins/DarkSkin/Images/IN foldout act on.png");
}
.unity-list-view > * #itemMainVisualContainer {
flex-direction: row;
flex: 1;
}
.unity-list-view > *.unity-list-view__item--selected #textLabel {
color: #FFFFFF;
}
.unity-list-view > * #itemMainVisualContainer {
padding-top: 3px;
align-items: center;
}
.unity-list-view > *.Category #textLabel {
font-size: 12px;
color: #676767;
}
#windowDetailsVisualContainer {
flex: 1;
background-color: #383838;
border-left-width: 2px;
border-color: #272727;
padding-left: 6px;
padding-right: 4px;
padding-top: 4px;
}
#windowDetailsVisualContainer.hidden {
display:none;
}
#windowDetailsVisualContainer > .unity-label {
font-size: 11px;
color: #C4C4C4;
white-space: normal;
}

View File

@@ -0,0 +1,11 @@
<UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="itemMainVisualContainer">
<Style path="Searcher"/>
<ui:VisualElement name="itemIndent"/>
<ui:VisualElement name="itemChildExpander">
<ui:VisualElement name="expanderIcon"/>
</ui:VisualElement>
<ui:VisualElement name="itemIconVisualElement"/>
<ui:VisualElement name="labelsContainer"/>
</ui:VisualElement>
</UXML>

View File

@@ -0,0 +1,24 @@
<UXML xmlns:ui="UnityEngine.UIElements" xmlns:ed="UnityEditor.Experimental.UIElements" xmlns:lv="UnityEditor.EditorCommon.UIElements.ListView">
<ui:VisualElement name="windowMainVisualContainer">
<Style path="Searcher"/>
<ed:VisualSplitter name="splitter">
<ui:VisualElement name="searcherVisualContainer">
<ui:VisualElement name="windowTitleContainer">
<ui:Label name="windowTitleLabel" focusable="false"/>
</ui:VisualElement>
<ui:VisualElement name="windowSearchBoxVisualContainer">
<ui:VisualElement name="searchIcon"/>
<ui:Label name="autoCompleteLabel"/>
<ui:TextField name="searchBox"/>
</ui:VisualElement>
<ui:VisualElement name="windowSelectionVisualContainer">
<ui:ListView class="focusableScrollView" item-height="25" name="windowResultsScrollView"/>
</ui:VisualElement>
</ui:VisualElement>
<ui:VisualElement name="windowDetailsVisualContainer"/>
</ed:VisualSplitter>
<ui:VisualElement name="windowResizer" focusable="false">
<ui:VisualElement name="windowResizerIcon" focusable="false"/>
</ui:VisualElement>
</ui:VisualElement>
</UXML>

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
namespace UnityEditor.Searcher
{
[PublicAPI]
public class Searcher
{
public ISearcherAdapter Adapter { get; }
public Comparison<SearcherItem> SortComparison { get; set; }
readonly List<SearcherDatabaseBase> m_Databases;
public Searcher(SearcherDatabaseBase database, string title)
: this(new List<SearcherDatabaseBase> { database }, title, null)
{ }
public Searcher(IEnumerable<SearcherDatabaseBase> databases, string title)
: this(databases, title, null)
{ }
public Searcher(SearcherDatabaseBase database, ISearcherAdapter adapter = null)
: this(new List<SearcherDatabaseBase> { database }, adapter)
{ }
public Searcher(IEnumerable<SearcherDatabaseBase> databases, ISearcherAdapter adapter = null)
: this(databases, string.Empty, adapter)
{ }
Searcher(IEnumerable<SearcherDatabaseBase> databases, string title, ISearcherAdapter adapter)
{
m_Databases = new List<SearcherDatabaseBase>();
var databaseId = 0;
foreach (var database in databases)
{
// This is needed for sorting items between databases.
database.OverwriteId(databaseId);
databaseId++;
m_Databases.Add(database);
}
Adapter = adapter ?? new SearcherAdapter(title);
}
public void BuildIndices()
{
foreach (var database in m_Databases)
{
database.BuildIndex();
}
}
public IEnumerable<SearcherItem> Search(string query)
{
query = query.ToLower();
var results = new List<SearcherItem>();
float maxScore = 0;
foreach (var database in m_Databases)
{
var localResults = database.Search(query, out var localMaxScore);
if (localMaxScore > maxScore)
{
// skip the highest scored item in the local results and
// insert it back as the first item. The first item should always be
// the highest scored item. The order of the other items does not matter
// because they will be reordered to recreate the tree properly.
if (results.Count > 0)
{
// backup previous best result
results.Add(results[0]);
// replace it with the new best result
results[0] = localResults[0];
// add remaining results at the end
results.AddRange(localResults.Skip(1));
}
else // best result will be the first item
results.AddRange(localResults);
maxScore = localMaxScore;
}
else // no new best result just append everything
{
results.AddRange(localResults);
}
}
return results;
}
[PublicAPI]
public class AnalyticsEvent
{
[PublicAPI]
public enum EventType{ Pending, Picked, Cancelled }
public readonly EventType eventType;
public readonly string currentSearchFieldText;
public AnalyticsEvent(EventType eventType, string currentSearchFieldText)
{
this.eventType = eventType;
this.currentSearchFieldText = currentSearchFieldText;
}
}
}
}

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Searcher
{
public enum ItemExpanderState
{
Hidden,
Collapsed,
Expanded
}
[PublicAPI]
public interface ISearcherAdapter
{
VisualElement MakeItem();
VisualElement Bind(VisualElement target, SearcherItem item, ItemExpanderState expanderState, string text);
string Title { get; }
bool HasDetailsPanel { get; }
bool AddAllChildResults { get; }
float InitialSplitterDetailRatio { get; }
void OnSelectionChanged(IEnumerable<SearcherItem> items);
void InitDetailsPanel(VisualElement detailsPanel);
}
[PublicAPI]
public class SearcherAdapter : ISearcherAdapter
{
const string k_EntryName = "smartSearchItem";
const int k_IndentDepthFactor = 15;
readonly VisualTreeAsset m_DefaultItemTemplate;
public virtual string Title { get; }
public virtual bool HasDetailsPanel => true;
public virtual bool AddAllChildResults => true;
Label m_DetailsLabel;
public virtual float InitialSplitterDetailRatio => 1.0f;
public SearcherAdapter(string title)
{
Title = title;
m_DefaultItemTemplate = Resources.Load<VisualTreeAsset>("SearcherItem");
}
public virtual VisualElement MakeItem()
{
// Create a visual element hierarchy for this search result.
var item = m_DefaultItemTemplate.CloneTree();
return item;
}
public virtual VisualElement Bind(VisualElement element, SearcherItem item, ItemExpanderState expanderState, string query)
{
var indent = element.Q<VisualElement>("itemIndent");
indent.style.width = item.Depth * k_IndentDepthFactor;
var expander = element.Q<VisualElement>("itemChildExpander");
var icon = expander.Query("expanderIcon").First();
icon.ClearClassList();
switch (expanderState)
{
case ItemExpanderState.Expanded:
icon.AddToClassList("Expanded");
break;
case ItemExpanderState.Collapsed:
icon.AddToClassList("Collapsed");
break;
}
var nameLabelsContainer = element.Q<VisualElement>("labelsContainer");
nameLabelsContainer.Clear();
if (string.IsNullOrWhiteSpace(query))
nameLabelsContainer.Add(new Label(item.Name));
else
SearcherHighlighter.HighlightTextBasedOnQuery(nameLabelsContainer, item.Name, query);
element.userData = item;
element.name = k_EntryName;
return expander;
}
public virtual void InitDetailsPanel(VisualElement detailsPanel)
{
m_DetailsLabel = new Label();
detailsPanel.Add(m_DetailsLabel);
}
public virtual void OnSelectionChanged(IEnumerable<SearcherItem> items)
{
if (m_DetailsLabel != null)
{
var itemsList = items.ToList();
m_DetailsLabel.text = itemsList.Any() ? itemsList[0].Help : "No results";
}
}
}
}

View File

@@ -0,0 +1,679 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace UnityEditor.Searcher
{
class SearcherControl : VisualElement
{
// Window constants.
const string k_WindowTitleLabel = "windowTitleLabel";
const string k_WindowDetailsPanel = "windowDetailsVisualContainer";
const string k_WindowResultsScrollViewName = "windowResultsScrollView";
const string k_WindowSearchTextFieldName = "searchBox";
const string k_WindowAutoCompleteLabelName = "autoCompleteLabel";
const string k_WindowSearchIconName = "searchIcon";
const string k_WindowResizerName = "windowResizer";
const string kWindowSearcherPanel = "searcherVisualContainer";
const int k_TabCharacter = 9;
Label m_AutoCompleteLabel;
IEnumerable<SearcherItem> m_Results;
List<SearcherItem> m_VisibleResults;
HashSet<SearcherItem> m_ExpandedResults;
Searcher m_Searcher;
string m_SuggestedTerm;
string m_Text = string.Empty;
Action<SearcherItem> m_SelectionCallback;
Action<Searcher.AnalyticsEvent> m_AnalyticsDataCallback;
ListView m_ListView;
TextField m_SearchTextField;
VisualElement m_SearchTextInput;
VisualElement m_DetailsPanel;
VisualElement m_SearcherPanel;
internal Label TitleLabel { get; }
internal VisualElement Resizer { get; }
public SearcherControl()
{
// Load window template.
var windowUxmlTemplate = Resources.Load<VisualTreeAsset>("SearcherWindow");
// Clone Window Template.
var windowRootVisualElement = windowUxmlTemplate.CloneTree();
windowRootVisualElement.AddToClassList("content");
windowRootVisualElement.StretchToParentSize();
// Add Window VisualElement to window's RootVisualContainer
Add(windowRootVisualElement);
m_VisibleResults = new List<SearcherItem>();
m_ExpandedResults = new HashSet<SearcherItem>();
m_ListView = this.Q<ListView>(k_WindowResultsScrollViewName);
if (m_ListView != null)
{
m_ListView.bindItem = Bind;
m_ListView.RegisterCallback<KeyDownEvent>(SetSelectedElementInResultsList);
#if UNITY_2020_1_OR_NEWER
m_ListView.onItemsChosen += obj => m_SelectionCallback((SearcherItem)obj.FirstOrDefault());
m_ListView.onSelectionChange += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>().ToList());
#else
m_ListView.onItemChosen += obj => m_SelectionCallback((SearcherItem)obj);
m_ListView.onSelectionChanged += selectedItems => m_Searcher.Adapter.OnSelectionChanged(selectedItems.OfType<SearcherItem>());
#endif
m_ListView.focusable = true;
m_ListView.tabIndex = 1;
}
m_DetailsPanel = this.Q(k_WindowDetailsPanel);
TitleLabel = this.Q<Label>(k_WindowTitleLabel);
m_SearcherPanel = this.Q(kWindowSearcherPanel);
m_SearchTextField = this.Q<TextField>(k_WindowSearchTextFieldName);
if (m_SearchTextField != null)
{
m_SearchTextField.focusable = true;
m_SearchTextField.RegisterCallback<InputEvent>(OnSearchTextFieldTextChanged);
m_SearchTextInput = m_SearchTextField.Q(TextInputBaseField<string>.textInputUssName);
m_SearchTextInput.RegisterCallback<KeyDownEvent>(OnSearchTextFieldKeyDown);
}
m_AutoCompleteLabel = this.Q<Label>(k_WindowAutoCompleteLabelName);
Resizer = this.Q(k_WindowResizerName);
RegisterCallback<AttachToPanelEvent>(OnEnterPanel);
RegisterCallback<DetachFromPanelEvent>(OnLeavePanel);
// TODO: HACK - ListView's scroll view steals focus using the scheduler.
EditorApplication.update += HackDueToListViewScrollViewStealingFocus;
style.flexGrow = 1;
}
void HackDueToListViewScrollViewStealingFocus()
{
m_SearchTextInput?.Focus();
// ReSharper disable once DelegateSubtraction
EditorApplication.update -= HackDueToListViewScrollViewStealingFocus;
}
void OnEnterPanel(AttachToPanelEvent e)
{
RegisterCallback<KeyDownEvent>(OnKeyDown);
}
void OnLeavePanel(DetachFromPanelEvent e)
{
UnregisterCallback<KeyDownEvent>(OnKeyDown);
}
void OnKeyDown(KeyDownEvent e)
{
if (e.keyCode == KeyCode.Escape)
{
CancelSearch();
}
}
void CancelSearch()
{
OnSearchTextFieldTextChanged(InputEvent.GetPooled(m_Text, string.Empty));
m_SelectionCallback(null);
m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
}
public void Setup(Searcher searcher, Action<SearcherItem> selectionCallback, Action<Searcher.AnalyticsEvent> analyticsDataCallback)
{
m_Searcher = searcher;
m_SelectionCallback = selectionCallback;
m_AnalyticsDataCallback = analyticsDataCallback;
if (m_Searcher.Adapter.HasDetailsPanel)
{
m_Searcher.Adapter.InitDetailsPanel(m_DetailsPanel);
m_DetailsPanel.RemoveFromClassList("hidden");
m_DetailsPanel.style.flexGrow = m_Searcher.Adapter.InitialSplitterDetailRatio;
m_SearcherPanel.style.flexGrow = 1;
}
else
{
m_DetailsPanel.AddToClassList("hidden");
var splitter = m_DetailsPanel.parent;
splitter.parent.Insert(0,m_SearcherPanel);
splitter.parent.Insert(1, m_DetailsPanel);
splitter.RemoveFromHierarchy();
}
TitleLabel.text = m_Searcher.Adapter.Title;
if (string.IsNullOrEmpty(TitleLabel.text))
{
TitleLabel.parent.style.visibility = Visibility.Hidden;
TitleLabel.parent.style.position = Position.Absolute;
}
m_Searcher.BuildIndices();
Refresh();
}
void Refresh()
{
var query = m_Text;
m_Results = m_Searcher.Search(query);
GenerateVisibleResults();
// The first item in the results is always the highest scored item.
// We want to scroll to and select this item.
var visibleIndex = -1;
m_SuggestedTerm = string.Empty;
var results = m_Results.ToList();
if (results.Any())
{
var scrollToItem = results.First();
visibleIndex = m_VisibleResults.IndexOf(scrollToItem);
var cursorIndex = m_SearchTextField.cursorIndex;
if (query.Length > 0)
{
var strings = scrollToItem.Name.Split(' ');
var wordStartIndex = cursorIndex == 0 ? 0 : query.LastIndexOf(' ', cursorIndex - 1) + 1;
var word = query.Substring(wordStartIndex, cursorIndex - wordStartIndex);
if (word.Length > 0)
foreach (var t in strings)
{
if (t.StartsWith(word, StringComparison.OrdinalIgnoreCase))
{
m_SuggestedTerm = t;
break;
}
}
}
}
m_ListView.itemsSource = m_VisibleResults;
m_ListView.makeItem = m_Searcher.Adapter.MakeItem;
m_ListView.Refresh();
SetSelectedElementInResultsList(visibleIndex);
}
void GenerateVisibleResults()
{
if (string.IsNullOrEmpty(m_Text))
{
m_ExpandedResults.Clear();
RemoveChildrenFromResults();
return;
}
RegenerateVisibleResults();
ExpandAllParents();
}
void ExpandAllParents()
{
m_ExpandedResults.Clear();
foreach (var item in m_VisibleResults)
if (item.HasChildren)
m_ExpandedResults.Add(item);
}
void RemoveChildrenFromResults()
{
m_VisibleResults.Clear();
var parents = new HashSet<SearcherItem>();
foreach (var item in m_Results.Where(i => !parents.Contains(i)))
{
var currentParent = item;
while (true)
{
if (currentParent.Parent == null)
{
if (parents.Contains(currentParent))
break;
parents.Add(currentParent);
m_VisibleResults.Add(currentParent);
break;
}
currentParent = currentParent.Parent;
}
}
if (m_Searcher.SortComparison != null)
m_VisibleResults.Sort(m_Searcher.SortComparison);
}
void RegenerateVisibleResults()
{
var idSet = new HashSet<SearcherItem>();
m_VisibleResults.Clear();
foreach (var item in m_Results.Where(item => !idSet.Contains(item)))
{
idSet.Add(item);
m_VisibleResults.Add(item);
var currentParent = item.Parent;
while (currentParent != null)
{
if (!idSet.Contains(currentParent))
{
idSet.Add(currentParent);
m_VisibleResults.Add(currentParent);
}
currentParent = currentParent.Parent;
}
AddResultChildren(item, idSet);
}
var comparison = m_Searcher.SortComparison ?? ((i1, i2) =>
{
var result = i1.Database.Id - i2.Database.Id;
return result != 0 ? result : i1.Id - i2.Id;
});
m_VisibleResults.Sort(comparison);
}
void AddResultChildren(SearcherItem item, ISet<SearcherItem> idSet)
{
if (!item.HasChildren)
return;
if (m_Searcher.Adapter.AddAllChildResults)
{
//add all children results for current search term
// eg "Book" will show both "Cook Book" and "Cooking" as children
foreach (var child in item.Children)
{
if (!idSet.Contains(child))
{
idSet.Add(child);
m_VisibleResults.Add(child);
}
AddResultChildren(child, idSet);
}
}
else
{
foreach (var child in item.Children)
{
//only add child results if the child matches the search term
// eg "Book" will show "Cook Book" but not "Cooking" as a child
if (!m_Results.Contains(child))
continue;
if (!idSet.Contains(child))
{
idSet.Add(child);
m_VisibleResults.Add(child);
}
AddResultChildren(child, idSet);
}
}
}
bool HasChildResult(SearcherItem item)
{
if (m_Results.Contains(item))
return true;
foreach (var child in item.Children)
{
if (HasChildResult(child))
return true;
}
return false;
}
ItemExpanderState GetExpanderState(int index)
{
var item = m_VisibleResults[index];
foreach (var child in item.Children)
{
if (!m_VisibleResults.Contains(child) && !HasChildResult(child))
continue;
return m_ExpandedResults.Contains(item) ? ItemExpanderState.Expanded : ItemExpanderState.Collapsed;
}
return ItemExpanderState.Hidden;
}
void Bind(VisualElement target, int index)
{
var item = m_VisibleResults[index];
var expanderState = GetExpanderState(index);
var expander = m_Searcher.Adapter.Bind(target, item, expanderState, m_Text);
expander.RegisterCallback<MouseDownEvent>(ExpandOrCollapse);
}
static void GetItemsToHide(SearcherItem parent, ref HashSet<SearcherItem> itemsToHide)
{
if (!parent.HasChildren)
{
itemsToHide.Add(parent);
return;
}
foreach (var child in parent.Children)
{
itemsToHide.Add(child);
GetItemsToHide(child, ref itemsToHide);
}
}
void HideUnexpandedItems()
{
// Hide unexpanded children.
var itemsToHide = new HashSet<SearcherItem>();
foreach (var item in m_VisibleResults)
{
if (m_ExpandedResults.Contains(item))
continue;
if (!item.HasChildren)
continue;
if (itemsToHide.Contains(item))
continue;
// We need to hide its children.
GetItemsToHide(item, ref itemsToHide);
}
foreach (var item in itemsToHide)
m_VisibleResults.Remove(item);
}
// ReSharper disable once UnusedMember.Local
void RefreshListViewOn()
{
// TODO: Call ListView.Refresh() when it is fixed.
// Need this workaround until then.
// See: https://fogbugz.unity3d.com/f/cases/1027728/
// And: https://gitlab.internal.unity3d.com/upm-packages/editor/com.unity.searcher/issues/9
var scrollView = m_ListView.Q<ScrollView>();
var scroller = scrollView?.Q<Scroller>("VerticalScroller");
if (scroller == null)
return;
var oldValue = scroller.value;
scroller.value = oldValue + 1.0f;
scroller.value = oldValue - 1.0f;
scroller.value = oldValue;
}
void Expand(SearcherItem item)
{
m_ExpandedResults.Add(item);
RegenerateVisibleResults();
HideUnexpandedItems();
m_ListView.Refresh();
}
void Collapse(SearcherItem item)
{
// if it's already collapsed or not collapsed
if (!m_ExpandedResults.Remove(item))
{
// this case applies for a left arrow key press
if (item.Parent != null)
SetSelectedElementInResultsList(m_VisibleResults.IndexOf(item.Parent));
// even if it's a root item and has no parents, do nothing more
return;
}
RegenerateVisibleResults();
HideUnexpandedItems();
// TODO: understand what happened
m_ListView.Refresh();
// RefreshListViewOn();
}
void ExpandOrCollapse(MouseDownEvent evt)
{
if (!(evt.target is VisualElement expanderLabel))
return;
VisualElement itemElement = expanderLabel.GetFirstAncestorOfType<TemplateContainer>();
if (!(itemElement?.userData is SearcherItem item)
|| !item.HasChildren
|| !expanderLabel.ClassListContains("Expanded") && !expanderLabel.ClassListContains("Collapsed"))
return;
if (!m_ExpandedResults.Contains(item))
Expand(item);
else
Collapse(item);
evt.StopImmediatePropagation();
}
void OnSearchTextFieldTextChanged(InputEvent inputEvent)
{
var text = inputEvent.newData;
if (string.Equals(text, m_Text))
return;
// This is necessary due to OnTextChanged(...) being called after user inputs that have no impact on the text.
// Ex: Moving the caret.
m_Text = text;
// If backspace is pressed and no text remain, clear the suggestion label.
if (string.IsNullOrEmpty(text))
{
this.Q(k_WindowSearchIconName).RemoveFromClassList("Active");
// Display the unfiltered results list.
Refresh();
m_AutoCompleteLabel.text = String.Empty;
m_SuggestedTerm = String.Empty;
SetSelectedElementInResultsList(0);
return;
}
if (!this.Q(k_WindowSearchIconName).ClassListContains("Active"))
this.Q(k_WindowSearchIconName).AddToClassList("Active");
Refresh();
// Calculate the start and end indexes of the word being modified (if any).
var cursorIndex = m_SearchTextField.cursorIndex;
// search toward the beginning of the string starting at the character before the cursor
// +1 because we want the char after a space, or 0 if the search fails
var wordStartIndex = cursorIndex == 0 ? 0 : (text.LastIndexOf(' ', cursorIndex - 1) + 1);
// search toward the end of the string from the cursor index
var wordEndIndex = text.IndexOf(' ', cursorIndex);
if (wordEndIndex == -1) // no space found, assume end of string
wordEndIndex = text.Length;
// Clear the suggestion term if the caret is not within a word (both start and end indexes are equal, ex: (space)caret(space))
// or the user didn't append characters to a word at the end of the query.
if (wordStartIndex == wordEndIndex || wordEndIndex < text.Length)
{
m_AutoCompleteLabel.text = string.Empty;
m_SuggestedTerm = string.Empty;
return;
}
var word = text.Substring(wordStartIndex, wordEndIndex - wordStartIndex);
if (!string.IsNullOrEmpty(m_SuggestedTerm))
{
var wordSuggestion =
word + m_SuggestedTerm.Substring(word.Length, m_SuggestedTerm.Length - word.Length);
text = text.Remove(wordStartIndex, word.Length);
text = text.Insert(wordStartIndex, wordSuggestion);
m_AutoCompleteLabel.text = text;
}
else
{
m_AutoCompleteLabel.text = String.Empty;
}
}
void OnSearchTextFieldKeyDown(KeyDownEvent keyDownEvent)
{
// First, check if we cancelled the search.
if (keyDownEvent.keyCode == KeyCode.Escape)
{
CancelSearch();
return;
}
// For some reason the KeyDown event is raised twice when entering a character.
// As such, we ignore one of the duplicate event.
// This workaround was recommended by the Editor team. The cause of the issue relates to how IMGUI works
// and a fix was not in the works at the moment of this writing.
if (keyDownEvent.character == k_TabCharacter)
{
// Prevent switching focus to another visual element.
keyDownEvent.PreventDefault();
return;
}
// If Tab is pressed, complete the query with the suggested term.
if (keyDownEvent.keyCode == KeyCode.Tab)
{
// Used to prevent the TAB input from executing it's default behavior. We're hijacking it for auto-completion.
keyDownEvent.PreventDefault();
if (!string.IsNullOrEmpty(m_SuggestedTerm))
{
SelectAndReplaceCurrentWord();
m_AutoCompleteLabel.text = string.Empty;
// TODO: Revisit, we shouldn't need to do this here.
m_Text = m_SearchTextField.text;
Refresh();
m_SuggestedTerm = string.Empty;
}
}
else
{
SetSelectedElementInResultsList(keyDownEvent);
}
}
void SelectAndReplaceCurrentWord()
{
var s = m_SearchTextField.value;
var lastWordIndex = s.LastIndexOf(' ');
lastWordIndex++;
var newText = s.Substring(0, lastWordIndex) + m_SuggestedTerm;
// Wait for SelectRange api to reach trunk
//#if UNITY_2018_3_OR_NEWER
// m_SearchTextField.value = newText;
// m_SearchTextField.SelectRange(m_SearchTextField.value.Length, m_SearchTextField.value.Length);
//#else
// HACK - relies on the textfield moving the caret when being assigned a value and skipping
// all low surrogate characters
var magicMoveCursorToEndString = new string('\uDC00', newText.Length);
m_SearchTextField.value = magicMoveCursorToEndString;
m_SearchTextField.value = newText;
//#endif
}
void SetSelectedElementInResultsList(KeyDownEvent keyDownEvent)
{
int index;
switch (keyDownEvent.keyCode)
{
case KeyCode.Escape:
m_SelectionCallback(null);
m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
break;
case KeyCode.Return:
case KeyCode.KeypadEnter:
if (m_ListView.selectedIndex != -1)
{
m_SelectionCallback((SearcherItem)m_ListView.selectedItem);
m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Picked, m_SearchTextField.value));
}
else
{
m_SelectionCallback(null);
m_AnalyticsDataCallback?.Invoke(new Searcher.AnalyticsEvent(Searcher.AnalyticsEvent.EventType.Cancelled, m_SearchTextField.value));
}
break;
case KeyCode.LeftArrow:
index = m_ListView.selectedIndex;
if (index >= 0 && index < m_ListView.itemsSource.Count)
Collapse(m_ListView.selectedItem as SearcherItem);
break;
case KeyCode.RightArrow:
index = m_ListView.selectedIndex;
if (index >= 0 && index < m_ListView.itemsSource.Count)
Expand(m_ListView.selectedItem as SearcherItem);
break;
case KeyCode.UpArrow:
case KeyCode.DownArrow:
case KeyCode.PageUp:
case KeyCode.PageDown:
index = m_ListView.selectedIndex;
if (index >= 0 && index < m_ListView.itemsSource.Count)
m_ListView.OnKeyDown(keyDownEvent);
break;
}
}
void SetSelectedElementInResultsList(int selectedIndex)
{
var newIndex = selectedIndex >= 0 && selectedIndex < m_VisibleResults.Count ? selectedIndex : -1;
if (newIndex < 0)
return;
m_ListView.selectedIndex = newIndex;
m_ListView.ScrollToItem(m_ListView.selectedIndex);
}
}
}

View File

@@ -0,0 +1,393 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using UnityEngine;
namespace UnityEditor.Searcher
{
[PublicAPI]
public class SearcherDatabase : SearcherDatabaseBase
{
Dictionary<string, IReadOnlyList<ValueTuple<string, float>>> m_Index = new Dictionary<string, IReadOnlyList<ValueTuple<string, float>>>();
class Result
{
public SearcherItem item;
public float maxScore;
}
const bool k_IsParallel = true;
public Func<string, SearcherItem, bool> MatchFilter { get; set; }
public static SearcherDatabase Create(
List<SearcherItem> items,
string databaseDirectory,
bool serializeToFile = true
)
{
if (serializeToFile && databaseDirectory != null && !Directory.Exists(databaseDirectory))
Directory.CreateDirectory(databaseDirectory);
var database = new SearcherDatabase(databaseDirectory, items);
if (serializeToFile)
database.SerializeToFile();
database.BuildIndex();
return database;
}
public static SearcherDatabase Load(string databaseDirectory)
{
if (!Directory.Exists(databaseDirectory))
throw new InvalidOperationException("databaseDirectory not found.");
var database = new SearcherDatabase(databaseDirectory, null);
database.LoadFromFile();
database.BuildIndex();
return database;
}
public SearcherDatabase(IReadOnlyCollection<SearcherItem> db)
: this("", db)
{
}
SearcherDatabase(string databaseDirectory, IReadOnlyCollection<SearcherItem> db)
: base(databaseDirectory)
{
m_ItemList = new List<SearcherItem>();
var nextId = 0;
if (db != null)
foreach (var item in db)
AddItemToIndex(item, ref nextId, null);
}
public override List<SearcherItem> Search(string query, out float localMaxScore)
{
// Match assumes the query is trimmed
query = query.Trim(' ', '\t');
localMaxScore = 0;
if (string.IsNullOrWhiteSpace(query))
{
if (MatchFilter == null)
return m_ItemList;
// ReSharper disable once RedundantLogicalConditionalExpressionOperand
if (k_IsParallel && m_ItemList.Count > 100)
return FilterMultiThreaded(query);
return FilterSingleThreaded(query);
}
var finalResults = new List<SearcherItem> { null };
var max = new Result();
var tokenizedQuery = new List<string>();
foreach (var token in Tokenize(query))
{
tokenizedQuery.Add(token.Trim().ToLower());
}
// ReSharper disable once RedundantLogicalConditionalExpressionOperand
if (k_IsParallel && m_ItemList.Count > 100)
SearchMultithreaded(query, tokenizedQuery, max, finalResults);
else
SearchSingleThreaded(query, tokenizedQuery, max, finalResults);
localMaxScore = max.maxScore;
if (max.item != null)
finalResults[0] = max.item;
else
finalResults.RemoveAt(0);
return finalResults;
}
protected virtual bool Match(string query, IReadOnlyList<string> tokenizedQuery, SearcherItem item, out float score)
{
var filter = MatchFilter?.Invoke(query, item) ?? true;
return Match(tokenizedQuery, item.Path, out score) && filter;
}
List<SearcherItem> FilterSingleThreaded(string query)
{
var result = new List<SearcherItem>();
foreach (var searcherItem in m_ItemList)
{
if (!MatchFilter.Invoke(query, searcherItem))
continue;
result.Add(searcherItem);
}
return result;
}
List<SearcherItem> FilterMultiThreaded(string query)
{
var result = new List<SearcherItem>();
var count = Environment.ProcessorCount;
var tasks = new Task[count];
var lists = new List<SearcherItem>[count];
var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
for (var i = 0; i < count; i++)
{
var i1 = i;
tasks[i] = Task.Run(() =>
{
lists[i1] = new List<SearcherItem>();
for (var j = 0; j < itemsPerTask; j++)
{
var index = j + itemsPerTask * i1;
if (index >= m_ItemList.Count)
break;
var item = m_ItemList[index];
if (!MatchFilter.Invoke(query, item))
continue;
lists[i1].Add(item);
}
});
}
Task.WaitAll(tasks);
for (var i = 0; i < count; i++)
{
result.AddRange(lists[i]);
}
return result;
}
readonly float k_ScoreCutOff = 0.33f;
void SearchSingleThreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, ICollection<SearcherItem> finalResults)
{
List<Result> results = new List<Result>();
foreach (var item in m_ItemList)
{
float score = 0;
if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
{
if (score > max.maxScore)
{
max.item = item;
max.maxScore = score;
}
results.Add(new Result() { item = item, maxScore = score});
}
}
PostprocessResults(results, finalResults, max);
}
void SearchMultithreaded(string query, IReadOnlyList<string> tokenizedQuery, Result max, List<SearcherItem> finalResults)
{
var count = Environment.ProcessorCount;
var tasks = new Task[count];
var localResults = new Result[count];
var queue = new ConcurrentQueue<Result>();
var itemsPerTask = (int)Math.Ceiling(m_ItemList.Count / (float)count);
for (var i = 0; i < count; i++)
{
var i1 = i;
localResults[i1] = new Result();
tasks[i] = Task.Run(() =>
{
var result = localResults[i1];
for (var j = 0; j < itemsPerTask; j++)
{
var index = j + itemsPerTask * i1;
if (index >= m_ItemList.Count)
break;
var item = m_ItemList[index];
float score = 0;
if (query.Length == 0 || Match(query, tokenizedQuery, item, out score))
{
if (score > result.maxScore)
{
result.maxScore = score;
result.item = item;
}
queue.Enqueue(new Result { item = item, maxScore = score });
}
}
});
}
Task.WaitAll(tasks);
for (var i = 0; i < count; i++)
{
if (localResults[i].maxScore > max.maxScore)
{
max.maxScore = localResults[i].maxScore;
max.item = localResults[i].item;
}
}
PostprocessResults(queue, finalResults, max);
}
void PostprocessResults(IEnumerable<Result> results, ICollection<SearcherItem> items, Result max)
{
foreach (var result in results)
{
var normalizedScore = result.maxScore / max.maxScore;
if (result.item != null && result.item != max.item && normalizedScore > k_ScoreCutOff)
{
items.Add(result.item);
}
}
}
public override void BuildIndex()
{
m_Index.Clear();
foreach (var item in m_ItemList)
{
if (!m_Index.ContainsKey(item.Path))
{
List<ValueTuple<string, float>> terms = new List<ValueTuple<string, float>>();
// If the item uses synonyms to return results for similar words/phrases, add them to the search terms
IList<string> tokens = null;
if (item.Synonyms == null)
tokens = Tokenize(item.Name);
else
tokens = Tokenize(string.Format("{0} {1}", item.Name, string.Join(" ", item.Synonyms)));
string tokenSuite = "";
foreach (var token in tokens)
{
var t = token.ToLower();
if (t.Length > 1)
{
terms.Add(new ValueTuple<string, float>(t, 0.8f));
}
if (tokenSuite.Length > 0)
{
tokenSuite += " " + t;
terms.Add(new ValueTuple<string, float>(tokenSuite, 1f));
}
else
{
tokenSuite = t;
}
}
// Add a term containing all the uppercase letters (CamelCase World BBox => CCWBB)
var initialList = Regex.Split(item.Name, @"\P{Lu}+");
var initials = string.Concat(initialList).Trim();
if (!string.IsNullOrEmpty(initials))
terms.Add(new ValueTuple<string, float>(initials.ToLower(), 0.5f));
m_Index.Add(item.Path, terms);
}
}
}
static IList<string> Tokenize(string s)
{
var knownTokens = new HashSet<string>();
var tokens = new List<string>();
// Split on word boundaries
foreach (var t in Regex.Split(s, @"\W"))
{
// Split camel case words
var tt = Regex.Split(t, @"(\p{Lu}+\P{Lu}*)");
foreach (var ttt in tt)
{
var tttt = ttt.Trim();
if (!string.IsNullOrEmpty(tttt) && !knownTokens.Contains(tttt))
{
knownTokens.Add(tttt);
tokens.Add(tttt);
}
}
}
return tokens;
}
bool Match(IReadOnlyList<string> tokenizedQuery, string itemPath, out float score)
{
itemPath = itemPath.Trim();
if (itemPath == "")
{
if (tokenizedQuery.Count == 0)
{
score = 1;
return true;
}
else
{
score = 0;
return false;
}
}
IReadOnlyList<ValueTuple<string, float>> indexTerms;
if (!m_Index.TryGetValue(itemPath, out indexTerms))
{
score = 0;
return false;
}
float maxScore = 0.0f;
foreach (var t in indexTerms)
{
float scoreForTerm = 0f;
var querySuite = "";
var querySuiteFactor = 1.25f;
foreach (var q in tokenizedQuery)
{
if (t.Item1.StartsWith(q))
{
scoreForTerm += t.Item2 * q.Length / t.Item1.Length;
}
if (querySuite.Length > 0)
{
querySuite += " " + q;
if (t.Item1.StartsWith(querySuite))
{
scoreForTerm += t.Item2 * querySuiteFactor * querySuite.Length / t.Item1.Length;
}
}
else
{
querySuite = q;
}
querySuiteFactor *= querySuiteFactor;
}
maxScore = Mathf.Max(maxScore, scoreForTerm);
}
score = maxScore;
return score > 0;
}
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.IO;
using JetBrains.Annotations;
using UnityEngine;
namespace UnityEditor.Searcher
{
[PublicAPI]
public abstract class SearcherDatabaseBase
{
protected const string k_SerializedJsonFile = "/SerializedDatabase.json";
public string DatabaseDirectory { get; set; }
public IList<SearcherItem> ItemList => m_ItemList;
// ReSharper disable once Unity.RedundantSerializeFieldAttribute
[SerializeField]
protected List<SearcherItem> m_ItemList;
protected SearcherDatabaseBase(string databaseDirectory)
{
DatabaseDirectory = databaseDirectory;
}
public virtual void BuildIndex() { }
public abstract List<SearcherItem> Search(string query, out float localMaxScore);
internal void OverwriteId(int newId)
{
Id = newId;
}
internal int Id { get; private set; }
protected void LoadFromFile()
{
var reader = new StreamReader(DatabaseDirectory + k_SerializedJsonFile);
var serializedData = reader.ReadToEnd();
reader.Close();
EditorJsonUtility.FromJsonOverwrite(serializedData, this);
foreach (var item in m_ItemList)
{
item.OverwriteDatabase(this);
item.ReInitAfterLoadFromFile();
}
}
protected void SerializeToFile()
{
if (DatabaseDirectory == null)
return;
var serializedData = EditorJsonUtility.ToJson(this, true);
var writer = new StreamWriter(DatabaseDirectory + k_SerializedJsonFile, false);
writer.Write(serializedData);
writer.Close();
}
protected void AddItemToIndex(SearcherItem item, ref int lastId, Action<SearcherItem> action)
{
m_ItemList.Insert(lastId, item);
// We can only set the id here as we only know the final index of the item here.
item.OverwriteId(lastId);
item.GeneratePath();
action?.Invoke(item);
lastId++;
// This is used for sorting results between databases.
item.OverwriteDatabase(this);
if (!item.HasChildren)
return;
var childrenIds = new List<int>();
foreach (SearcherItem child in item.Children)
{
AddItemToIndex(child, ref lastId, action);
childrenIds.Add(child.Id);
}
item.OverwriteChildrenIds(childrenIds);
}
}
}

View File

@@ -0,0 +1,95 @@
using System;
using System.Text.RegularExpressions;
using UnityEngine.UIElements;
namespace UnityEditor.Searcher
{
static class SearcherHighlighter
{
const char k_StartHighlightSeparator = '{';
const char k_EndHighlightSeparator = '}';
const string k_HighlightedStyleClassName = "Highlighted";
public static void HighlightTextBasedOnQuery(VisualElement container, string text, string query)
{
var formattedText = text;
var queryParts = query.Split(new[] {" "}, StringSplitOptions.RemoveEmptyEntries);
var regex = string.Empty;
for (var index = 0; index < queryParts.Length; index++)
{
var queryPart = queryParts[index];
regex += $"({queryPart})";
if (index < queryParts.Length - 1)
regex += "|";
}
var matches = Regex.Matches(formattedText, regex, RegexOptions.IgnoreCase);
foreach (Match match in matches)
{
formattedText = formattedText.Replace(match.Value,
$"{k_StartHighlightSeparator}{match.Value}{k_EndHighlightSeparator}");
}
BuildHighlightLabels(container, formattedText);
}
static void BuildHighlightLabels(VisualElement container, string formattedHighlightText)
{
if (string.IsNullOrEmpty(formattedHighlightText))
return;
var substring = string.Empty;
var highlighting = false;
var skipCount = 0;
foreach (var character in formattedHighlightText.ToCharArray())
{
switch (character)
{
// Skip embedded separators
// Ex:
// Query: middle e
// Text: Middle Eastern
// Formatted Text: {Middl{e}} {E}ast{e}rn
// ^ ^
case k_StartHighlightSeparator when highlighting:
skipCount++;
continue;
case k_StartHighlightSeparator: {
highlighting = true;
if (!string.IsNullOrEmpty(substring))
{
container.Add(new Label(substring));
substring = string.Empty;
}
continue;
}
case k_EndHighlightSeparator when skipCount > 0:
skipCount--;
continue;
case k_EndHighlightSeparator: {
var label = new Label(substring);
label.AddToClassList(k_HighlightedStyleClassName);
container.Add(label);
highlighting = false;
substring = string.Empty;
continue;
}
default:
substring += character;
break;
}
}
if (!string.IsNullOrEmpty(substring))
{
var label = new Label(substring);
if (highlighting)
label.AddToClassList(k_HighlightedStyleClassName);
container.Add(label);
}
}
}
}

View File

@@ -0,0 +1,118 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using UnityEngine;
namespace UnityEditor.Searcher
{
[PublicAPI]
[Serializable]
public class SearcherItem
{
[SerializeField] int m_Id;
[SerializeField] List<int> m_ChildrenIds;
[SerializeField] string m_Name;
[SerializeField] string m_Help;
[SerializeField] string[] m_Synonyms;
public int Id => m_Id;
public virtual string Name => m_Name;
public string Path { get; private set; }
public string Help => m_Help;
public string[] Synonyms { get { return m_Synonyms; } set { m_Synonyms = value; } }
public int Depth => Parent?.Depth + 1 ?? 0;
public SearcherItem Parent { get; private set; }
public SearcherDatabaseBase Database { get; private set; }
public List<SearcherItem> Children { get; private set; }
public bool HasChildren => Children.Count > 0;
public SearcherItem(string name, string help = "", List<SearcherItem> children = null)
{
m_Id = -1;
Parent = null;
Database = null;
m_Name = name;
m_Help = help;
Children = new List<SearcherItem>();
if (children == null)
return;
Children = children;
foreach (var child in children)
child.OverwriteParent(this);
}
public void AddChild(SearcherItem child)
{
if (child == null)
throw new ArgumentNullException(nameof(child));
if (Database != null)
throw new InvalidOperationException(
"Cannot add more children to an item that was already used in a database.");
if (Children == null)
Children = new List<SearcherItem>();
Children.Add(child);
child.OverwriteParent(this);
}
internal void OverwriteId(int newId)
{
m_Id = newId;
}
void OverwriteParent(SearcherItem newParent)
{
Parent = newParent;
}
internal void OverwriteDatabase(SearcherDatabaseBase newDatabase)
{
Database = newDatabase;
}
internal void OverwriteChildrenIds(List<int> childrenIds)
{
m_ChildrenIds = childrenIds;
}
internal void GeneratePath()
{
if (Parent != null)
Path = Parent.Path + " ";
else
Path = string.Empty;
Path += Name;
}
internal void ReInitAfterLoadFromFile()
{
if (Children == null)
Children = new List<SearcherItem>();
foreach (var id in m_ChildrenIds)
{
var child = Database.ItemList[id];
Children.Add(child);
child.OverwriteParent(this);
}
GeneratePath();
}
public override string ToString()
{
return $"{nameof(Id)}: {Id}, {nameof(Name)}: {Name}, {nameof(Depth)}: {Depth}";
}
}
}

View File

@@ -0,0 +1,428 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.UIElements;
namespace UnityEditor.Searcher
{
[PublicAPI]
public class SearcherWindow : EditorWindow
{
[PublicAPI]
public struct Alignment
{
[PublicAPI]
public enum Horizontal { Left = 0, Center, Right }
[PublicAPI]
public enum Vertical { Top = 0, Center, Bottom }
public readonly Vertical vertical;
public readonly Horizontal horizontal;
public Alignment(Vertical v, Horizontal h)
{
vertical = v;
horizontal = h;
}
}
const string k_DatabaseDirectory = "/../Library/Searcher";
static readonly float k_SearcherDefaultWidth = 300;
static readonly float k_DetailsDefaultWidth = 200;
static readonly float k_DefaultHeight = 300;
static readonly Vector2 k_MinSize = new Vector2(300, 150);
static Vector2 s_Size = Vector2.zero;
static IEnumerable<SearcherItem> s_Items;
static Searcher s_Searcher;
static Func<SearcherItem, bool> s_ItemSelectedDelegate;
Action<Searcher.AnalyticsEvent> m_AnalyticsDataDelegate;
SearcherControl m_SearcherControl;
Vector2 m_OriginalMousePos;
Rect m_OriginalWindowPos;
Rect m_NewWindowPos;
bool m_IsMouseDownOnResizer;
bool m_IsMouseDownOnTitle;
Focusable m_FocusedBefore;
static Vector2 Size
{
get
{
if (s_Size == Vector2.zero)
{
s_Size = s_Searcher != null && s_Searcher.Adapter.HasDetailsPanel
? new Vector2(k_SearcherDefaultWidth + k_DetailsDefaultWidth, k_DefaultHeight)
: new Vector2(k_SearcherDefaultWidth, k_DefaultHeight);
}
return s_Size;
}
set => s_Size = value;
}
public static void Show(
EditorWindow host,
IList<SearcherItem> items,
string title,
Func<SearcherItem, bool> itemSelectedDelegate,
Vector2 displayPosition,
Alignment align = default)
{
Show(host, items, title, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate, displayPosition, align);
}
public static void Show(
EditorWindow host,
IList<SearcherItem> items,
ISearcherAdapter adapter,
Func<SearcherItem, bool> itemSelectedDelegate,
Vector2 displayPosition,
Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
Alignment align = default)
{
Show(host, items, adapter, Application.dataPath + k_DatabaseDirectory, itemSelectedDelegate,
displayPosition, analyticsDataDelegate, align);
}
public static void Show(
EditorWindow host,
IList<SearcherItem> items,
string title,
string directoryPath,
Func<SearcherItem, bool> itemSelectedDelegate,
Vector2 displayPosition,
Alignment align = default)
{
s_Items = items;
var databaseDir = directoryPath;
var database = SearcherDatabase.Create(s_Items.ToList(), databaseDir);
s_Searcher = new Searcher(database, title);
Show(host, s_Searcher, itemSelectedDelegate, displayPosition, null, align);
}
public static void Show(
EditorWindow host,
IEnumerable<SearcherItem> items,
ISearcherAdapter adapter,
string directoryPath,
Func<SearcherItem, bool> itemSelectedDelegate,
Vector2 displayPosition,
Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
Alignment align = default)
{
s_Items = items;
var databaseDir = directoryPath;
var database = SearcherDatabase.Create(s_Items.ToList(), databaseDir);
s_Searcher = new Searcher(database, adapter);
Show(host, s_Searcher, itemSelectedDelegate, displayPosition, analyticsDataDelegate, align);
}
public static void Show(
EditorWindow host,
Searcher searcher,
Func<SearcherItem, bool> itemSelectedDelegate,
Vector2 displayPosition,
Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
Alignment align = default)
{
var position = GetPosition(host, displayPosition, align);
var rect = new Rect(GetPositionWithAlignment(position + host.position.position, Size, align), Size);
Show(host, searcher, itemSelectedDelegate, analyticsDataDelegate, rect);
}
public static void Show(
EditorWindow host,
Searcher searcher,
Func<SearcherItem, bool> itemSelectedDelegate,
Action<Searcher.AnalyticsEvent> analyticsDataDelegate,
Rect rect)
{
s_Searcher = searcher;
s_ItemSelectedDelegate = itemSelectedDelegate;
var window = CreateInstance<SearcherWindow>();
window.m_AnalyticsDataDelegate = analyticsDataDelegate;
window.position = rect;
window.ShowPopup();
window.Focus();
}
public static Vector2 GetPositionWithAlignment(Vector2 pos, Vector2 size, Alignment align)
{
var x = pos.x;
var y = pos.y;
switch (align.horizontal)
{
case Alignment.Horizontal.Center:
x -= size.x / 2;
break;
case Alignment.Horizontal.Right:
x -= size.x;
break;
}
switch (align.vertical)
{
case Alignment.Vertical.Center:
y -= size.y / 2;
break;
case Alignment.Vertical.Bottom:
y -= size.y;
break;
}
return new Vector2(x, y);
}
static Vector2 GetPosition(EditorWindow host, Vector2 displayPosition, Alignment align)
{
var x = displayPosition.x;
var y = displayPosition.y;
// Searcher overlaps with the right boundary.
if (x + Size.x >= host.position.size.x)
{
switch (align.horizontal)
{
case Alignment.Horizontal.Center:
x -= Size.x / 2;
break;
case Alignment.Horizontal.Right:
x -= Size.x;
break;
}
}
// The displayPosition should be in window world space but the
// EditorWindow.position is actually the rootVisualElement
// rectangle, not including the tabs area. So we need to do a
// small correction here.
y -= host.rootVisualElement.resolvedStyle.top;
// Searcher overlaps with the bottom boundary.
if (y + Size.y >= host.position.size.y)
{
switch (align.vertical)
{
case Alignment.Vertical.Center:
y -= Size.y / 2;
break;
case Alignment.Vertical.Bottom:
y -= Size.y;
break;
}
}
return new Vector2(x, y);
}
void OnEnable()
{
m_SearcherControl = new SearcherControl();
m_SearcherControl.Setup(s_Searcher, SelectionCallback, OnAnalyticsDataCallback);
m_SearcherControl.TitleLabel.RegisterCallback<MouseDownEvent>(OnTitleMouseDown);
m_SearcherControl.TitleLabel.RegisterCallback<MouseUpEvent>(OnTitleMouseUp);
m_SearcherControl.Resizer.RegisterCallback<MouseDownEvent>(OnResizerMouseDown);
m_SearcherControl.Resizer.RegisterCallback<MouseUpEvent>(OnResizerMouseUp);
var root = rootVisualElement;
root.style.flexGrow = 1;
root.Add(m_SearcherControl);
}
void OnDisable()
{
m_SearcherControl.TitleLabel.UnregisterCallback<MouseDownEvent>(OnTitleMouseDown);
m_SearcherControl.TitleLabel.UnregisterCallback<MouseUpEvent>(OnTitleMouseUp);
m_SearcherControl.Resizer.UnregisterCallback<MouseDownEvent>(OnResizerMouseDown);
m_SearcherControl.Resizer.UnregisterCallback<MouseUpEvent>(OnResizerMouseUp);
}
void OnTitleMouseDown(MouseDownEvent evt)
{
if (evt.button != (int)MouseButton.LeftMouse)
return;
m_IsMouseDownOnTitle = true;
m_NewWindowPos = position;
m_OriginalWindowPos = position;
m_OriginalMousePos = evt.mousePosition;
m_FocusedBefore = rootVisualElement.panel.focusController.focusedElement;
m_SearcherControl.TitleLabel.RegisterCallback<MouseMoveEvent>(OnTitleMouseMove);
m_SearcherControl.TitleLabel.RegisterCallback<KeyDownEvent>(OnSearcherKeyDown);
m_SearcherControl.TitleLabel.CaptureMouse();
}
void OnTitleMouseUp(MouseUpEvent evt)
{
if (evt.button != (int)MouseButton.LeftMouse)
return;
if (!m_SearcherControl.TitleLabel.HasMouseCapture())
return;
FinishMove();
}
void FinishMove()
{
m_SearcherControl.TitleLabel.UnregisterCallback<MouseMoveEvent>(OnTitleMouseMove);
m_SearcherControl.TitleLabel.UnregisterCallback<KeyDownEvent>(OnSearcherKeyDown);
m_SearcherControl.TitleLabel.ReleaseMouse();
m_FocusedBefore?.Focus();
m_IsMouseDownOnTitle = false;
}
void OnTitleMouseMove(MouseMoveEvent evt)
{
var delta = evt.mousePosition - m_OriginalMousePos;
// TODO Temporary fix for Visual Scripting 1st drop. Find why position.position is 0,0 on MacOs in MouseMoveEvent
// Bug occurs with Unity 2019.2.0a13
#if UNITY_EDITOR_OSX
m_NewWindowPos = new Rect(m_NewWindowPos.position + delta, position.size);
#else
m_NewWindowPos = new Rect(position.position + delta, position.size);
#endif
Repaint();
}
void OnResizerMouseDown(MouseDownEvent evt)
{
if (evt.button != (int)MouseButton.LeftMouse)
return;
m_IsMouseDownOnResizer = true;
m_NewWindowPos = position;
m_OriginalWindowPos = position;
m_OriginalMousePos = evt.mousePosition;
m_FocusedBefore = rootVisualElement.panel.focusController.focusedElement;
m_SearcherControl.Resizer.RegisterCallback<MouseMoveEvent>(OnResizerMouseMove);
m_SearcherControl.Resizer.RegisterCallback<KeyDownEvent>(OnSearcherKeyDown);
m_SearcherControl.Resizer.CaptureMouse();
}
void OnResizerMouseUp(MouseUpEvent evt)
{
if (evt.button != (int)MouseButton.LeftMouse)
return;
if (!m_SearcherControl.Resizer.HasMouseCapture())
return;
FinishResize();
}
void FinishResize()
{
m_SearcherControl.Resizer.UnregisterCallback<MouseMoveEvent>(OnResizerMouseMove);
m_SearcherControl.Resizer.UnregisterCallback<KeyDownEvent>(OnSearcherKeyDown);
m_SearcherControl.Resizer.ReleaseMouse();
m_FocusedBefore?.Focus();
m_IsMouseDownOnResizer = false;
}
void OnResizerMouseMove(MouseMoveEvent evt)
{
var delta = evt.mousePosition - m_OriginalMousePos;
Size = m_OriginalWindowPos.size + delta;
Size = new Vector2(Math.Max(k_MinSize.x, Size.x), Math.Max(k_MinSize.y, Size.y));
// TODO Temporary fix for Visual Scripting 1st drop. Find why position.position is 0,0 on MacOs in MouseMoveEvent
// Bug occurs with Unity 2019.2.0a13
#if UNITY_EDITOR_OSX
m_NewWindowPos = new Rect(m_NewWindowPos.position, Size);
#else
m_NewWindowPos = new Rect(position.position, Size);
#endif
Repaint();
}
void OnSearcherKeyDown(KeyDownEvent evt)
{
if (evt.keyCode == KeyCode.Escape)
{
if (m_IsMouseDownOnTitle)
{
FinishMove();
position = m_OriginalWindowPos;
}
else if (m_IsMouseDownOnResizer)
{
FinishResize();
position = m_OriginalWindowPos;
}
}
}
void OnGUI()
{
if ((m_IsMouseDownOnTitle || m_IsMouseDownOnResizer) && Event.current.type == EventType.Layout)
position = m_NewWindowPos;
}
void SelectionCallback(SearcherItem item)
{
// Don't close the window if a category is selected (only categories/titles have children, node entries are leaf elements)
// We want to prevent collapsing the window due to accidental double-clicks on a title entry, for instance
if (item != null && item.HasChildren)
return;
if (s_ItemSelectedDelegate == null || s_ItemSelectedDelegate(item))
Close();
}
void OnAnalyticsDataCallback(Searcher.AnalyticsEvent item)
{
m_AnalyticsDataDelegate?.Invoke(item);
}
void OnLostFocus()
{
if (m_IsMouseDownOnTitle)
{
FinishMove();
}
else if (m_IsMouseDownOnResizer)
{
FinishResize();
}
// TODO: HACK - ListView's scroll view steals focus using the scheduler.
EditorApplication.update += HackDueToCloseOnLostFocusCrashing;
}
// See: https://fogbugz.unity3d.com/f/cases/1004504/
void HackDueToCloseOnLostFocusCrashing()
{
// Notify user that the searcher action was cancelled.
s_ItemSelectedDelegate?.Invoke(null);
Close();
// ReSharper disable once DelegateSubtraction
EditorApplication.update -= HackDueToCloseOnLostFocusCrashing;
}
}
}

View File

@@ -0,0 +1,8 @@
{
"name": "Unity.Searcher.Editor",
"references": [],
"includePlatforms": [
"Editor"
],
"excludePlatforms": []
}

View File

@@ -0,0 +1,31 @@
**Unity Companion Package License v1.0 ("_License_")**
Copyright © 2019 Unity Technologies ApS ("**_Unity_**")
Unity hereby grants to you a worldwide, non-exclusive, no-charge, and royalty-free copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute the software that is made available with this License ("**_Software_**"), subject to the following terms and conditions:
1. *Unity Companion Use Only*. Exercise of the license granted herein is limited to exercise for the creation, use, and/or distribution of applications, software, or other content pursuant to a valid Unity development engine software license ("**_Engine License_**"). That means while use of the Software is not limited to use in the software licensed under the Engine License, the Software may not be used for any purpose other than the creation, use, and/or distribution of Engine License-dependent applications, software, or other content. No other exercise of the license granted herein is permitted.
1. *No Modification of Engine License*. Neither this License nor any exercise of the license granted herein modifies the Engine License in any way.
1. *Ownership & Grant Back to You*.
3.1. You own your content. In this License, "derivative works" means derivatives of the Software itself--works derived only from the Software by you under this License (for example, modifying the code of the Software itself to improve its efficacy); “derivative works” of the Software do not include, for example, games, apps, or content that you create using the Software. You keep all right, title, and interest to your own content.
3.2. Unity owns its content. While you keep all right, title, and interest to your own content per the above, as between Unity and you, Unity will own all right, title, and interest to all intellectual property rights (including patent, trademark, and copyright) in the Software and derivative works of the Software, and you hereby assign and agree to assign all such rights in those derivative works to Unity.
3.3. You have a license to those derivative works. Subject to this License, Unity grants to you the same worldwide, non-exclusive, no-charge, and royalty-free copyright license to derivative works of the Software you create as is granted to you for the Software under this License.
1. *Trademarks*. You are not granted any right or license under this License to use any trademarks, service marks, trade names, products names, or branding of Unity or its affiliates ("**_Trademarks_**"). Descriptive uses of Trademarks are permitted; see, for example, Unitys Branding Usage Guidelines at [https://unity3d.com/public-relations/brand](https://unity3d.com/public-relations/brand).
1. *Notices & Third-Party Rights*. This License, including the copyright notice above, must be provided in all substantial portions of the Software and derivative works thereof (or, if that is impracticable, in any other location where such notices are customarily placed). Further, if the Software is accompanied by a Unity "third-party notices" or similar file, you acknowledge and agree that software identified in that file is governed by those separate license terms.
1. *DISCLAIMER, LIMITATION OF LIABILITY*. THE SOFTWARE AND ANY DERIVATIVE WORKS THEREOF IS PROVIDED ON AN "AS IS" BASIS, AND IS PROVIDED WITHOUT WARRANTY OF ANY KIND, WHETHER EXPRESS OR IMPLIED, INCLUDING ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND/OR NONINFRINGEMENT. IN NO EVENT SHALL ANY COPYRIGHT HOLDER OR AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES (WHETHER DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL, INCLUDING PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, LOSS OF USE, DATA, OR PROFITS, AND BUSINESS INTERRUPTION), OR OTHER LIABILITY WHATSOEVER, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM OR OUT OF, OR IN CONNECTION WITH, THE SOFTWARE OR ANY DERIVATIVE WORKS THEREOF OR THE USE OF OR OTHER DEALINGS IN SAME, EVEN WHERE ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
1. *USE IS ACCEPTANCE and License Versions*. Your receipt and use of the Software constitutes your acceptance of this License and its terms and conditions. Software released by Unity under this License may be modified or updated and the License with it; upon any such modification or update, you will comply with the terms of the updated License for any use of any of the Software under the updated License.
1. *Use in Compliance with Law and Termination*. Your exercise of the license granted herein will at all times be in compliance with applicable law and will not infringe any proprietary rights (including intellectual property rights); this License will terminate immediately on any breach by you of this License.
1. *Severability*. If any provision of this License is held to be unenforceable or invalid, that provision will be enforced to the maximum extent possible and the other provisions will remain in full force and effect.
1. *Governing Law and Venue*. This License is governed by and construed in accordance with the laws of Denmark, except for its conflict of laws rules; the United Nations Convention on Contracts for the International Sale of Goods will not apply. If you reside (or your principal place of business is) within the United States, you and Unity agree to submit to the personal and exclusive jurisdiction of and venue in the state and federal courts located in San Francisco County, California concerning any dispute arising out of this License ("**_Dispute_**"). If you reside (or your principal place of business is) outside the United States, you and Unity agree to submit to the personal and exclusive jurisdiction of and venue in the courts located in Copenhagen, Denmark concerning any Dispute.

View File

@@ -0,0 +1,155 @@
# Searcher
Use the Searcher package to quickly search a large list of items via a popup window. For example, use Searcher to find, select, and put down a new node in a graph. The Searcher package also includes samples and tests.
## Features
![GitHub Logo](/Documentation~/images/tree_view.png) ![GitHub Logo](/Documentation~/images/quick_search.png)
* Popup Window Placement
* Tree View
* Keyboard Navigation
* Quick Search
* Auto-Complete
* Match Highlighting
* Multiple Databases
## Quick Usage Example
```csharp
void OnMouseDown( MouseDownEvent evt )
{
var items = new List<SearcherItem>
{
new SearcherItem( "Books", "Description", new List<SearcherItem>()
{
new SearcherItem( "Dune" ),
} )
};
items[0].AddChild( new SearcherItem( "Ender's Game" ) );
SearcherWindow.Show(
this, // this EditorWindow
items, "Optional Title",
item => { Debug.Log( item.name ); return /*close window?*/ true; },
evt.mousePosition );
}
```
## Installing the Package
Open this file in your project:
```
Packages/manifest.json
```
Add this to the ```dependencies``` array (makes sure to change the version string to your current version):
```json
"com.unity.searcher": "4.0.0-preview"
```
For example, if this it he only package you depend on, you should have something like this (makes sure to change the version string to your current version):
```json
{
"dependencies": {
"com.unity.searcher": "4.0.0-preview"
}
}
```
## Enabling the Samples and Tests
Right now, it seems Samples and Tests only show for local packages, meaning you cloned this repo *inside* your **Packages** folder. Given you've done that, open this file in your project:
```
Packages/manifest.json
```
Add a ```testables``` list with the package name so you get something like this (makes sure to change the version string to your current version):
```json
{
"dependencies": {
"com.unity.searcher": "4.0.0-preview"
},
"testables" : [ "com.unity.searcher" ]
}
```
You should see a new top-level menu called **Searcher** and you should see Searcher tests in **Test Runner**.
### Searcher Creation from Database
```csharp
var bookItems = new List<SearcherItem> { new SearcherItem( "Books" ) };
var foodItems = new List<SearcherItem> { new SearcherItem( "Foods" ) };
// Create databases.
var databaseDir = Application.dataPath + "/../Library/Searcher";
var bookDatabase = SearcherDatabase.Create( bookItems, databaseDir + "/Books" );
var foodDatabase = SearcherDatabase.Create( foodItems, databaseDir + "/Foods" );
// At a later time, load database from disk.
bookDatabase = SearcherDatabase.Load( databaseDir + "/Books" );
var searcher = new Searcher(
new SearcherDatabase[]{ foodDatabase, bookDatabase },
"Optional Title" );
```
### Popup Window or Create Control
```csharp
Searcher m_Searcher;
void OnMouseDown( MouseDownEvent evt ) { // Popup window...
SearcherWindow.Show( this, m_Searcher,
item => { Debug.Log( item.name ); return /*close window?*/ true; },
evt.mousePosition );
}
// ...or create SearcherControl VisualElement
void OnEnable() { // ...or create SearcherControl VisualElement
var searcherControl = new SearcherControl();
searcherControl.Setup( m_Searcher, item => Debug.Log( item.name ) );
this.GetRootVisualContainer().Add( searcherControl );
}
```
### Customize the UI via `ISearcherAdapter`
```csharp
public interface ISearcherAdapter {
VisualElement MakeItem();
VisualElement Bind( VisualElement target, SearcherItem item,
ItemExpanderState expanderState, string text );
string title { get; }
bool hasDetailsPanel { get; }
void DisplaySelectionDetails( VisualElement detailsPanel, SearcherItem o );
void DisplayNoSelectionDetails( VisualElement detailsPanel );
void InitDetailsPanel( VisualElement detailsPanel );
}
var bookDatabase = SearcherDatabase.Load( Application.dataPath + "/Books" );
var myAdapter = new MyAdapter(); // class MyAdapter : ISearcherAdapter
var searcher = new Searcher( bookDatabase, myAdapter );
```
# Technical details
## Requirements
This version of Searcher is compatible with the following versions of the Unity Editor:
* 2019.1 and later (recommended)
## Known limitations
Searcher version 1.0 includes the following known limitations:
* Only works with .Net 4.0
## Package contents
The following table indicates the main folders of the package:
|Location|Description|
|---|---|
|`Editor/Resources`|Contains images used in the UI.|
|`Editor/Searcher`|Contains Searcher source files.|
|`Samples`|Contains the samples.|
|`Tests`|Contains the tests.|

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework.Constraints;
namespace UnityEditor.Searcher.Tests
{
[PublicAPI]
class Is : NUnit.Framework.Is
{
public static SearcherItemCollectionEquivalentConstraint SearcherItemCollectionEquivalent(IEnumerable<SearcherItem> expected)
{
return new SearcherItemCollectionEquivalentConstraint(expected);
}
}
class SearcherItemCollectionEquivalentConstraint : CollectionItemsEqualConstraint
{
readonly List<SearcherItem> m_Expected;
public SearcherItemCollectionEquivalentConstraint(IEnumerable<SearcherItem> expected)
: base(expected)
{
m_Expected = expected.ToList();
}
protected override bool Matches(IEnumerable actual)
{
if (m_Expected == null)
{
Description = "Expected is not a valid collection";
return false;
}
if (!(actual is IEnumerable<SearcherItem> actualCollection))
{
Description = "Actual is not a valid collection";
return false;
}
var actualList = actualCollection.ToList();
if (actualList.Count != m_Expected.Count)
{
Description = $"Collections lengths are not equal. \nExpected length: {m_Expected.Count}, " +
$"\nBut was: {actualList.Count}";
return false;
}
for (var i = 0; i < m_Expected.Count; ++i)
{
var res1 = m_Expected[i].Name;
var res2 = actualList[i].Name;
if (!string.Equals(res1, res2))
{
Description = $"Object at index {i} are not the same.\nExpected: {res1},\nBut was: {res2}";
return false;
}
var constraint = new SearcherItemCollectionEquivalentConstraint(m_Expected[i].Children);
if (constraint.Matches(actualList[i].Children))
continue;
Description = constraint.Description;
return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,227 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
using NUnit.Framework;
using UnityEngine;
// ReSharper disable StringLiteralTypo
namespace UnityEditor.Searcher.Tests
{
class SearcherTests
{
Searcher m_Searcher;
[OneTimeSetUp]
public void Init()
{
var bookItems = new List<SearcherItem>
{
new SearcherItem("Books", "Books Category", new List<SearcherItem>
{
new SearcherItem("Cooking", "Cooking Category", new List<SearcherItem>
{
new SearcherItem("Japanese", "A long-standing staple in any Japanese cuisine aficionados kitchen, Shizuo Tsujis Japanese Cooking has been around for more than a quarter of a century. Often referred to as the bible of Japanese cooking, this book has the most informative foreword describing different traditional ingredients, kitchen tools and cooking techniques of any cookbook on the market. Tsuji emphasizes just how important color, texture and artful presentation are in Japanese cooking and helps the novice chef to master these before turning their focus to improving the taste of their dishes. The 130 recipes that make up part two of the book include a mixture of simple, speedy weekday dinners and time-consuming but incredibly impressive dinner party fare. Be sure to try out the sea bream sashimi, spicy eggplant nimono, nigiri sushi and the lotus root agemono."),
new SearcherItem("Chinese", "Chinese cuisine is an important part of Chinese culture, which includes cuisine originating from the diverse regions of China, as well as from Chinese people in other parts of the world. Because of the Chinese diaspora and historical power of the country, Chinese cuisine has influenced many other cuisines in Asia, with modifications made to cater to local palates. The preference for seasoning and cooking techniques of Chinese provinces depend on differences in historical background and ethnic groups. Geographic features including mountains, rivers, forests and deserts also have a strong effect on the local available ingredients, considering climate of China varies from tropical in the south to subarctic in the northeast. Imperial, royal and noble preference also plays a role in the change of Chinese cuisines. Because of imperial expansion and trading, ingredients and cooking techniques from other cultures are integrated into Chinese cuisines over time.The most praised \"Four Major Cuisines\" are Chuan, Lu, Yue and Huaiyang, representing West, North, South and East China cuisine correspondingly.[2] Modern \"Eight Cuisines\" of China[3] are Anhui, Cantonese, Fujian, Hunan, Jiangsu, Shandong, Sichuan, and Zhejiang cuisines.[4]"),
new SearcherItem("Middle Eastern", "Middle-Eastern cuisine is the cuisine of the various countries and peoples of the Middle East. The cuisine of the region is diverse while having a degree of homogeneity. It includes Arab cuisine, Iranian/Persian cuisine, Israeli cuisine/Jewish cuisine, Assyrian cuisine, Armenian cuisine, Kurdish cuisine, Greek cuisine/Cypriot cuisine, Bosnian cuisine, Azerbaijani cuisine, and Turkish cuisine.[1] Some commonly used ingredients include olives and olive oil, pitas, honey, sesame seeds, dates,[1] sumac, chickpeas, mint, rice, and parsley. Some popular dishes include Kebabs, Dolma, Baklava, Doogh, and Doner Kebab (similar to Shawarma)."),
new SearcherItem("African", "Traditionally, the various cuisines of Africa use a combination of locally available fruits, cereal grains and vegetables, as well as milk and meat products, and do not usually get food imported. In some parts of the continent, the traditional diet features a lot of milk, curd and whey products."),
new SearcherItem("French", "In the 14th century Guillaume Tirel, a court chef known as \"Taillevent\", wrote Le Viandier, one of the earliest recipe collections of medieval France. During that time, French cuisine was heavily influenced by Italian cuisine. In the 17th century, chefs François Pierre La Varenne and Marie-Antoine Carême spearheaded movements that shifted French cooking away from its foreign influences and developed France's own indigenous style. Cheese and wine are a major part of the cuisine. They play different roles regionally and nationally, with many variations and appellation d'origine contrôlée (AOC) (regulated appellation) laws.")
}),
new SearcherItem("Science Fiction", "Science Fiction Category", new List<SearcherItem>
{
new SearcherItem("Dune", "Dune is a 1965 epic science fiction novel by American author Frank Herbert, originally published as two separate serials in Analog magazine. It tied with Roger Zelazny's This Immortal for the Hugo Award in 1966,[3] and it won the inaugural Nebula Award for Best Novel.[4] It is the first installment of the Dune saga, and in 2003 was cited as the world's best-selling science fiction novel.[5][6]"),
new SearcherItem("Ender's Game", "Ender's Game is a 1985 military science fiction novel by American author Orson Scott Card. Set in Earth's future, the novel presents an imperiled mankind after two conflicts with the \"buggers\", an insectoid alien species. In preparation for an anticipated third invasion, children, including the novel's protagonist, Ender Wiggin, are trained from a very young age through increasingly difficult games including some in zero gravity, where Ender's tactical genius is revealed."),
new SearcherItem("The Time Machine", "The Time Machine is a science fiction novel by H. G. Wells, published in 1895 and written as a frame narrative. The work is generally credited with the popularization of the concept of time travel by using a vehicle that allows an operator to travel purposely and selectively forwards or backwards in time. The term \"time machine\", coined by Wells, is now almost universally used to refer to such a vehicle.[1]"),
new SearcherItem("War of the Worlds", "The War of the Worlds is a science fiction novel by English author H. G. Wells first serialised in 1897 by Pearson's Magazine in the UK and by Cosmopolitan magazine in the US. The novel's first appearance in hardcover was in 1898 from publisher William Heinemann of London. Written between 1895 and 1897,[2] it is one of the earliest stories that detail a conflict between mankind and an extraterrestrial race.[3] The novel is the first-person narrative of both an unnamed protagonist in Surrey and of his younger brother in London as southern England is invaded by Martians. The novel is one of the most commented-on works in the science fiction canon.[4]"),
}),
})
};
var foodItems = new List<SearcherItem>
{
new SearcherItem("Food", "Food Category", new List<SearcherItem>
{
new SearcherItem("Vegetables", "Vegetables Category", new List<SearcherItem>
{
new SearcherItem("Lettuce", "Lettuce (Lactuca sativa) is an annual plant of the daisy family, Asteraceae. It is most often grown as a leaf vegetable, but sometimes for its stem and seeds. Lettuce is most often used for salads, although it is also seen in other kinds of food, such as soups, sandwiches and wraps; it can also be grilled.[3] One variety, the woju (莴苣), or asparagus lettuce (celtuce), is grown for its stems, which are eaten either raw or cooked"),
new SearcherItem("Avocado", "The avocado (Persea americana) is a tree long thought to have originated in South Central Mexico,[2] classified as a member of the flowering plant family Lauraceae.[3] Recent archaeological research produced evidence that the avocado was present in Peru as long as 8,000 to 15,000 years ago.[4] Avocado (also alligator pear) refers to the tree's fruit, which is botanically a large berry containing a single large seed.[5]"),
new SearcherItem("Cucumber", "Cucumber (Cucumis sativus) is a widely cultivated plant in the gourd family, Cucurbitaceae. It is a creeping vine that bears cucumiform fruits that are used as vegetables. There are three main varieties of cucumber: slicing, pickling, and seedless. Within these varieties, several cultivars have been created. In North America, the term \"wild cucumber\" refers to plants in the genera Echinocystis and Marah, but these are not closely related. The cucumber is originally from South Asia, but now grows on most continents. Many different types of cucumber are traded on the global market."),
new SearcherItem("Cauliflower", "Cauliflower is one of several vegetables in the species Brassica oleracea in the genus Brassica, which is in the family Brassicaceae. It is an annual plant that reproduces by seed. Typically, only the head is eaten the edible white flesh sometimes called \"curd\" (similar appearance to cheese curd).[1] The cauliflower head is composed of a white inflorescence meristem. Cauliflower heads resemble those in broccoli, which differs in having flower buds as the edible portion. Brassica oleracea also includes broccoli, brussels sprouts, cabbage, collard greens, and kale, collectively called \"cole\" crops,[2] though they are of different cultivar groups."),
new SearcherItem("Broccoli", "Broccoli is an edible green plant in the cabbage family whose large flowering head is eaten as a vegetable."),
new SearcherItem("Artichoke", "The globe artichoke (Cynara cardunculus var. scolymus)[1] is a variety of a species of thistle cultivated as a food."),
}),
new SearcherItem("Fruits", "Fruits Category", new List<SearcherItem>
{
new SearcherItem("Blueberry", "Blueberries (Vaccinium corymbosum) are perennial flowering plants with indigo-colored berries. They are classified in the section Cyanococcus within the genus Vaccinium. Vaccinium also includes cranberries, bilberries and grouseberries.[1] Commercial \"blueberries\" are native to North America, and the \"highbush\" varieties were not introduced into Europe until the 1930s.[2]"),
new SearcherItem("Grapes", "A grape is a fruit, botanically a berry, of the deciduous woody vines of the flowering plant genus Vitis."),
new SearcherItem("Strawberry", "The garden strawberry (or simply strawberry; Fragaria × ananassa)[1] is a widely grown hybrid species of the genus Fragaria, collectively known as the strawberries. It is cultivated worldwide for its fruit. The fruit is widely appreciated for its characteristic aroma, bright red color, juicy texture, and sweetness. It is consumed in large quantities, either fresh or in such prepared foods as preserves, juice, pies, ice creams, milkshakes, and chocolates. Artificial strawberry flavorings and aromas are also widely used in many products like lip gloss, candy, hand sanitizers, perfume, and many others."),
new SearcherItem("Tomato", "The tomato (see pronunciation) is the edible fruit of Solanum lycopersicum,[2] commonly known as a tomato plant, which belongs to the nightshade family, Solanaceae.[1]"),
new SearcherItem("Cranberry", "Cranberries are a group of evergreen dwarf shrubs or trailing vines in the subgenus Oxycoccus of the genus Vaccinium."),
new SearcherItem("Pumpkin", "A pumpkin is a cultivar of a squash plant, most commonly of Cucurbita pepo, that is round, with smooth, slightly ribbed skin, and deep yellow to orange coloration. The thick shell contains the seeds and pulp. Some exceptionally large cultivars of squash with similar appearance have also been derived from Cucurbita maxima."),
new SearcherItem("Apple", "The apple tree (Malus pumila, commonly and erroneously called Malus domestica) is a deciduous tree in the rose family best known for its sweet, pomaceous fruit, the apple")
})
})
};
var databaseDir = Application.dataPath + "/../Library/Searcher/Tests";
// Demonstrating creation and then loading from disk of database.
SearcherDatabase.Create(bookItems, databaseDir + "/Books");
var bookDatabase = SearcherDatabase.Load(databaseDir + "/Books");
var foodDatabase = SearcherDatabase.Create(foodItems, databaseDir + "/Foods");
m_Searcher = new Searcher(new[]{ foodDatabase, bookDatabase }, "Popup Example");
}
[OneTimeTearDown]
public void Cleanup()
{
}
[TestCase("Japanese", 1)]
[TestCase("books", 1)]
[TestCase("C", 5)]
public void SingleTermSearch(string term, int expectedResultCount)
{
Assert.IsTrue(term != null, "Term must not be null");
var items = m_Searcher.Search(term);
Assert.AreEqual(expectedResultCount, items.Count(), "Term '" + term + "' must match at least one data stub.");
Assert.IsTrue(items.First().Name.ToLower().StartsWith(term.ToLower()));
}
[TestCase("The Time Machine", 1)]
[TestCase("Books Cook", 2)]
[TestCase("Food Vegetables Lett", 3)]
public void MultipleTermsSearch(string term, int expectedResultCount)
{
Assert.IsTrue(term != null, "Term must not be null");
var items = m_Searcher.Search(term);
Assert.AreEqual(expectedResultCount, items.Count(), "Term '" + term + "' must match at least one data stub.");
}
}
class MatchTests
{
static TestCaseData[] Make(string prefix, TestCaseData[] input)
{
foreach (var data in input)
data.SetName($"{prefix} - '{data.Arguments[1]}' should{((bool)data.Arguments[2] ? "" : " NOT")} match '{data.Arguments[0]}'");
return input;
}
static TestCaseData[] BasicCases() => Make("Basic ", new[]
{
new TestCaseData( "transform position x", "traxxxx", false),
new TestCaseData( "transform position x", "trxxxx pos", true),
new TestCaseData( "transform position x", "transform xx", true),
new TestCaseData( "transform position x", "transform", true),
new TestCaseData( "transform position x", "transform pos", true),
new TestCaseData( "transform position x", "transform pos", true),
new TestCaseData( "transform position x", "pos", true),
});
static TestCaseData[] PrefixCases() => Make("Prefix", new[]
{
new TestCaseData( "transform position x", "tr pos", true),
new TestCaseData( "transform position x", "tr pos x", true),
new TestCaseData( "transform position x", "tr pos x", true),
new TestCaseData( "transform position x", "tr pos x", true),
new TestCaseData( "transform position x", "ta pos", true),
new TestCaseData( "transform position x", "ta pos", true),
new TestCaseData( "transform position x", "ta pout", false),
});
static TestCaseData[] CamelCaseSplitCases() => Make("CamelCase", new[]
{
new TestCaseData("GameObject OnDestroy", "o x", true),
new TestCaseData("GameObject OnDestroy", "object x", true),
});
static TestCaseData[] AbreviationCases() => Make("AbreviationCase", new[]
{
new TestCaseData("GameObject OnDestroy", "go", true),
new TestCaseData("GameObject OnDestroy", "good", true),
new TestCaseData("GameObject OnDestroy", "gof", false),
});
[TestCaseSource(nameof(BasicCases))]
[TestCaseSource(nameof(PrefixCases))]
[TestCaseSource(nameof(CamelCaseSplitCases))]
[TestCaseSource(nameof(AbreviationCases))]
public void Match(string path, string query, bool expectedResult)
{
Assume.That(query, Does.Not.StartWith(" "), () => "Queries start/end spaces are supposed to be trimmed earlier");
Assume.That(query, Does.Not.EndWith(" "), () => "Queries start/end spaces are supposed to be trimmed earlier");
// Reflection for:
// IList<string> tokenizedQuery = SearcherDatabase.Tokenize(query);
var t = typeof(SearcherDatabase).GetMethod("Tokenize", BindingFlags.Static | BindingFlags.NonPublic);
Assert.IsNotNull(t, "Tokenize method not found");
var tokenizeParameters = new object[] { query };
IList<string> tokenizedQuery = (IList<string>)t?.Invoke(null, tokenizeParameters);
// Reflection for:
// bool actual = SearcherDatabase.Match(tokenizedQuery, path, out score);
Type sdType = typeof(SearcherDatabase);
var m = sdType.GetMethod("Match", BindingFlags.NonPublic | BindingFlags.Instance, null,
new [] {tokenizedQuery.GetType(), path.GetType(), typeof(float).MakeByRefType()}, null);
Assert.IsNotNull(m, "Match method not found");
var searcherDatabase = new SearcherDatabase(new SearcherItem[] {new SearcherItem(path)});
searcherDatabase.BuildIndex();
var parameters = new object[] { tokenizedQuery, path, null };
bool actual = (bool)m.Invoke(searcherDatabase, parameters);
Assert.AreEqual(expectedResult, actual);
}
[UsedImplicitly]
static IEnumerable<TestCaseData> FilterCases
{
get
{
var origin = new List<SearcherItem> { new SearcherItem("transform") };
var expectedResult = new List<SearcherItem> { new SearcherItem("transform") };
var query = "tr";
yield return new TestCaseData(origin, query, null, expectedResult).SetName("Query with no filter");
origin = new List<SearcherItem> { new SearcherItem("transform") };
expectedResult = new List<SearcherItem>();
query = "tr";
Func<string, SearcherItem, bool> filter = (q, item) => !item.Name.StartsWith("trans", StringComparison.Ordinal);
yield return new TestCaseData(origin, query, filter, expectedResult).SetName("Query with filter");
origin = new List<SearcherItem> { new SearcherItem("Transformations", "", new List<SearcherItem> {
new SearcherItem("Position"),
new SearcherItem("Rotation")
})};
expectedResult = new List<SearcherItem> { new SearcherItem("Transformations", "", new List<SearcherItem> {
new SearcherItem("Position"),
new SearcherItem("Rotation")
}), new SearcherItem("Rotation") };
query = "";
filter = (q, item) => !item.Name.StartsWith("Pos", StringComparison.Ordinal);
yield return new TestCaseData(origin, query, filter, expectedResult).SetName("No query with filter. (Invoked at Setup)");
}
}
[TestCaseSource(nameof(FilterCases))]
public void MatchWithFilter(List<SearcherItem> items, string query, Func<string, SearcherItem, bool> filter, List<SearcherItem> expectedResult)
{
var db = SearcherDatabase.Create(items, "", false);
db.MatchFilter = filter;
var result = db.Search(query, out _);
Assert.That(result, Is.SearcherItemCollectionEquivalent(expectedResult));
}
}
}

View File

@@ -0,0 +1,17 @@
{
"name": "Unity.Searcher.EditorTests",
"references": [
"Unity.Searcher.Editor",
"Unity.Searcher.EditorSamples"
],
"optionalUnityReferences": [
"TestAssemblies"
],
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": []
}

View File

@@ -0,0 +1,8 @@
This package contains third-party software components governed by the license(s) indicated below:
---------
Component Name: Lucene
License Type: Apache 2.0
http://www.apache.org/licenses/LICENSE-2.0

View File

@@ -0,0 +1,26 @@
{
"name": "com.unity.searcher",
"displayName": "Searcher",
"version": "4.3.2",
"unity": "2019.1",
"description": "General search window for use in the Editor. First target use is for GraphView node search.",
"keywords": [
"search",
"searcher"
],
"upmCi": {
"footprint": "446e57c3161c2a422ead18b927f81939131b5531"
},
"repository": {
"url": "https://github.cds.internal.unity3d.com/unity/com.unity.searcher.git",
"type": "git",
"revision": "798bfb84c8f09f7788a3a88c3c16d72c50451530"
},
"samples": [
{
"displayName": "Searchers Samples",
"description": "Some Searcher's basic examples",
"path": "Samples~"
}
]
}