diff --git a/BeefLibs/corlib/src/Windows.bf b/BeefLibs/corlib/src/Windows.bf index b94d61c0..7a3a135c 100644 --- a/BeefLibs/corlib/src/Windows.bf +++ b/BeefLibs/corlib/src/Windows.bf @@ -1131,7 +1131,6 @@ namespace System public function [CallingConvention(.Stdcall)] HResult(COM_IFileDialogEvents* self, COM_IFileDialog* fileDialog, FDE_SHAREVIOLATION_RESPONSE* pResponse) OnShareViolation; public function [CallingConvention(.Stdcall)] HResult(COM_IFileDialogEvents* self, COM_IFileDialog* fileDialog) OnTypeChange; public function [CallingConvention(.Stdcall)] HResult(COM_IFileDialogEvents* self, COM_IFileDialog* fileDialog, COM_IShellItem* shellItem, FDE_OVERWRITE_RESPONSE* response) OnOverwrite; - } } @@ -1159,7 +1158,6 @@ namespace System public function [CallingConvention(.Stdcall)] HResult(COM_IShellItem* self, SIGDN sigdnName, out char16* ppszName) GetDisplayName; public function [CallingConvention(.Stdcall)] HResult(COM_IShellItem* self, uint sfgaoMask, out uint psfgaoAttribs) GetAttributes; public function [CallingConvention(.Stdcall)] HResult(COM_IShellItem* self, COM_IShellItem* psi, uint32 hint, out int32 piOrder) Compare; - } public new VTable* VT { diff --git a/IDE/src/Compiler/BfParser.bf b/IDE/src/Compiler/BfParser.bf index ae592597..5bf14ae8 100644 --- a/IDE/src/Compiler/BfParser.bf +++ b/IDE/src/Compiler/BfParser.bf @@ -78,6 +78,7 @@ namespace IDE.Compiler public bool mCancelled; public int32 mTextVersion = -1; public bool mIsUserRequested; + public bool mDoFuzzyAutoComplete; public Stopwatch mStopwatch ~ delete _; public ProfileInstance mProfileInstance ~ _.Dispose(); } @@ -128,7 +129,7 @@ namespace IDE.Compiler static extern char8* BfParser_GetDebugExpressionAt(void* bfParser, int32 cursorIdx); [CallingConvention(.Stdcall), CLink] - static extern void* BfParser_CreateResolvePassData(void* bfSystem, int32 resolveType); + static extern void* BfParser_CreateResolvePassData(void* bfSystem, int32 resolveType, bool doFuzzyAutoComplete); [CallingConvention(.Stdcall), CLink] static extern bool BfParser_BuildDefs(void* bfParser, void* bfPassInstance, void* bfResolvePassData, bool fullRefresh); @@ -256,10 +257,10 @@ namespace IDE.Compiler BfParser_GenerateAutoCompletionFrom(mNativeBfParser, srcPosition); } - public BfResolvePassData CreateResolvePassData(ResolveType resolveType = ResolveType.Autocomplete) + public BfResolvePassData CreateResolvePassData(ResolveType resolveType = ResolveType.Autocomplete, bool doFuzzyAutoComplete = false) { var resolvePassData = new BfResolvePassData(); - resolvePassData.mNativeResolvePassData = BfParser_CreateResolvePassData(mNativeBfParser, (int32)resolveType); + resolvePassData.mNativeResolvePassData = BfParser_CreateResolvePassData(mNativeBfParser, (int32)resolveType, doFuzzyAutoComplete); return resolvePassData; } diff --git a/IDE/src/Compiler/BfResolvePassData.bf b/IDE/src/Compiler/BfResolvePassData.bf index 9d7ebccc..eb48a844 100644 --- a/IDE/src/Compiler/BfResolvePassData.bf +++ b/IDE/src/Compiler/BfResolvePassData.bf @@ -94,10 +94,10 @@ namespace IDE.Compiler BfResolvePassData_SetDocumentationRequest(mNativeResolvePassData, entryName); } - public static BfResolvePassData Create(ResolveType resolveType = ResolveType.Autocomplete) + public static BfResolvePassData Create(ResolveType resolveType = ResolveType.Autocomplete, bool doFuzzyAutoComplete = false) { var resolvePassData = new BfResolvePassData(); - resolvePassData.mNativeResolvePassData = BfParser.[Friend]BfParser_CreateResolvePassData(null, (int32)resolveType); + resolvePassData.mNativeResolvePassData = BfParser.[Friend]BfParser_CreateResolvePassData(null, (int32)resolveType, doFuzzyAutoComplete); return resolvePassData; } } diff --git a/IDE/src/Settings.bf b/IDE/src/Settings.bf index 30e51db1..dab259bd 100644 --- a/IDE/src/Settings.bf +++ b/IDE/src/Settings.bf @@ -604,7 +604,7 @@ namespace IDE No, Yes, BackupOnly - } + } public List mFonts = new .() ~ DeleteContainerAndItems!(_); public float mFontSize = 12; @@ -613,6 +613,7 @@ namespace IDE public bool mAutoCompleteRequireTab = false; public bool mAutoCompleteOnEnter = true; public bool mAutoCompleteShowDocumentation = true; + public bool mFuzzyAutoComplete = false; public bool mShowLocatorAnim = true; public bool mHiliteCursorReferences = true; public bool mLockEditing; @@ -640,6 +641,7 @@ namespace IDE sd.Add("AutoCompleteRequireTab", mAutoCompleteRequireTab); sd.Add("AutoCompleteOnEnter", mAutoCompleteOnEnter); sd.Add("AutoCompleteShowDocumentation", mAutoCompleteShowDocumentation); + sd.Add("FuzzyAutoComplete", mFuzzyAutoComplete); sd.Add("ShowLocatorAnim", mShowLocatorAnim); sd.Add("HiliteCursorReferences", mHiliteCursorReferences); sd.Add("LockEditing", mLockEditing); @@ -670,6 +672,7 @@ namespace IDE sd.Get("AutoCompleteRequireTab", ref mAutoCompleteRequireTab); sd.Get("AutoCompleteOnEnter", ref mAutoCompleteOnEnter); sd.Get("AutoCompleteShowDocumentation", ref mAutoCompleteShowDocumentation); + sd.Get("FuzzyAutoComplete", ref mFuzzyAutoComplete); sd.Get("ShowLocatorAnim", ref mShowLocatorAnim); sd.Get("HiliteCursorReferences", ref mHiliteCursorReferences); sd.Get("LockEditing", ref mLockEditing); diff --git a/IDE/src/ui/AutoComplete.bf b/IDE/src/ui/AutoComplete.bf index 88fb6d2f..eeb42bfb 100644 --- a/IDE/src/ui/AutoComplete.bf +++ b/IDE/src/ui/AutoComplete.bf @@ -382,6 +382,8 @@ namespace IDE.ui public String mEntryInsert; public String mDocumentation; public Image mIcon; + public List mMatchIndices; + public int32 mScore; public float Y { @@ -401,8 +403,41 @@ namespace IDE.ui g.Draw(mIcon, 0, 0); g.SetFont(IDEApp.sApp.mCodeFont); - g.DrawString(mEntryDisplay, GS!(20), 0); - } + + float offset = GS!(20); + + int index = 0; + for(char32 c in mEntryDisplay.DecodedChars) + loop: + { + if(mMatchIndices?.Contains((uint8)index) == true) + { + g.PushColor(DarkTheme.COLOR_MENU_FOCUSED); + defer:loop g.PopColor(); + } + + let str = StringView(mEntryDisplay, index, @c.NextIndex - index); + + g.DrawString(str, offset, 0); + + offset += IDEApp.sApp.mCodeFont.GetWidth(str); + + index = @c.NextIndex; + } + } + + public void SetMatches(Span matchIndices) + { + mMatchIndices?.Clear(); + + if (!matchIndices.IsEmpty) + { + if(mMatchIndices == null) + mMatchIndices = new:(mAutoCompleteListWidget.mAlloc) List(matchIndices.Length); + + mMatchIndices.AddRange(matchIndices); + } + } } class Content : Widget @@ -602,8 +637,8 @@ namespace IDE.ui mMaxWidth = Math.Max(mMaxWidth, entryWidth); }*/ } - - public void AddEntry(StringView entryType, StringView entryDisplay, Image icon, StringView entryInsert = default, StringView documentation = default) + + public void AddEntry(StringView entryType, StringView entryDisplay, Image icon, StringView entryInsert = default, StringView documentation = default, List matchIndices = null) { var entryWidget = new:mAlloc EntryWidget(); entryWidget.mAutoCompleteListWidget = this; @@ -615,10 +650,12 @@ namespace IDE.ui entryWidget.mDocumentation = new:mAlloc String(documentation); entryWidget.mIcon = icon; + entryWidget.SetMatches(matchIndices); + UpdateEntry(entryWidget, mEntryList.Count); mEntryList.Add(entryWidget); //mScrollContent.AddWidget(entryWidget); - } + } public void EnsureSelectionVisible() { @@ -1574,6 +1611,49 @@ namespace IDE.ui mInvokeWidget.mIgnoreMove += ignoreMove ? 1 : -1; } + // IDEHelper/third_party/FtsFuzzyMatch.h + [CallingConvention(.Stdcall), CLink] + static extern bool fts_fuzzy_match(char8* pattern, char8* str, ref int32 outScore, uint8* matches, int maxMatches); + + /// Checks whether the given entry matches the filter and updates its score and match indices accordingly. + bool DoesFilterMatchFuzzy(AutoCompleteListWidget.EntryWidget entry, String filter) + { + if (filter.Length == 0) + return true; + + if (filter.Length > entry.mEntryDisplay.Length) + return false; + + int32 score = 0; + uint8[256] matches = ?; + + if (!fts_fuzzy_match(filter.CStr(), entry.mEntryDisplay.CStr(), ref score, &matches, matches.Count)) + { + entry.SetMatches(Span(null, 0)); + entry.mScore = score; + return false; + } + + // Should be the amount of Unicode-codepoints in filter though it' probably faster to do it this way + int matchesLength = 0; + + for (uint8 i = 0;; i++) + { + uint8 matchIndex = matches[i]; + + if ((matchIndex == 0 && i != 0) || i == uint8.MaxValue) + { + matchesLength = i; + break; + } + } + + entry.SetMatches(Span(&matches, matchesLength)); + entry.mScore = score; + + return true; + } + bool DoesFilterMatch(String entry, String filter) { if (filter.Length == 0) @@ -1640,6 +1720,9 @@ namespace IDE.ui //return strnicmp(filter, initialStr, filterLen) == 0; } + [LinkName("_stricmp")] + static extern int32 stricmp(char8* lhs, char8* rhs); + void UpdateData(String selectString, bool changedAfterInfo) { if ((mInsertEndIdx != -1) && (mInsertEndIdx < mInsertStartIdx)) @@ -1692,14 +1775,21 @@ namespace IDE.ui if (curString == ".") curString.Clear(); + bool doFuzzyAutoComplete = gApp.mSettings.mEditorSettings.mFuzzyAutoComplete; + for (int i < mAutoCompleteListWidget.mFullEntryList.Count) { var entry = mAutoCompleteListWidget.mFullEntryList[i]; - //if (String.Compare(entry.mEntryDisplay, 0, curString, 0, curString.Length, true) == 0) - if (DoesFilterMatch(entry.mEntryDisplay, curString)) + + if (doFuzzyAutoComplete && DoesFilterMatchFuzzy(entry, curString)) { mAutoCompleteListWidget.mEntryList.Add(entry); - mAutoCompleteListWidget.UpdateEntry(entry, visibleCount); + visibleCount++; + } + else if (!doFuzzyAutoComplete && DoesFilterMatch(entry.mEntryDisplay, curString)) + { + mAutoCompleteListWidget.mEntryList.Add(entry); + mAutoCompleteListWidget.UpdateEntry(entry, visibleCount); visibleCount++; } else @@ -1708,6 +1798,25 @@ namespace IDE.ui } } + if (doFuzzyAutoComplete) + { + // sort entries because the scores probably have changed + mAutoCompleteListWidget.mEntryList.Sort(scope (left, right) => + { + if (left.mScore > right.mScore) + return -1; + else if (left.mScore < right.mScore) + return 1; + else + return ((stricmp(left.mEntryDisplay.CStr(), right.mEntryDisplay.CStr()) < 0) ? -1 : 1); + }); + + for (int i < mAutoCompleteListWidget.mEntryList.Count) + { + mAutoCompleteListWidget.UpdateEntry(mAutoCompleteListWidget.mEntryList[i], i); + } + } + if ((visibleCount == 0) && (mInvokeSrcPositions == null)) { mPopulating = false; @@ -1853,6 +1962,7 @@ namespace IDE.ui public void UpdateInfo(String info) { + List matchIndices = new:ScopedAlloc! .(256); for (var entryView in info.Split('\n')) { StringView entryType = StringView(entryView); @@ -1863,13 +1973,35 @@ namespace IDE.ui entryDisplay = StringView(entryView, tabPos + 1); entryType = StringView(entryType, 0, tabPos); } + + StringView matches = default; + int matchesPos = entryDisplay.IndexOf('\x02'); + matchIndices.Clear(); + if (matchesPos != -1) + { + matches = StringView(entryDisplay, matchesPos + 1); + entryDisplay = StringView(entryDisplay, 0, matchesPos); + + for(var sub in matches.Split(',')) + { + if(sub.StartsWith('X')) + break; + + var result = int64.Parse(sub, .HexNumber); + + Debug.Assert((result case .Ok(let value)) && value <= uint8.MaxValue); + + // TODO(FUZZY): we could save start and length instead of single chars + matchIndices.Add((uint8)result.Value); + } + } StringView documentation = default; - int docPos = entryDisplay.IndexOf('\x03'); + int docPos = matches.IndexOf('\x03'); if (docPos != -1) { - documentation = StringView(entryDisplay, docPos + 1); - entryDisplay = StringView(entryDisplay, 0, docPos); + documentation = StringView(matches, docPos + 1); + matches = StringView(matches, 0, docPos); } StringView entryInsert = default; @@ -1892,15 +2024,27 @@ namespace IDE.ui case "select": default: { - if ((!documentation.IsEmpty) && (mAutoCompleteListWidget != null)) + if (((!documentation.IsEmpty) || (!matchIndices.IsEmpty)) && (mAutoCompleteListWidget != null)) { while (entryIdx < mAutoCompleteListWidget.mEntryList.Count) { let entry = mAutoCompleteListWidget.mEntryList[entryIdx]; if ((entry.mEntryDisplay == entryDisplay) && (entry.mEntryType == entryType)) { - if (entry.mDocumentation == null) + if (!matchIndices.IsEmpty) + { + if (entry.mMatchIndices == null) + entry.mMatchIndices = new:(mAutoCompleteListWidget.[Friend]mAlloc) List(matchIndices.GetEnumerator()); + else + { + entry.mMatchIndices.Clear(); + entry.mMatchIndices.AddRange(matchIndices); + } + } + + if ((!documentation.IsEmpty) && entry.mDocumentation == null) entry.mDocumentation = new:(mAutoCompleteListWidget.[Friend]mAlloc) String(documentation); + break; } entryIdx++; @@ -1982,9 +2126,9 @@ namespace IDE.ui InvokeWidget oldInvokeWidget = null; String selectString = null; + List matchIndices = new:ScopedAlloc! .(256); for (var entryView in info.Split('\n')) { - Image entryIcon = null; StringView entryType = StringView(entryView); int tabPos = entryType.IndexOf('\t'); @@ -1995,12 +2139,34 @@ namespace IDE.ui entryType = StringView(entryType, 0, tabPos); } + StringView matches = default; + int matchesPos = entryDisplay.IndexOf('\x02'); + matchIndices.Clear(); + if (matchesPos != -1) + { + matches = StringView(entryDisplay, matchesPos + 1); + entryDisplay = StringView(entryDisplay, 0, matchesPos); + + for(var sub in matches.Split(',')) + { + if(sub.StartsWith('X')) + break; + + var result = int64.Parse(sub, .HexNumber); + + Debug.Assert((result case .Ok(let value)) && value <= uint8.MaxValue); + + // TODO(FUZZY): we could save start and length instead of single chars + matchIndices.Add((uint8)result.Value); + } + } + StringView documentation = default; - int docPos = entryDisplay.IndexOf('\x03'); + int docPos = matches.IndexOf('\x03'); if (docPos != -1) { - documentation = StringView(entryDisplay, docPos + 1); - entryDisplay = StringView(entryDisplay, 0, docPos); + documentation = StringView(matches, docPos + 1); + matches = StringView(matches, 0, docPos); } StringView entryInsert = default; @@ -2129,7 +2295,7 @@ namespace IDE.ui if (!mInvokeOnly) { mIsFixit |= entryType == "fixit"; - mAutoCompleteListWidget.AddEntry(entryType, entryDisplay, entryIcon, entryInsert, documentation); + mAutoCompleteListWidget.AddEntry(entryType, entryDisplay, entryIcon, entryInsert, documentation, matchIndices); } } } diff --git a/IDE/src/ui/SettingsDialog.bf b/IDE/src/ui/SettingsDialog.bf index 4ada4d19..b82bd49a 100644 --- a/IDE/src/ui/SettingsDialog.bf +++ b/IDE/src/ui/SettingsDialog.bf @@ -98,6 +98,7 @@ namespace IDE.ui AddPropertiesItem(category, "Autocomplete Require Tab", "mAutoCompleteRequireTab"); AddPropertiesItem(category, "Autocomplete on Enter", "mAutoCompleteOnEnter"); AddPropertiesItem(category, "Autocomplete Show Documentation", "mAutoCompleteShowDocumentation"); + AddPropertiesItem(category, "Fuzzy Autocomplete", "mFuzzyAutoComplete"); AddPropertiesItem(category, "Show Locator Animation", "mShowLocatorAnim"); AddPropertiesItem(category, "Hilite Symbol at Cursor", "mHiliteCursorReferences"); diff --git a/IDE/src/ui/SourceViewPanel.bf b/IDE/src/ui/SourceViewPanel.bf index 8e75ddb1..7d09f003 100644 --- a/IDE/src/ui/SourceViewPanel.bf +++ b/IDE/src/ui/SourceViewPanel.bf @@ -548,6 +548,11 @@ namespace IDE.ui public ~this() { + if (mProjectSource?.mEditData?.HasTextChanged() == true) + { + mProjectSource.ClearEditData(); + } + if (mInPostRemoveUpdatePanels) { //Debug.WriteLine("Removing sourceViewPanel from mPostRemoveUpdatePanel {0} in ~this ", this); @@ -623,6 +628,7 @@ namespace IDE.ui if (gApp.mDbgPerfAutocomplete) resolveParams.mProfileInstance = Profiler.StartSampling("Autocomplete").GetValueOrDefault(); resolveParams.mIsUserRequested = options.HasFlag(.UserRequested); + resolveParams.mDoFuzzyAutoComplete = gApp.mSettings.mEditorSettings.mFuzzyAutoComplete; Classify(.Autocomplete, resolveParams); if (!resolveParams.mInDeferredList) delete resolveParams; @@ -1854,7 +1860,9 @@ namespace IDE.ui /*else (!isFullClassify) -- do we ever need to do this? parser.SetCursorIdx(mEditWidget.mEditWidgetContent.CursorTextPos);*/ - var resolvePassData = parser.CreateResolvePassData(resolveType); + bool doFuzzyAutoComplete = resolveParams?.mDoFuzzyAutoComplete ?? false; + + var resolvePassData = parser.CreateResolvePassData(resolveType, doFuzzyAutoComplete); if (resolveParams != null) { if (resolveParams.mLocalId != -1) diff --git a/IDEHelper/BeefProj.toml b/IDEHelper/BeefProj.toml index 7f6d1208..f773ae51 100644 --- a/IDEHelper/BeefProj.toml +++ b/IDEHelper/BeefProj.toml @@ -508,3 +508,11 @@ Path = "X86Target.h" [[ProjectFolder.Items]] Type = "Source" Path = "X86XmmInfo.cpp" + +[[ProjectFolder.Items]] +Type = "Folder" +Name = "third_party" + +[[ProjectFolder.Items.Items]] +Type = "Source" +Path = "third_party/FtsFuzzyMatch.h" diff --git a/IDEHelper/Compiler/BfAutoComplete.cpp b/IDEHelper/Compiler/BfAutoComplete.cpp index d3474719..105ed529 100644 --- a/IDEHelper/Compiler/BfAutoComplete.cpp +++ b/IDEHelper/Compiler/BfAutoComplete.cpp @@ -6,6 +6,9 @@ #include "BfFixits.h" #include "BfResolvedTypeUtils.h" +#define FTS_FUZZY_MATCH_IMPLEMENTATION +#include "../third_party/FtsFuzzyMatch.h" + #pragma warning(disable:4996) using namespace llvm; @@ -16,6 +19,7 @@ AutoCompleteBase::AutoCompleteBase() { mIsGetDefinition = false; mIsAutoComplete = true; + mDoFuzzyAutoComplete = false; mInsertStartIdx = -1; mInsertEndIdx = -1; } @@ -25,22 +29,70 @@ AutoCompleteBase::~AutoCompleteBase() Clear(); } -AutoCompleteEntry* AutoCompleteBase::AddEntry(const AutoCompleteEntry& entry, const StringImpl& filter) +inline void UpdateEntryMatchindices(uint8* matches, AutoCompleteEntry& entry) { - if ((!DoesFilterMatch(entry.mDisplay, filter.c_str())) || (entry.mNamePrefixCount < 0)) - return NULL; - return AddEntry(entry); + if (matches[0] != UINT8_MAX) + { + // Count entries in matches + // Note: entry.mMatchesLength should be the amount of unicode-codepoints in the filter + for (uint8 i = 0;; i++) + { + uint8 matchIndex = matches[i]; + + if ((matchIndex == 0 && i != 0) || i == UINT8_MAX) + { + entry.mMatchesLength = i; + break; + } + } + + entry.mMatches = matches; + } + else + { + entry.mMatches = nullptr; + entry.mMatchesLength = 0; + } } -AutoCompleteEntry* AutoCompleteBase::AddEntry(const AutoCompleteEntry& entry, const char* filter) +AutoCompleteEntry* AutoCompleteBase::AddEntry(AutoCompleteEntry& entry, const StringImpl& filter) { - if ((!DoesFilterMatch(entry.mDisplay, filter)) || (entry.mNamePrefixCount < 0)) + uint8 matches[256]; + + if (!DoesFilterMatch(entry.mDisplay, filter.c_str(), entry.mScore, matches, 256) || (entry.mNamePrefixCount < 0)) return NULL; - return AddEntry(entry); + + UpdateEntryMatchindices(matches, entry); + + auto result = AddEntry(entry); + + // Reset matches because the array will be invalid after return + entry.mMatches = nullptr; + entry.mMatchesLength = 0; + + return result; +} + +AutoCompleteEntry* AutoCompleteBase::AddEntry(AutoCompleteEntry& entry, const char* filter) +{ + uint8 matches[256]; + + if (!DoesFilterMatch(entry.mDisplay, filter, entry.mScore, matches, 256) || (entry.mNamePrefixCount < 0)) + return NULL; + + UpdateEntryMatchindices(matches, entry); + + auto result = AddEntry(entry); + + // Reset matches because the array will be invalid after return + entry.mMatches = nullptr; + entry.mMatchesLength = 0; + + return result; } AutoCompleteEntry* AutoCompleteBase::AddEntry(const AutoCompleteEntry& entry) -{ +{ if (mEntriesSet.mAllocSize == 0) { mEntriesSet.Reserve(128); @@ -55,13 +107,16 @@ AutoCompleteEntry* AutoCompleteBase::AddEntry(const AutoCompleteEntry& entry) int size = (int)strlen(display) + 1; insertedEntry->mDisplay = (char*)mAlloc.AllocBytes(size); memcpy((char*)insertedEntry->mDisplay, display, size); + + insertedEntry->mMatches = (uint8*)mAlloc.AllocBytes(insertedEntry->mMatchesLength); + memcpy((char*)insertedEntry->mMatches, entry.mMatches, insertedEntry->mMatchesLength); } return insertedEntry; } -bool AutoCompleteBase::DoesFilterMatch(const char* entry, const char* filter) -{ +bool AutoCompleteBase::DoesFilterMatch(const char* entry, const char* filter, int& score, uint8* matches, int maxMatches) +{ if (mIsGetDefinition) { int entryLen = (int)strlen(entry); @@ -73,59 +128,71 @@ bool AutoCompleteBase::DoesFilterMatch(const char* entry, const char* filter) if (!mIsAutoComplete) return false; - if (filter[0] == 0) + matches[0] = UINT8_MAX; + + if (filter[0] == '\0') return true; int filterLen = (int)strlen(filter); int entryLen = (int)strlen(entry); - bool hasUnderscore = false; - bool checkInitials = filterLen > 1; - for (int i = 0; i < (int)filterLen; i++) - { - char c = filter[i]; - if (c == '_') - hasUnderscore = true; - else if (islower((uint8)filter[i])) - checkInitials = false; - } - - if (hasUnderscore) - return strnicmp(filter, entry, filterLen) == 0; - - char initialStr[256]; - char* initialStrP = initialStr; - - //String initialStr; - bool prevWasUnderscore = false; - - for (int entryIdx = 0; entryIdx < entryLen; entryIdx++) - { - char entryC = entry[entryIdx]; - - if (entryC == '_') - { - prevWasUnderscore = true; - continue; - } - - if ((entryIdx == 0) || (prevWasUnderscore) || (isupper((uint8)entryC) || (isdigit((uint8)entryC)))) - { - if (strnicmp(filter, entry + entryIdx, filterLen) == 0) - return true; - if (checkInitials) - *(initialStrP++) = entryC; - } - prevWasUnderscore = false; - - if (filterLen == 1) - break; // Don't check inners for single-character case - } - - if (!checkInitials) + if (filterLen > entryLen) return false; - *(initialStrP++) = 0; - return strnicmp(filter, initialStr, filterLen) == 0; + + if (mDoFuzzyAutoComplete) + { + return fts::fuzzy_match(filter, entry, score, matches, maxMatches); + } + else + { + bool hasUnderscore = false; + bool checkInitials = filterLen > 1; + for (int i = 0; i < (int)filterLen; i++) + { + char c = filter[i]; + if (c == '_') + hasUnderscore = true; + else if (islower((uint8)filter[i])) + checkInitials = false; + } + + if (hasUnderscore) + return strnicmp(filter, entry, filterLen) == 0; + + char initialStr[256]; + char* initialStrP = initialStr; + + //String initialStr; + bool prevWasUnderscore = false; + + for (int entryIdx = 0; entryIdx < entryLen; entryIdx++) + { + char entryC = entry[entryIdx]; + + if (entryC == '_') + { + prevWasUnderscore = true; + continue; + } + + if ((entryIdx == 0) || (prevWasUnderscore) || (isupper((uint8)entryC) || (isdigit((uint8)entryC)))) + { + if (strnicmp(filter, entry + entryIdx, filterLen) == 0) + return true; + if (checkInitials) + *(initialStrP++) = entryC; + } + prevWasUnderscore = false; + + if (filterLen == 1) + break; // Don't check inners for single-character case + } + + if (!checkInitials) + return false; + *(initialStrP++) = 0; + return strnicmp(filter, initialStr, filterLen) == 0; + } } void AutoCompleteBase::Clear() @@ -137,7 +204,7 @@ void AutoCompleteBase::Clear() ////////////////////////////////////////////////////////////////////////// -BfAutoComplete::BfAutoComplete(BfResolveType resolveType) +BfAutoComplete::BfAutoComplete(BfResolveType resolveType, bool doFuzzyAutoComplete) { mResolveType = resolveType; mModule = NULL; @@ -154,6 +221,8 @@ BfAutoComplete::BfAutoComplete(BfResolveType resolveType) (resolveType == BfResolveType_GoToDefinition); mIsAutoComplete = (resolveType == BfResolveType_Autocomplete); + mDoFuzzyAutoComplete = doFuzzyAutoComplete; + mGetDefinitionNode = NULL; mShowAttributeProperties = NULL; mIdentifierUsed = NULL; @@ -550,7 +619,9 @@ void BfAutoComplete::AddTypeDef(BfTypeDef* typeDef, const StringImpl& filter, bo return; } - if (!DoesFilterMatch(name.c_str(), filter.c_str())) + int score; + uint8 matches[256]; + if (!DoesFilterMatch(name.c_str(), filter.c_str(), score, matches, sizeof(matches))) return; auto type = mModule->ResolveTypeDef(typeDef, BfPopulateType_Declaration); @@ -1128,8 +1199,10 @@ void BfAutoComplete::AddExtensionMethods(BfTypeInstance* targetType, BfTypeInsta if (methodInstance == NULL) continue; + int score; + uint8 matches[256]; // Do filter match first- may be cheaper than generic validation - if (!DoesFilterMatch(methodDef->mName.c_str(), filter.c_str())) + if (!DoesFilterMatch(methodDef->mName.c_str(), filter.c_str(), score, matches, sizeof(matches))) continue; auto thisType = methodInstance->GetParamType(0); diff --git a/IDEHelper/Compiler/BfAutoComplete.h b/IDEHelper/Compiler/BfAutoComplete.h index 183bf514..6e610512 100644 --- a/IDEHelper/Compiler/BfAutoComplete.h +++ b/IDEHelper/Compiler/BfAutoComplete.h @@ -16,11 +16,16 @@ public: const char* mDisplay; const char* mDocumentation; int8 mNamePrefixCount; + int mScore; + uint8* mMatches; + uint8 mMatchesLength; public: AutoCompleteEntry() { mNamePrefixCount = 0; + mMatches = nullptr; + mMatchesLength = 0; } AutoCompleteEntry(const char* entryType, const char* display) @@ -29,6 +34,9 @@ public: mDisplay = display; mDocumentation = NULL; mNamePrefixCount = 0; + mScore = 0; + mMatches = nullptr; + mMatchesLength = 0; } AutoCompleteEntry(const char* entryType, const StringImpl& display) @@ -37,6 +45,9 @@ public: mDisplay = display.c_str(); mDocumentation = NULL; mNamePrefixCount = 0; + mScore = 0; + mMatches = nullptr; + mMatchesLength = 0; } AutoCompleteEntry(const char* entryType, const StringImpl& display, int namePrefixCount) @@ -45,8 +56,11 @@ public: mDisplay = display.c_str(); mDocumentation = NULL; mNamePrefixCount = (int8)namePrefixCount; + mScore = 0; + mMatches = nullptr; + mMatchesLength = 0; } - + bool operator==(const AutoCompleteEntry& other) const { return strcmp(mDisplay, other.mDisplay) == 0; @@ -97,12 +111,13 @@ public: bool mIsGetDefinition; bool mIsAutoComplete; + bool mDoFuzzyAutoComplete; int mInsertStartIdx; int mInsertEndIdx; - bool DoesFilterMatch(const char* entry, const char* filter); - AutoCompleteEntry* AddEntry(const AutoCompleteEntry& entry, const StringImpl& filter); - AutoCompleteEntry* AddEntry(const AutoCompleteEntry& entry, const char* filter); + bool DoesFilterMatch(const char* entry, const char* filter, int& score, uint8* matches, int maxMatches); + AutoCompleteEntry* AddEntry(AutoCompleteEntry& entry, const StringImpl& filter); + AutoCompleteEntry* AddEntry(AutoCompleteEntry& entry, const char* filter); AutoCompleteEntry* AddEntry(const AutoCompleteEntry& entry); AutoCompleteBase(); @@ -226,7 +241,7 @@ public: String ConstantToString(BfIRConstHolder* constHolder, BfIRValue id); public: - BfAutoComplete(BfResolveType resolveType = BfResolveType_Autocomplete); + BfAutoComplete(BfResolveType resolveType = BfResolveType_Autocomplete, bool doFuzzyAutoComplete = false); ~BfAutoComplete(); void SetModule(BfModule* module); diff --git a/IDEHelper/Compiler/BfCompiler.cpp b/IDEHelper/Compiler/BfCompiler.cpp index 7953d190..b1950a53 100644 --- a/IDEHelper/Compiler/BfCompiler.cpp +++ b/IDEHelper/Compiler/BfCompiler.cpp @@ -8026,9 +8026,13 @@ void BfCompiler::GenerateAutocompleteInfo() { entries.Add(&entry); } + std::sort(entries.begin(), entries.end(), [](AutoCompleteEntry* lhs, AutoCompleteEntry* rhs) { - return stricmp(lhs->mDisplay, rhs->mDisplay) < 0; + if (lhs->mScore == rhs->mScore) + return stricmp(lhs->mDisplay, rhs->mDisplay) < 0; + + return lhs->mScore > rhs->mScore; }); String docString; @@ -8043,6 +8047,25 @@ void BfCompiler::GenerateAutocompleteInfo() autoCompleteResultString += '@'; autoCompleteResultString += String(entry->mDisplay); + if (entry->mMatchesLength > 0) + { + autoCompleteResultString += "\x02"; + for (int i = 0; i < entry->mMatchesLength; i++) + { + int match = entry->mMatches[i]; + + // Need max 3 chars (largest Hex (FF) + '\0') + char buffer[3]; + + _itoa_s(match, buffer, 16); + + autoCompleteResultString += String(buffer); + autoCompleteResultString += ","; + } + + autoCompleteResultString += "X"; + } + if (entry->mDocumentation != NULL) { autoCompleteResultString += '\x03'; diff --git a/IDEHelper/Compiler/BfModuleTypeUtils.cpp b/IDEHelper/Compiler/BfModuleTypeUtils.cpp index b8f17a4f..33eb2122 100644 --- a/IDEHelper/Compiler/BfModuleTypeUtils.cpp +++ b/IDEHelper/Compiler/BfModuleTypeUtils.cpp @@ -2071,7 +2071,7 @@ void BfModule::UpdateCEEmit(CeEmitContext* ceEmitContext, BfTypeInstance* typeIn { for (int ifaceTypeId : ceEmitContext->mInterfaces) typeInstance->mCeTypeInfo->mPendingInterfaces.Add(ifaceTypeId); - + if (ceEmitContext->mEmitData.IsEmpty()) return; diff --git a/IDEHelper/Compiler/BfParser.cpp b/IDEHelper/Compiler/BfParser.cpp index 060fe028..49548b98 100644 --- a/IDEHelper/Compiler/BfParser.cpp +++ b/IDEHelper/Compiler/BfParser.cpp @@ -3898,13 +3898,13 @@ BF_EXPORT const char* BF_CALLTYPE BfParser_GetDebugExpressionAt(BfParser* bfPars return outString.c_str(); } -BF_EXPORT BfResolvePassData* BF_CALLTYPE BfParser_CreateResolvePassData(BfParser* bfParser, BfResolveType resolveType) +BF_EXPORT BfResolvePassData* BF_CALLTYPE BfParser_CreateResolvePassData(BfParser* bfParser, BfResolveType resolveType, bool doFuzzyAutoComplete) { auto bfResolvePassData = new BfResolvePassData(); bfResolvePassData->mResolveType = resolveType; bfResolvePassData->mParser = bfParser; if ((bfParser != NULL) && ((bfParser->mParserFlags & ParserFlag_Autocomplete) != 0)) - bfResolvePassData->mAutoComplete = new BfAutoComplete(resolveType); + bfResolvePassData->mAutoComplete = new BfAutoComplete(resolveType, doFuzzyAutoComplete); return bfResolvePassData; } diff --git a/IDEHelper/IDEHelper.vcxproj b/IDEHelper/IDEHelper.vcxproj index 10e6e389..f2e39a4f 100644 --- a/IDEHelper/IDEHelper.vcxproj +++ b/IDEHelper/IDEHelper.vcxproj @@ -400,6 +400,7 @@ + diff --git a/IDEHelper/IDEHelper.vcxproj.filters b/IDEHelper/IDEHelper.vcxproj.filters index 8055b890..5f026346 100644 --- a/IDEHelper/IDEHelper.vcxproj.filters +++ b/IDEHelper/IDEHelper.vcxproj.filters @@ -24,6 +24,9 @@ {83b97406-2f83-49ad-bbbc-3ff70ecda6bb} + + {d36777f2-b326-4a8c-84a3-5c2f39153f75} + @@ -399,5 +402,8 @@ Compiler + + third_party + \ No newline at end of file diff --git a/IDEHelper/Tests/src/Comptime.bf b/IDEHelper/Tests/src/Comptime.bf index 09831d41..117e3424 100644 --- a/IDEHelper/Tests/src/Comptime.bf +++ b/IDEHelper/Tests/src/Comptime.bf @@ -183,7 +183,7 @@ namespace Tests mStr.AppendF($"{name} {val}\n"); } } - + interface ISerializable { void Serialize(SerializationContext ctx); diff --git a/IDEHelper/third_party/FtsFuzzyMatch.h b/IDEHelper/third_party/FtsFuzzyMatch.h new file mode 100644 index 00000000..988adf82 --- /dev/null +++ b/IDEHelper/third_party/FtsFuzzyMatch.h @@ -0,0 +1,256 @@ +// LICENSE +// +// This software is dual-licensed to the public domain and under the following +// license: you are granted a perpetual, irrevocable license to copy, modify, +// publish, and distribute this file as you see fit. +// +// VERSION +// 0.2.0 (2017-02-18) Scored matches perform exhaustive search for best score +// 0.1.0 (2016-03-28) Initial release +// +// AUTHOR +// Forrest Smith +// +// NOTES +// Compiling +// You MUST add '#define FTS_FUZZY_MATCH_IMPLEMENTATION' before including this header in ONE source file to create implementation. +// +// fuzzy_match_simple(...) +// Returns true if each character in pattern is found sequentially within str +// +// fuzzy_match(...) +// Returns true if pattern is found AND calculates a score. +// Performs exhaustive search via recursion to find all possible matches and match with highest score. +// Scores values have no intrinsic meaning. Possible score range is not normalized and varies with pattern. +// Recursion is limited internally (default=10) to prevent degenerate cases (pattern="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") +// Uses uint8_t for match indices. Therefore patterns are limited to 256 characters. +// Score system should be tuned for YOUR use case. Words, sentences, file names, or method names all prefer different tuning. + + +#ifndef FTS_FUZZY_MATCH_H +#define FTS_FUZZY_MATCH_H + + +#include // uint8_t +#include // ::tolower, ::toupper +#include // memcpy + +#include + +#include "BeefySysLib/util/UTF8.h" +#include "BeefySysLib/third_party/utf8proc/utf8proc.h" + +// Public interface +namespace fts { + static bool fuzzy_match_simple(char const* pattern, char const* str); + static bool fuzzy_match(char const* pattern, char const* str, int& outScore); + static bool fuzzy_match(char const* pattern, char const* str, int& outScore, uint8_t* matches, int maxMatches); +} + +BF_EXPORT bool BF_CALLTYPE fts_fuzzy_match(char const* pattern, char const* str, int& outScore, uint8_t* matches, int maxMatches); + +#ifdef FTS_FUZZY_MATCH_IMPLEMENTATION +namespace fts { + + // Forward declarations for "private" implementation + namespace fuzzy_internal { + static bool fuzzy_match_recursive(const char* pattern, const char* str, int& outScore, const char* strBegin, + uint8_t const* srcMatches, uint8_t* newMatches, int maxMatches, int nextMatch, + int& recursionCount, int recursionLimit); + } + + // Public interface + static bool fuzzy_match_simple(char const* pattern, char const* str) { + while (*pattern != '\0' && *str != '\0') { + if (tolower(*pattern) == tolower(*str)) + ++pattern; + ++str; + } + + return *pattern == '\0' ? true : false; + } + + static bool fuzzy_match(char const* pattern, char const* str, int& outScore) { + + uint8_t matches[256]; + return fuzzy_match(pattern, str, outScore, matches, sizeof(matches)); + } + + static bool fuzzy_match(char const* pattern, char const* str, int& outScore, uint8_t* matches, int maxMatches) { + int recursionCount = 0; + int recursionLimit = 10; + + return fuzzy_internal::fuzzy_match_recursive(pattern, str, outScore, str, nullptr, matches, maxMatches, 0, recursionCount, recursionLimit); + } + + bool IsLower(uint32 c) + { + return utf8proc_category(c) == UTF8PROC_CATEGORY_LL; + } + + bool IsUpper(uint32 c) + { + return utf8proc_category(c) == UTF8PROC_CATEGORY_LU; + } + + // Private implementation + static bool fuzzy_internal::fuzzy_match_recursive(const char* pattern, const char* str, int& outScore, + const char* strBegin, uint8_t const* srcMatches, uint8_t* matches, int maxMatches, + int nextMatch, int& recursionCount, int recursionLimit) + { + // Count recursions + ++recursionCount; + if (recursionCount >= recursionLimit) + return false; + + // Detect end of strings + if (*pattern == '\0' || *str == '\0') + return false; + + // Recursion params + bool recursiveMatch = false; + uint8_t bestRecursiveMatches[256]; + int bestRecursiveScore = 0; + + // Loop through pattern and str looking for a match + bool first_match = true; + while (*pattern != '\0' && *str != '\0') { + + int patternOffset = 0; + uint32 patternChar = Beefy::u8_nextchar((char*)pattern, &patternOffset); + int strOffset = 0; + uint32 strChar = Beefy::u8_nextchar((char*)str, &strOffset); + + // TODO: tolower only works for A-Z + // Found match + if (utf8proc_tolower(patternChar) == utf8proc_tolower(strChar)) { + + // Supplied matches buffer was too short + if (nextMatch >= maxMatches) + return false; + + // "Copy-on-Write" srcMatches into matches + if (first_match && srcMatches) { + memcpy(matches, srcMatches, nextMatch); + first_match = false; + } + + // Recursive call that "skips" this match + uint8_t recursiveMatches[256]; + int recursiveScore; + if (fuzzy_match_recursive(pattern, str + strOffset, recursiveScore, strBegin, matches, recursiveMatches, sizeof(recursiveMatches), nextMatch, recursionCount, recursionLimit)) { + + // Pick best recursive score + if (!recursiveMatch || recursiveScore > bestRecursiveScore) { + memcpy(bestRecursiveMatches, recursiveMatches, 256); + bestRecursiveScore = recursiveScore; + } + recursiveMatch = true; + } + + // Advance + matches[nextMatch++] = (uint8_t)(str - strBegin); + // Clear the next char so that we know which match is the last one + matches[nextMatch + 1] = 0; + pattern += patternOffset; + } + str += strOffset; + } + + // Determine if full pattern was matched + bool matched = *pattern == '\0' ? true : false; + + // Calculate score + if (matched) { + const int sequential_bonus = 15; // bonus for adjacent matches + const int separator_bonus = 30; // bonus if match occurs after a separator + const int camel_bonus = 30; // bonus if match is uppercase and prev is lower + const int first_letter_bonus = 15; // bonus if the first letter is matched + + const int leading_letter_penalty = -5; // penalty applied for every letter in str before the first match + const int max_leading_letter_penalty = -15; // maximum penalty for leading letters + const int unmatched_letter_penalty = -1; // penalty for every letter that doesn't matter + + // Iterate str to end + while (*str != '\0') + ++str; + + // Initialize score + outScore = 100; + + // Apply leading letter penalty + int penalty = leading_letter_penalty * matches[0]; + if (penalty < max_leading_letter_penalty) + penalty = max_leading_letter_penalty; + outScore += penalty; + + // Apply unmatched penalty + int unmatched = (int)(str - strBegin) - nextMatch; + outScore += unmatched_letter_penalty * unmatched; + + // Apply ordering bonuses + for (int i = 0; i < nextMatch; ++i) { + uint8_t currIdx = matches[i]; + + int currOffset = currIdx; + uint32 curr = Beefy::u8_nextchar((char*)strBegin, &currOffset); + + if (i > 0) { + uint8_t prevIdx = matches[i - 1]; + + int offsetPrevidx = prevIdx; + Beefy::u8_inc((char*)strBegin, &offsetPrevidx); + + // Sequential + if (currIdx == offsetPrevidx) + outScore += sequential_bonus; + } + + // Check for bonuses based on neighbor character value + if (currIdx > 0) { + int neighborOffset = currIdx; + Beefy::u8_dec((char*)strBegin, &neighborOffset); + uint32 neighbor = Beefy::u8_nextchar((char*)strBegin, &neighborOffset); + + // Camel case + if (IsLower(neighbor) && IsUpper(curr)) + outScore += camel_bonus; + + // Separator + bool neighborSeparator = neighbor == '_' || neighbor == ' '; + if (neighborSeparator) + outScore += separator_bonus; + } + else { + // First letter + outScore += first_letter_bonus; + } + } + } + + // Return best result + if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) { + // Recursive score is better than "this" + memcpy(matches, bestRecursiveMatches, maxMatches); + outScore = bestRecursiveScore; + return true; + } + else if (matched) { + // "this" score is better than recursive + return true; + } + else { + // no match + return false; + } + } +} // namespace fts + +BF_EXPORT bool BF_CALLTYPE fts_fuzzy_match(char const* pattern, char const* str, int& outScore, uint8_t* matches, int maxMatches) +{ + return fts::fuzzy_match(pattern, str, outScore, matches, maxMatches); +} + +#endif // FTS_FUZZY_MATCH_IMPLEMENTATION + +#endif // FTS_FUZZY_MATCH_H