diff --git a/.gitmodules b/.gitmodules index 9a340f39..527df5ac 100644 --- a/.gitmodules +++ b/.gitmodules @@ -67,3 +67,6 @@ [submodule "Animations/vendor/optick"] path = Animations/vendor/optick url = https://github.com/bombomby/optick +[submodule "Animations/vendor/rapidFuzz"] + path = Animations/vendor/rapidFuzz + url = https://github.com/rapidfuzz/rapidfuzz-cpp.git diff --git a/Animations/CMakeLists.txt b/Animations/CMakeLists.txt index b76c0d38..54fa364c 100644 --- a/Animations/CMakeLists.txt +++ b/Animations/CMakeLists.txt @@ -94,6 +94,9 @@ target_link_libraries(MathAnimations PUBLIC # Optick $<$:OptickCore> + + # Rapid Fuzz + rapidfuzz::rapidfuzz ) if(MATH_ANIMATION_OS_LINUX) diff --git a/Animations/include/editor/EditorSettings.h b/Animations/include/editor/EditorSettings.h index 31a078a7..2748a86f 100644 --- a/Animations/include/editor/EditorSettings.h +++ b/Animations/include/editor/EditorSettings.h @@ -50,6 +50,7 @@ namespace MathAnim float svgTargetScale; Vec4 activeObjectHighlightColor; float activeObjectOutlineWidth; + bool smoothCursor; }; namespace EditorSettings diff --git a/Animations/include/editor/panels/CodeEditorPanel.h b/Animations/include/editor/panels/CodeEditorPanel.h index 422f8556..e855402a 100644 --- a/Animations/include/editor/panels/CodeEditorPanel.h +++ b/Animations/include/editor/panels/CodeEditorPanel.h @@ -1,5 +1,6 @@ #include "core.h" #include "parsers/SyntaxHighlighter.h" +#include "scripting/ScriptAnalyzer.h" #include @@ -35,6 +36,8 @@ namespace MathAnim int32 mouseByteDragStart; int32 firstByteInSelection; int32 lastByteInSelection; + Vec2 lastCursorPosition; + float cursorTimeSpentInterpolating; CppUtils::BasicUtf8StringIter cursor; uint32 cursorCurrentLine; int32 numOfCharsFromBeginningOfLine; @@ -45,6 +48,16 @@ namespace MathAnim uint8* visibleCharacterBuffer; size_t visibleCharacterBufferSize; + FunctionIntellisense functionInfo; + uint32 currentFunctionIntellisenseParam; + + std::vector intellisenseSuggestions; + std::vector visibleIntellisenseSuggestions; + uint32 intellisenseScrollOffset; + uint32 selectedIntellisenseSuggestion; + bool intellisensePanelOpen; + std::string stringTypedSinceLastDot; + CodeHighlights syntaxHighlightTree; CodeEditorPanelDebugData debugData; }; diff --git a/Animations/include/parsers/SyntaxTheme.h b/Animations/include/parsers/SyntaxTheme.h index 3bd2262c..6bf223ee 100644 --- a/Animations/include/parsers/SyntaxTheme.h +++ b/Animations/include/parsers/SyntaxTheme.h @@ -142,6 +142,10 @@ namespace MathAnim uint32 scrollbarSliderActiveBackground; uint32 scrollbarSliderHoverBackground; + uint32 editorSuggestWidgetBackground; + uint32 editorSuggestWidgetBorder; + uint32 editorSuggestWidgetSelectedBackground; + // TODO: Switch to this, once we verify it's working correctly SyntaxTrieNode root; std::unordered_map colorMap; diff --git a/Animations/include/scripting/LuauLayer.h b/Animations/include/scripting/LuauLayer.h index 17ee2edb..432f9271 100644 --- a/Animations/include/scripting/LuauLayer.h +++ b/Animations/include/scripting/LuauLayer.h @@ -5,6 +5,7 @@ namespace MathAnim { struct AnimationManagerData; + class ScriptAnalyzer; namespace LuauLayer { @@ -21,6 +22,9 @@ namespace MathAnim bool remove(const std::string& scriptName); + // TODO: Remove this, for testing only + ScriptAnalyzer& getScriptAnalyzer(); + void free(); } } diff --git a/Animations/include/scripting/ScriptAnalyzer.h b/Animations/include/scripting/ScriptAnalyzer.h index ac38b4fa..95453fd3 100644 --- a/Animations/include/scripting/ScriptAnalyzer.h +++ b/Animations/include/scripting/ScriptAnalyzer.h @@ -1,6 +1,14 @@ #ifndef MATH_ANIM_SCRIPT_ANALYZER #define MATH_ANIM_SCRIPT_ANALYZER #include "core.h" +#include "parsers/SyntaxHighlighter.h" + +#pragma warning( push ) +#pragma warning( disable : 4100 ) +#pragma warning( disable : 4324 ) +#pragma warning( disable : 4324 ) +#include +#pragma warning( pop ) namespace Luau { @@ -11,6 +19,30 @@ namespace Luau namespace MathAnim { + struct AutocompleteSuggestion + { + std::string text; + Luau::AutocompleteEntry data; + // Rank from 0.0-100.0 where 100.0 is a perfect match + float rank; + bool containsQuery; + }; + + struct FunctionParameter + { + std::optional name; + std::string stringifiedType; + }; + + struct FunctionIntellisense + { + std::string fnName; + std::vector parameters; + std::vector returnTypes; + + CodeHighlights highlightInfo; + }; + class ScriptAnalyzer { public: @@ -19,6 +51,10 @@ namespace MathAnim bool analyze(const std::string& filename); bool analyze(const std::string& sourceCode, const std::string& scriptName); + FunctionIntellisense getFunctionParameterIntellisense(std::string const& sourceCode, std::string const& scriptName, uint32 line, uint32 column); + std::vector getSuggestions(std::string const& sourceCode, std::string const& scriptName, uint32 line, uint32 column); + void sortSuggestionsByQuery(std::string const& query, std::vector& suggestions, std::vector& visibleSuggestions); + void free(); private: diff --git a/Animations/src/core/Input.cpp b/Animations/src/core/Input.cpp index 03387966..91f671f7 100644 --- a/Animations/src/core/Input.cpp +++ b/Animations/src/core/Input.cpp @@ -15,8 +15,8 @@ namespace MathAnim // ----------- Internal Variables ----------- - static constexpr float slowKeyRepeatTimeInterval = 0.013f; - static constexpr float firstRepeatFlag = -0.3f; + static constexpr float slowKeyRepeatTimeInterval = 0.03f; + static constexpr float firstRepeatFlag = -0.5f; static uint32 lastCharacterTyped = 0; diff --git a/Animations/src/editor/EditorSettings.cpp b/Animations/src/editor/EditorSettings.cpp index 0c7f2296..a5dde7bf 100644 --- a/Animations/src/editor/EditorSettings.cpp +++ b/Animations/src/editor/EditorSettings.cpp @@ -21,6 +21,7 @@ namespace MathAnim data->viewMode = ViewMode::Normal; data->activeObjectOutlineWidth = 9.0f; data->activeObjectHighlightColor = "#FF9E28"_hex; + data->smoothCursor = false; } void imgui(AnimationManagerData* am) @@ -75,6 +76,8 @@ namespace MathAnim ImGui::EndCombo(); } + ImGui::Checkbox(": Smooth Cursor", &data->smoothCursor); + ImGui::End(); } } diff --git a/Animations/src/editor/panels/CodeEditorPanel.cpp b/Animations/src/editor/panels/CodeEditorPanel.cpp index b2b6e6c8..87e5e2f2 100644 --- a/Animations/src/editor/panels/CodeEditorPanel.cpp +++ b/Animations/src/editor/panels/CodeEditorPanel.cpp @@ -1,13 +1,17 @@ +#include "editor/EditorSettings.h" #include "editor/panels/CodeEditorPanel.h" #include "editor/panels/CodeEditorPanelManager.h" #include "editor/imgui/ImGuiLayer.h" +#include "editor/TextEditUndo.h" #include "platform/Platform.h" #include "core/Application.h" #include "core/Input.h" #include "core/Window.h" +#include "math/CMath.h" #include "renderer/Fonts.h" -#include "editor/TextEditUndo.h" #include "parsers/SyntaxTheme.h" +#include "scripting/LuauLayer.h" +#include "scripting/ScriptAnalyzer.h" #include @@ -35,6 +39,15 @@ namespace MathAnim static uint32 numMsToShowLineUpdates = 500; static Vec4 flashColor = "#36d174"_hex; + static float maxTimeToInterpolateCursor = 0.1f; + + static float minIntellisensePanelWidth = 400.0f; + static float functionInfoPanelWidth = 800.0f; + static float intellisensePanelBorderWidth = 2.0f; + static float intellisenseSuggestionSpacing = 1.0f; + static float intellisenseFrameRounding = 4.0f; + static uint32 maxIntellisenseSuggestions = 12; + // ------------- Internal Functions ------------- static void resetSelection(CodeEditorPanelData& panel); static void handleTypingUndo(CodeEditorPanelData& panel); @@ -42,7 +55,9 @@ namespace MathAnim static void handleDeleteUndo(CodeEditorPanelData& panel, bool shouldSetTextSelectedOnUndo); static void moveTextCursor(CodeEditorPanelData& panel, KeyMoveDirection direction); static void moveTextCursorAndResetSelection(CodeEditorPanelData& panel, KeyMoveDirection direction); - static void renderTextCursor(CodeEditorPanelData& panel, ImVec2 const& textCursorDrawPosition, SizedFont const* const font); + static void renderTextCursor(CodeEditorPanelData& panel, ImVec2 textCursorDrawPosition, SizedFont const* const font); + static void renderIntellisensePanel(CodeEditorPanelData& panel, SizedFont const* const font); + static void renderFunctionInfoPanel(CodeEditorPanelData& panel, SizedFont const* const font); static ImVec2 renderNextLinePrefix(CodeEditorPanelData& panel, uint32 lineNumber, SizedFont const* const font); static bool mouseInTextEditArea(CodeEditorPanelData const& panel); static ImVec2 addStringToDrawList(ImDrawList* drawList, SizedFont const* const font, std::string const& str, ImVec2 const& drawPos, ImColor const& color); @@ -53,6 +68,7 @@ namespace MathAnim static bool removeText(CodeEditorPanelData& panel, int32 textToRemoveOffset, int32 textToRemoveNumBytes); static int32 getNewCursorPositionFromMove(CodeEditorPanelData const& panel, KeyMoveDirection direction); static void scrollCursorIntoViewIfNeeded(CodeEditorPanelData& panel); + static void openIntellisensePanelAtCursor(CodeEditorPanelData& panel); // TODO: Move this to static uint8 codepointToUtf8Str(uint8* const buffer, uint32 codepoint); @@ -144,6 +160,10 @@ namespace MathAnim res->hzCharacterOffset = 0; res->maxLineLength = 0; + res->intellisensePanelOpen = false; + res->intellisenseScrollOffset = 0; + res->selectedIntellisenseSuggestion = 0; + preprocessText((uint8*)memory.data, fileSize, &res->visibleCharacterBuffer, &res->visibleCharacterBufferSize, &res->totalNumberLines, &res->maxLineLength); // +1 for the extra line for EOF res->totalNumberLines++; @@ -302,11 +322,11 @@ namespace MathAnim { moveTextCursorAndResetSelection(panel, KeyMoveDirection::Left); } - else if (Input::keyRepeatedOrDown(GLFW_KEY_UP)) + else if (Input::keyRepeatedOrDown(GLFW_KEY_UP) && !panel.intellisensePanelOpen) { moveTextCursorAndResetSelection(panel, KeyMoveDirection::Up); } - else if (Input::keyRepeatedOrDown(GLFW_KEY_DOWN)) + else if (Input::keyRepeatedOrDown(GLFW_KEY_DOWN) && !panel.intellisensePanelOpen) { moveTextCursorAndResetSelection(panel, KeyMoveDirection::Down); } @@ -328,6 +348,80 @@ namespace MathAnim } } + // Handle intellisense key combos + { + if (Input::keyPressed(GLFW_KEY_SPACE, KeyMods::Ctrl)) + { + openIntellisensePanelAtCursor(panel); + } + + if (Input::keyPressed(GLFW_KEY_ESCAPE)) + { + panel.intellisensePanelOpen = false; + } + + if (panel.intellisensePanelOpen) + { + if (Input::keyRepeatedOrDown(GLFW_KEY_DOWN)) + { + panel.selectedIntellisenseSuggestion++; + if (panel.selectedIntellisenseSuggestion >= (uint32)panel.visibleIntellisenseSuggestions.size()) + { + panel.selectedIntellisenseSuggestion = 0; + panel.intellisenseScrollOffset = 0; + } + + if ((panel.selectedIntellisenseSuggestion - panel.intellisenseScrollOffset) >= maxIntellisenseSuggestions) + { + panel.intellisenseScrollOffset++; + } + } + else if (Input::keyRepeatedOrDown(GLFW_KEY_UP)) + { + if (panel.selectedIntellisenseSuggestion == 0) + { + if (panel.visibleIntellisenseSuggestions.size() > 0) + { + panel.selectedIntellisenseSuggestion = (uint32)panel.visibleIntellisenseSuggestions.size() - 1; + } + + if (panel.visibleIntellisenseSuggestions.size() > maxIntellisenseSuggestions) + { + panel.intellisenseScrollOffset = (uint32)panel.visibleIntellisenseSuggestions.size() - maxIntellisenseSuggestions; + } + } + else + { + panel.selectedIntellisenseSuggestion--; + + if (panel.selectedIntellisenseSuggestion < panel.intellisenseScrollOffset) + { + panel.intellisenseScrollOffset = panel.selectedIntellisenseSuggestion; + } + } + } + + // Blit the current intellisense suggestion into the buffer at the cursor + if (Input::keyPressed(GLFW_KEY_TAB) || Input::keyPressed(GLFW_KEY_ENTER)) + { + // First remove the string that's been typed so far + removeTextWithBackspace( + panel, + (int32)(panel.cursor.bytePos - panel.stringTypedSinceLastDot.size()), + (int32)panel.stringTypedSinceLastDot.size() + ); + + // Then blit the whole suggestion over that empty space + int index = panel.visibleIntellisenseSuggestions[panel.selectedIntellisenseSuggestion]; + auto const& suggestion = panel.intellisenseSuggestions[index]; + addUtf8StringToBuffer(panel, (uint8*)suggestion.text.c_str(), suggestion.text.size(), panel.cursor.bytePos); + panel.intellisensePanelOpen = false; + panel.stringTypedSinceLastDot = ""; + fileHasBeenEdited = true; + } + } + } + // Handle key presses to move cursor + select modifier (Shift) { int32 oldBytePos = (int32)panel.cursor.bytePos; @@ -386,6 +480,11 @@ namespace MathAnim // TODO: Not all backspaces are handled for some reason if (Input::keyRepeatedOrDown(GLFW_KEY_BACKSPACE)) { + panel.cursorTimeSpentInterpolating = 0.0f; + panel.cursorIsBlinkedOn = true; + panel.timeSinceCursorLastBlinked = 0.0f; + panel.intellisensePanelOpen = false; + handleTypingUndo(panel); if (removeSelectedTextWithBackspace(panel)) @@ -397,6 +496,10 @@ namespace MathAnim // Handle delete if (Input::keyRepeatedOrDown(GLFW_KEY_DELETE)) { + panel.cursorIsBlinkedOn = true; + panel.timeSinceCursorLastBlinked = 0.0f; + panel.intellisensePanelOpen = false; + handleTypingUndo(panel); if (removeSelectedTextWithDelete(panel)) @@ -408,6 +511,114 @@ namespace MathAnim // Handle text-insertion if (uint32 codepoint = Input::getLastCharacterTyped(); codepoint != 0) { + if (!(codepoint == ' ' && Input::keyDown(GLFW_KEY_SPACE, KeyMods::Ctrl))) + { + panel.cursorTimeSpentInterpolating = 0.0f; + panel.cursorIsBlinkedOn = true; + panel.timeSinceCursorLastBlinked = 0.0f; + + // Update autocompletesuggestions if possible + if (panel.intellisensePanelOpen) + { + if (Parser::isWhitespace((char)codepoint)) + { + // If the user entered whitespace after entering a string since the last dot, close the current suggestions + if (panel.stringTypedSinceLastDot.size() > 0) + { + panel.intellisensePanelOpen = false; + } + + // Otherwise, if the user entered white space and still haven't typed a string, keep the intellisense panel + // open until they type a non-whitespace string + } + else + { + // Only add alpha-numeric characters to the current suggestion + if (Parser::isAlpha((char)codepoint) || Parser::isDigit((char)codepoint) || (char)codepoint == '_') + { + uint8 charStr[5] = { '\0', '\0' , '\0' , '\0' , '\0' }; + codepointToUtf8Str(charStr, codepoint); + g_logger_assert(charStr[4] == '\0', "Codepoint to UTF8 corrupted the string somehow. Missing null byte."); + panel.stringTypedSinceLastDot += (const char*)charStr; + + // Update intellisense suggestions + auto& analyzer = LuauLayer::getScriptAnalyzer(); + analyzer.sortSuggestionsByQuery(panel.stringTypedSinceLastDot, panel.intellisenseSuggestions, panel.visibleIntellisenseSuggestions); + } + else + { + panel.intellisensePanelOpen = false; + } + } + } + + if (panel.firstByteInSelection != panel.lastByteInSelection) + { + removeSelectedTextWithBackspace(panel); + } + + if (panel.undoTypingStart == -1) + { + panel.undoTypingStart = (int32)panel.cursor.bytePos; + } + + addCodepointToBuffer(panel, codepoint, (int32)panel.cursor.bytePos); + fileHasBeenEdited = true; + } + + // Check to see if we should open intellisense + if (codepoint == '.' || codepoint == ':') + { + // If the previous character was not also the same character we'll open intellisense + auto prevChar = panel.cursor; + // We have to go back two characters because the cursor is already one character ahead of the last character typed + --prevChar; + --prevChar; + auto maybePrevChar = *prevChar; + + if ((maybePrevChar.hasValue() && maybePrevChar.value() != codepoint) || prevChar.bytePos == panel.cursor.bytePos) + { + openIntellisensePanelAtCursor(panel); + } + } + // Check if we should pull up function documentation + else if (codepoint == '(' && panel.cursor.bytePos > 0) + { + auto& analyzer = LuauLayer::getScriptAnalyzer(); + + uint32 lineNumber = getLineNumberFromPosition(panel, (uint32)panel.cursor.bytePos - 1); + uint32 lineByteStart = getLineNumberByteStartFrom(panel, lineNumber); + uint32 column = (uint32)panel.cursor.bytePos - 1 - lineByteStart; + + panel.functionInfo = analyzer.getFunctionParameterIntellisense( + std::string((const char*)panel.visibleCharacterBuffer, panel.visibleCharacterBufferSize), + "code-being-edited", + lineNumber, + column + ); + panel.currentFunctionIntellisenseParam = 0; + } + // Check if we move to the next function parameter + else if (codepoint == ',' && panel.functionInfo.parameters.size() > 0) + { + panel.currentFunctionIntellisenseParam++; + } + // Check if we should close the function intellisense info + else if (codepoint == ')') + { + panel.functionInfo = {}; + panel.currentFunctionIntellisenseParam = 0; + } + } + + // Handle tab key + if (Input::keyRepeatedOrDown(GLFW_KEY_TAB) && !panel.intellisensePanelOpen) + { + panel.cursorTimeSpentInterpolating = 0.0f; + panel.cursorIsBlinkedOn = true; + panel.timeSinceCursorLastBlinked = 0.0f; + panel.intellisensePanelOpen = false; + if (panel.firstByteInSelection != panel.lastByteInSelection) { removeSelectedTextWithBackspace(panel); @@ -418,13 +629,18 @@ namespace MathAnim panel.undoTypingStart = (int32)panel.cursor.bytePos; } - addCodepointToBuffer(panel, codepoint, (int32)panel.cursor.bytePos); + addCodepointToBuffer(panel, (uint32)'\t', panel.cursor.bytePos); fileHasBeenEdited = true; } // Handle newline-insertion - if (Input::keyRepeatedOrDown(GLFW_KEY_ENTER)) + if (Input::keyRepeatedOrDown(GLFW_KEY_ENTER) && !panel.intellisensePanelOpen) { + panel.cursorTimeSpentInterpolating = 0.0f; + panel.cursorIsBlinkedOn = true; + panel.timeSinceCursorLastBlinked = 0.0f; + panel.intellisensePanelOpen = false; + if (panel.firstByteInSelection != panel.lastByteInSelection) { removeSelectedTextWithBackspace(panel); @@ -600,8 +816,7 @@ namespace MathAnim // Render the text cursor if (cursor.bytePos == panel.cursor.bytePos) { - ImVec2 textCursorDrawPosition = letterBoundsStart; - renderTextCursor(panel, textCursorDrawPosition, codeFont); + renderTextCursor(panel, letterBoundsStart, codeFont); } else if (cursor.bytePos == panel.visibleCharacterBufferSize - 1 && panel.cursor.bytePos == panel.visibleCharacterBufferSize) { @@ -753,6 +968,7 @@ namespace MathAnim panel.mouseByteDragStart = (int32)panel.cursor.bytePos; panel.firstByteInSelection = (int32)panel.cursor.bytePos; panel.lastByteInSelection = (int32)panel.cursor.bytePos; + panel.cursorTimeSpentInterpolating = 0.0f; } } @@ -762,6 +978,9 @@ namespace MathAnim renderTextCursor(panel, currentLetterDrawPos, codeFont); } + renderIntellisensePanel(panel, codeFont); + renderFunctionInfoPanel(panel, codeFont); + static bool inspectorOn = false; if (windowIsFocused && Input::keyPressed(GLFW_KEY_I, KeyMods::Ctrl | KeyMods::Shift)) { @@ -793,7 +1012,11 @@ namespace MathAnim ImGui::TableNextColumn(); ImGui::Text("Cursor Byte"); ImGui::TableNextColumn(); ImGui::Text("%d", panel.cursor.bytePos); ImGui::TableNextRow(); - + + ImGui::TableNextColumn(); ImGui::Text("Cursor current line"); + ImGui::TableNextColumn(); ImGui::Text("%d", panel.cursorCurrentLine); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); ImGui::Text("Line start dist (Chars)"); ImGui::TableNextColumn(); ImGui::Text("%d", panel.numOfCharsFromBeginningOfLine); ImGui::TableNextRow(); @@ -1261,6 +1484,8 @@ namespace MathAnim panel.cursorIsBlinkedOn = true; panel.timeSinceCursorLastBlinked = 0.0f; + panel.cursorTimeSpentInterpolating = 0.0f; + panel.intellisensePanelOpen = false; panel.cursor.bytePos = getNewCursorPositionFromMove(panel, direction); @@ -1285,7 +1510,7 @@ namespace MathAnim resetSelection(panel); } - static void renderTextCursor(CodeEditorPanelData& panel, ImVec2 const& drawPosition, SizedFont const* const font) + static void renderTextCursor(CodeEditorPanelData& panel, ImVec2 drawPosition, SizedFont const* const font) { if (!ImGui::IsWindowFocused() && !panel.mouseIsDragSelecting) { @@ -1294,6 +1519,25 @@ namespace MathAnim SyntaxTheme const& syntaxTheme = CodeEditorPanelManager::getTheme(); + // Smooth the cursor movement if needed + EditorSettingsData const& editorSettings = EditorSettings::getSettings(); + if (editorSettings.smoothCursor && panel.cursorTimeSpentInterpolating < maxTimeToInterpolateCursor) + { + panel.timeSinceCursorLastBlinked = 0.0f; + panel.cursorIsBlinkedOn = true; + panel.cursorTimeSpentInterpolating += Application::getDeltaTime(); + + float t = panel.cursorTimeSpentInterpolating / maxTimeToInterpolateCursor; + t = CMath::ease(t, EaseType::Linear, EaseDirection::In); + + panel.lastCursorPosition = CMath::interpolate(t, panel.lastCursorPosition, drawPosition); + drawPosition = panel.lastCursorPosition; + } + else + { + panel.lastCursorPosition = drawPosition; + } + if (panel.timeSinceCursorLastBlinked >= cursorBlinkTime) { panel.cursorIsBlinkedOn = !panel.cursorIsBlinkedOn; @@ -1315,6 +1559,210 @@ namespace MathAnim } } + static void renderIntellisensePanel(CodeEditorPanelData& panel, SizedFont const* const font) + { + if (!panel.intellisensePanelOpen || !ImGui::IsWindowFocused() || panel.visibleIntellisenseSuggestions.size() == 0) + { + return; + } + + SyntaxTheme const& syntaxTheme = CodeEditorPanelManager::getTheme(); + ImGuiStyle& style = ImGui::GetStyle(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + Vec4 const& borderColor = syntaxTheme.getColor(syntaxTheme.editorSuggestWidgetBorder); + Vec4 const& normalBgColor = syntaxTheme.getColor(syntaxTheme.editorSuggestWidgetBackground); + Vec4 const& normalFgColor = syntaxTheme.getColor(syntaxTheme.defaultForeground); + Vec4 const& selectedBgColor = syntaxTheme.getColor(syntaxTheme.editorSuggestWidgetSelectedBackground); + Vec4 const& selectedFgColor = "#FFF"_hex; + + float maxSuggestionStrLength = minIntellisensePanelWidth; + for (auto index : panel.visibleIntellisenseSuggestions) + { + g_logger_assert(index < panel.intellisenseSuggestions.size(), "Invalid index '{}' in visible intellisense suggestions.", index); + auto const& suggestion = panel.intellisenseSuggestions[index]; + float strLength = ImGui::CalcTextSize(suggestion.text.c_str()).x + intellisenseFrameRounding * 2.0f; + maxSuggestionStrLength = glm::max(maxSuggestionStrLength, strLength); + } + + float lineHeight = getLineHeight(font); + float selectionHeight = lineHeight + intellisenseFrameRounding * 2.0f; + + size_t numItemsToShow = panel.visibleIntellisenseSuggestions.size() > maxIntellisenseSuggestions + ? maxIntellisenseSuggestions + : panel.visibleIntellisenseSuggestions.size(); + ImVec2 panelBgDrawStart = panel.lastCursorPosition + ImVec2(0.0f, lineHeight); + ImVec2 panelBgDrawEnd = panelBgDrawStart + + ImVec2(maxSuggestionStrLength, numItemsToShow * selectionHeight) + + (style.FramePadding * 2.0f); + + ImVec2 borderSize = ImVec2(intellisensePanelBorderWidth, intellisensePanelBorderWidth); + drawList->AddRectFilled(panelBgDrawStart - borderSize, panelBgDrawEnd + borderSize, ImColor(borderColor), intellisenseFrameRounding); + drawList->AddRectFilled(panelBgDrawStart, panelBgDrawEnd, ImColor(normalBgColor), intellisenseFrameRounding); + + ImVec2 cursor = panelBgDrawStart + style.FramePadding; + uint32 endIndex = panel.visibleIntellisenseSuggestions.size() >= maxIntellisenseSuggestions + ? maxIntellisenseSuggestions + : (uint32)panel.visibleIntellisenseSuggestions.size(); + for (uint32 index = panel.intellisenseScrollOffset; (index - panel.intellisenseScrollOffset) < endIndex; index++) + { + auto suggestionIndex = panel.visibleIntellisenseSuggestions[index]; + auto const& suggestion = panel.intellisenseSuggestions[suggestionIndex]; + + ImColor fgColor = normalFgColor; + + if (index == panel.selectedIntellisenseSuggestion) + { + fgColor = ImColor(selectedFgColor); + + drawList->AddRectFilled( + ImVec2(panelBgDrawStart.x, cursor.y), + ImVec2(panelBgDrawEnd.x, cursor.y + selectionHeight), + ImColor(selectedBgColor) + ); + } + + drawList->AddText(cursor + ImVec2(intellisenseFrameRounding, intellisenseFrameRounding), fgColor, suggestion.text.c_str()); + cursor = cursor + ImVec2(0.0f, selectionHeight); + } + + // Render scrollbar + if (panel.visibleIntellisenseSuggestions.size() > maxIntellisenseSuggestions) + { + float bgHeight = panelBgDrawEnd.y - panelBgDrawStart.y; + float scrollbarHeight = ((float)maxIntellisenseSuggestions / (float)panel.visibleIntellisenseSuggestions.size()) * bgHeight; + float scrollbarStartY = ((float)panel.intellisenseScrollOffset / (float)panel.visibleIntellisenseSuggestions.size()) * bgHeight; + + ImVec2 scrollbarStart = ImVec2(panelBgDrawEnd.x, scrollbarStartY + panelBgDrawStart.y); + ImVec2 scrollbarEnd = scrollbarStart + ImVec2(-scrollbarWidth, scrollbarHeight); + ImColor scrollbarColor = syntaxTheme.getColor(syntaxTheme.scrollbarSliderBackground); + + drawList->AddRectFilled(scrollbarStart, scrollbarEnd, scrollbarColor); + } + } + + static void insertTextIntoFunctionInfoPanel(std::string const& str, ImGuiStyle const& style, float lineHeight, ImVec2* drawCursor, float* panelWidth, float* panelHeight, std::vector>* textPositions) + { + ImVec2 strSize = ImGui::CalcTextSize(str.c_str()); + + if (strSize.x > *panelWidth - (style.FramePadding.x * 2.0f)) + { + *panelWidth = strSize.x + (style.FramePadding.x * 2.0f); + } + + if (drawCursor->x + strSize.x > *panelWidth - (style.FramePadding.x * 2.0f)) + { + drawCursor->y += lineHeight; + *panelHeight += lineHeight; + drawCursor->x = style.FramePadding.x; + } + + textPositions->push_back({ str, *drawCursor }); + drawCursor->x += strSize.x; + } + + static void renderFunctionInfoPanel(CodeEditorPanelData& panel, SizedFont const* const font) + { + if (!ImGui::IsWindowFocused() || panel.functionInfo.fnName.length() == 0) + { + return; + } + + ImGui::PushFont(ImGuiLayer::getMonoFont()); + + SyntaxTheme const& syntaxTheme = CodeEditorPanelManager::getTheme(); + ImGuiStyle& style = ImGui::GetStyle(); + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + Vec4 const& borderColor = syntaxTheme.getColor(syntaxTheme.editorSuggestWidgetBorder); + Vec4 const& normalBgColor = syntaxTheme.getColor(syntaxTheme.editorSuggestWidgetBackground); + //Vec4 const& selectedFgColor = "#FFF"_hex; + + float lineHeight = getLineHeight(font); + + // First calculate where we'll put all the pieces of text (this does any wrapping of the text + // that's necessary within the panel and stuff) + std::vector> textPositions = {}; + ImVec2 fnNameTextSize = ImGui::CalcTextSize(panel.functionInfo.fnName.c_str()); + + float panelWidth = functionInfoPanelWidth; + float panelHeight = fnNameTextSize.y + style.FramePadding.y * 2.0f; + + ImVec2 drawCursor = style.FramePadding; + + insertTextIntoFunctionInfoPanel("type ", style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + insertTextIntoFunctionInfoPanel(panel.functionInfo.fnName, style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + insertTextIntoFunctionInfoPanel(" = (", style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + + // Insert all parameter info draw positions + for (size_t i = 0; i < panel.functionInfo.parameters.size(); i++) + { + auto const& param = panel.functionInfo.parameters[i]; + if (param.name.has_value()) + { + insertTextIntoFunctionInfoPanel(param.name.value() + ": ", style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + } + + if (i < panel.functionInfo.parameters.size() - 1) + { + insertTextIntoFunctionInfoPanel(param.stringifiedType + ", ", style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + } + else + { + insertTextIntoFunctionInfoPanel(param.stringifiedType, style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + } + } + + insertTextIntoFunctionInfoPanel("): (", style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + + // Insert all return type information into panel + for (size_t i = 0; i < panel.functionInfo.returnTypes.size(); i++) + { + auto const& returnType = panel.functionInfo.returnTypes[i]; + insertTextIntoFunctionInfoPanel(returnType, style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + + if (i < panel.functionInfo.returnTypes.size() - 1) + { + insertTextIntoFunctionInfoPanel(", ", style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + } + } + + insertTextIntoFunctionInfoPanel(")", style, lineHeight, &drawCursor, &panelWidth, &panelHeight, &textPositions); + + ImVec2 panelBgDrawStart = panel.lastCursorPosition + ImVec2(0.0f, lineHeight); + ImVec2 panelBgDrawEnd = panelBgDrawStart + + ImVec2(panelWidth, panelHeight) + + (style.FramePadding * 2.0f); + + ImVec2 borderSize = ImVec2(intellisensePanelBorderWidth, intellisensePanelBorderWidth); + drawList->AddRectFilled(panelBgDrawStart - borderSize, panelBgDrawEnd + borderSize, ImColor(borderColor), intellisenseFrameRounding); + drawList->AddRectFilled(panelBgDrawStart, panelBgDrawEnd, ImColor(normalBgColor), intellisenseFrameRounding); + + ImVec2 textAreaStart = panelBgDrawStart + style.FramePadding; + uint32 currentByte = 0; + auto currentSourceToken = panel.functionInfo.highlightInfo.begin(); + for (auto const& text : textPositions) + { + ImVec2 drawPos = textAreaStart + text.second; + const char* textCStr = text.first.c_str(); + + // Go character by character so that we color the characters correctly + for (size_t i = 0; i < text.first.length(); i++) + { + currentSourceToken = currentSourceToken.next(currentByte); + currentByte++; + + ImColor color = ImColor(currentSourceToken.getForegroundColor(syntaxTheme)); + drawList->AddText(drawPos, color, textCStr + i, textCStr + i + 1); + drawPos.x += ImGui::CalcTextSize(textCStr + i, textCStr + i + 1).x; + } + } + + // Handle scrollbar stuff + + ImGui::PopFont(); + } + static ImVec2 renderNextLinePrefix(CodeEditorPanelData& panel, uint32 lineNumber, SizedFont const* const font) { ImGuiStyle& style = ImGui::GetStyle(); @@ -1573,7 +2021,7 @@ namespace MathAnim static inline bool isBoundaryCharacter(uint32 c) { if (c == ':' || c == ';' || c == '"' || c == '\'' || c == '.' || c == '(' || c == ')' || c == '{' || c == '}' - || c == '-' || c == '+' || c == '*' || c == '/' || c == ',' || c == '=' || c == '!' || c == '`') + || c == '-' || c == '+' || c == '*' || c == '/' || c == ',' || c == '=' || c == '!' || c == '`' || c == '<' || c == '>') { return true; } @@ -1703,8 +2151,8 @@ namespace MathAnim { int32 endOfCurrentLine = getEndOfLineFrom(panel, (int32)panel.cursor.bytePos); - // If we're at the end of the file, move all the way to the end - if (endOfCurrentLine == panel.visibleCharacterBufferSize - 1) + // If we're at the end of the file, move all the way to the end if it's not a newline + if (endOfCurrentLine == panel.visibleCharacterBufferSize - 1 && panel.visibleCharacterBuffer[panel.visibleCharacterBufferSize - 1] != '\n') { endOfCurrentLine++; } @@ -1869,6 +2317,28 @@ namespace MathAnim } } + static void openIntellisensePanelAtCursor(CodeEditorPanelData& panel) + { + auto& analyzer = LuauLayer::getScriptAnalyzer(); + panel.intellisenseSuggestions = analyzer.getSuggestions( + std::string((const char*)panel.visibleCharacterBuffer, panel.visibleCharacterBufferSize), + "code-being-edited", + panel.cursorCurrentLine, + panel.numOfCharsFromBeginningOfLine + ); + + panel.visibleIntellisenseSuggestions = {}; + for (int i = 0; i < (int)panel.intellisenseSuggestions.size(); i++) + { + panel.visibleIntellisenseSuggestions.push_back(i); + } + + panel.intellisensePanelOpen = true; + panel.intellisenseScrollOffset = 0; + panel.selectedIntellisenseSuggestion = 0; + panel.stringTypedSinceLastDot = ""; + } + static uint8 codepointToUtf8Str(uint8* const outBuffer, uint32 code) { if (code <= 0x7F) diff --git a/Animations/src/editor/panels/DebugPanel.cpp b/Animations/src/editor/panels/DebugPanel.cpp index 7c518bef..0511a7f5 100644 --- a/Animations/src/editor/panels/DebugPanel.cpp +++ b/Animations/src/editor/panels/DebugPanel.cpp @@ -103,7 +103,7 @@ namespace MathAnim { fpsIndicatorColor = Colors::AccentYellow[3]; } - ImGui::TextColored(fpsIndicatorColor, "%2.3fms (%2.3f FPS)", avgFrameTime, 1.0f / avgFrameTime); + ImGui::TextColored(fpsIndicatorColor, "%2.3fms (%2.3f FPS)", avgFrameTime * 1000.0f, 1.0f / avgFrameTime); // Update the array of previous frame times previousFrameTimes[previousFrameTimesIndex] = Application::getDeltaTime(); diff --git a/Animations/src/parsers/SyntaxHighlighter.cpp b/Animations/src/parsers/SyntaxHighlighter.cpp index 5d78491f..06e61c06 100644 --- a/Animations/src/parsers/SyntaxHighlighter.cpp +++ b/Animations/src/parsers/SyntaxHighlighter.cpp @@ -362,6 +362,7 @@ namespace MathAnim } // Update beginnings/endings of all lines and mark any lines that need to be updated + if (lineIndexStartedRemovingFrom < highlights.tree.sourceInfo.size()) { auto currentLineInfo = highlights.tree.sourceInfo.begin() + lineIndexStartedRemovingFrom; g_logger_assert(currentLineInfo != highlights.tree.sourceInfo.end(), "Cannot remove syntax highlighting from beyond the end of the file."); diff --git a/Animations/src/parsers/SyntaxTheme.cpp b/Animations/src/parsers/SyntaxTheme.cpp index 1d72f684..cc2de287 100644 --- a/Animations/src/parsers/SyntaxTheme.cpp +++ b/Animations/src/parsers/SyntaxTheme.cpp @@ -641,6 +641,10 @@ namespace MathAnim theme->scrollbarSliderActiveBackground = tryParseColor(theme, colorsJson, "scrollbarSlider.activeBackground"); theme->scrollbarSliderHoverBackground = tryParseColor(theme, colorsJson, "scrollbarSlider.hoverBackground"); + theme->editorSuggestWidgetBackground = tryParseColor(theme, colorsJson, "editorSuggestWidget.background"); + theme->editorSuggestWidgetBorder = tryParseColor(theme, colorsJson, "editorSuggestWidget.border"); + theme->editorSuggestWidgetSelectedBackground = tryParseColor(theme, colorsJson, "editorSuggestWidget.selectedBackground"); + // Initialize the root of our tree (these are global settings) theme->root.style.setForegroundColor(theme->defaultForeground); theme->root.style.setBackgroundColor(theme->defaultBackground); diff --git a/Animations/src/scripting/LuauLayer.cpp b/Animations/src/scripting/LuauLayer.cpp index c6d62de4..72225d21 100644 --- a/Animations/src/scripting/LuauLayer.cpp +++ b/Animations/src/scripting/LuauLayer.cpp @@ -290,6 +290,11 @@ namespace MathAnim return true; } + ScriptAnalyzer& getScriptAnalyzer() + { + return *analyzer; + } + void free() { if (analyzer) diff --git a/Animations/src/scripting/MathAnimGlobals.cpp b/Animations/src/scripting/MathAnimGlobals.cpp index f307ff2d..790ff1cc 100644 --- a/Animations/src/scripting/MathAnimGlobals.cpp +++ b/Animations/src/scripting/MathAnimGlobals.cpp @@ -5,7 +5,7 @@ namespace MathAnim namespace MathAnimGlobals { static const std::string_view builtinDefinitionLuaSrc = R"BUILTIN_SRC( -type Logger = { +export type Logger = { write: (T...) -> (), info: (T...) -> (), warn: (T...) -> (), @@ -84,7 +84,7 @@ export type AnimObject = { svgObject: SvgObject, } -type MathAnimModule = { +export type MathAnimModule = { createAnimObject: (parent: AnimObject) -> AnimObject } diff --git a/Animations/src/scripting/ScriptAnalyzer.cpp b/Animations/src/scripting/ScriptAnalyzer.cpp index 49095d92..2aec01d7 100644 --- a/Animations/src/scripting/ScriptAnalyzer.cpp +++ b/Animations/src/scripting/ScriptAnalyzer.cpp @@ -1,8 +1,11 @@ #include "scripting/ScriptAnalyzer.h" #include "scripting/MathAnimGlobals.h" #include "platform/Platform.h" +#include "editor/panels/CodeEditorPanelManager.h" #include "editor/panels/ConsoleLog.h" +#include + #pragma warning( push ) #pragma warning( disable : 4100 ) #pragma warning( disable : 4324 ) @@ -10,12 +13,17 @@ #include #include #include -#include #include +#include +#include +#include +#include #pragma warning( pop ) +using namespace Luau; + // ------------------------------- Internal Types ------------------------------- -struct ScriptFileResolver : public Luau::FileResolver +struct ScriptFileResolver : public FileResolver { std::string anonymousSource; std::string anonymousName; @@ -25,20 +33,20 @@ struct ScriptFileResolver : public Luau::FileResolver void setAnonymousFile(const std::string& source, const std::string& name); - virtual std::optional readSource(const Luau::ModuleName& name) override; + virtual std::optional readSource(const ModuleName& name) override; - std::optional resolveModule(const Luau::ModuleInfo* context, Luau::AstExpr* node) override; + std::optional resolveModule(const ModuleInfo* context, AstExpr* node) override; - std::string getHumanReadableModuleName(const Luau::ModuleName& name) const override; + std::string getHumanReadableModuleName(const ModuleName& name) const override; }; -struct ScriptConfigResolver : public Luau::ConfigResolver +struct ScriptConfigResolver : public ConfigResolver { - Luau::Config defaultConfig; + Config defaultConfig; ScriptConfigResolver(); - virtual const Luau::Config& getConfig(const Luau::ModuleName& name) const override; + virtual const Config& getConfig(const ModuleName& name) const override; }; enum class ReportFormat @@ -49,32 +57,55 @@ enum class ReportFormat }; // ------------------------------- Internal Functions ------------------------------- -static void reportError(const Luau::Frontend* frontend, const char* filepath, ReportFormat format, const Luau::TypeError& error); -static void report(ReportFormat format, const char* filepath, const Luau::Location& loc, const char* type, const char* message); +static void reportError(const Frontend* frontend, const char* filepath, ReportFormat format, const TypeError& error); +static void report(ReportFormat format, const char* filepath, const Location& loc, const char* type, const char* message); + +// -- Internal Data -- +constexpr auto autocompleteKindPrecedence = fixedSizeArray( + 0, // Property, + 1, // Binding, + 4, // Keyword, + 5, // String, + 2, // Type, + 3, // Module, + 6 // GeneratedFunction, + ); namespace MathAnim { ScriptAnalyzer::ScriptAnalyzer(const std::filesystem::path& scriptDirectory) : m_scriptDirectory(scriptDirectory) { - Luau::FrontendOptions frontendOptions; - frontendOptions.retainFullTypeGraphs = false; + FrontendOptions frontendOptions; + frontendOptions.retainFullTypeGraphs = true; fileResolver = g_memory_new ScriptFileResolver(scriptDirectory); configResolver = g_memory_new ScriptConfigResolver(); - frontend = g_memory_new Luau::Frontend(fileResolver, configResolver, frontendOptions); + frontend = g_memory_new Frontend(fileResolver, configResolver, frontendOptions); + + unfreeze(frontend->globals.globalTypes); + unfreeze(frontend->globalsForAutocomplete.globalTypes); // Register the bundled builtin globals that come with Luau - Luau::registerBuiltinGlobals(frontend->typeChecker); + registerBuiltinGlobals(*frontend, frontend->globals, true); + + freeze(frontend->globals.globalTypes); + freeze(frontend->globalsForAutocomplete.globalTypes); + { // Register our own builtin globals - Luau::LoadDefinitionFileResult loadResult = - Luau::loadDefinitionFile( - frontend->typeChecker, - frontend->typeChecker.globalScope, - MathAnimGlobals::getBuiltinDefinitionSource(), - "math-anim" - ); + GlobalTypes& globals = frontend->globalsForAutocomplete; + unfreeze(globals.globalTypes); + LoadDefinitionFileResult loadResult = frontend->loadDefinitionFile( + globals, + globals.globalScope, + MathAnimGlobals::getBuiltinDefinitionSource(), + "math-anim", + true, /* Capture comments */ + true /* Typecheck for autocomplete */ + ); + freeze(globals.globalTypes); + if (!loadResult.success) { g_logger_error("The ScriptAnalyzer failed to load math-anim builtin definitions. Errors:"); @@ -86,33 +117,34 @@ namespace MathAnim loadResult.parseResult.errors[i].getLocation().begin.column); } } - - // TODO: Why was this code here and then commented out? - //Luau::TypeArena& arena = frontend->typeChecker.globalTypes; - //arena.addType(loadResult.module.get()->astTypes[0]); } + { - // Register our own builtin types - Luau::LoadDefinitionFileResult loadResult = - Luau::loadDefinitionFile( - frontend->typeChecker, - frontend->typeChecker.globalScope, - MathAnimGlobals::getMathAnimApiTypes(), - "math-anim" - ); + // Register our own builtin globals + GlobalTypes& globals = frontend->globalsForAutocomplete; + unfreeze(globals.globalTypes); + LoadDefinitionFileResult loadResult = frontend->loadDefinitionFile( + globals, + globals.globalScope, + MathAnimGlobals::getMathAnimApiTypes(), + "math-anim", + true, /* Capture comments */ + true /* Typecheck for autocomplete */ + ); + freeze(globals.globalTypes); + if (!loadResult.success) { g_logger_error("The ScriptAnalyzer failed to load math-anim builtin definitions. Errors:"); for (int i = 0; i < loadResult.parseResult.errors.size(); i++) { - g_logger_error("{} at line:column {}:{}", + g_logger_error("{} at line : column {}:{}", loadResult.parseResult.errors[i].getMessage(), loadResult.parseResult.errors[i].getLocation().begin.line, loadResult.parseResult.errors[i].getLocation().begin.column); } } } - Luau::freeze(frontend->typeChecker.globalTypes); } bool ScriptAnalyzer::analyze(const std::string& filename) @@ -128,7 +160,7 @@ namespace MathAnim return false; } - Luau::CheckResult cr; + CheckResult cr; if (frontend->isDirty(filename)) cr = frontend->check(filename); @@ -164,7 +196,7 @@ namespace MathAnim ScriptFileResolver* scriptFileResolver = dynamic_cast(fileResolver); scriptFileResolver->setAnonymousFile(sourceCode, scriptName); - Luau::CheckResult cr; + CheckResult cr; if (frontend->isDirty(scriptName)) cr = frontend->check(scriptName); @@ -183,6 +215,267 @@ namespace MathAnim return cr.errors.size() == 0; } + FunctionIntellisense ScriptAnalyzer::getFunctionParameterIntellisense(std::string const& sourceCode, std::string const& scriptName, uint32 line, uint32 column) + { + // TODO: Abstract this stuff into a check function + ScriptFileResolver* scriptFileResolver = dynamic_cast(fileResolver); + scriptFileResolver->setAnonymousFile(sourceCode, scriptName); + + CheckResult cr; + frontend->markDirty(scriptName); + + FrontendOptions frontendOpts; + frontendOpts.forAutocomplete = true; + frontendOpts.retainFullTypeGraphs = true; + cr = frontend->check(scriptName, frontendOpts); + + auto mainSource = frontend->getSourceModule(scriptName); + + // If this is nullptr, we can't get type information + if (!mainSource) + { + return {}; + } + + AstExpr* astExpr = findExprAtPosition(*mainSource, Position(line - 1, column)); + if (!astExpr) + { + return {}; + } + + AstExprCall* exprCall = astExpr->as(); + if (!exprCall || !exprCall->func) + { + return {}; + } + + FunctionIntellisense res = {}; + Position fnIdentifierBegin = Position(line - 1, column + 1); + if (auto* funcName = exprCall->func->as(); funcName && funcName->index.value) + { + res.fnName = funcName->index.value; + fnIdentifierBegin = funcName->indexLocation.begin; + } + else if (auto* globalFunc = exprCall->func->as(); globalFunc && globalFunc->name.value) + { + res.fnName = globalFunc->name.value; + fnIdentifierBegin = globalFunc->location.begin; + } + else if (auto* localFunc = exprCall->func->as(); + localFunc && localFunc->local && localFunc->local->name.value) + { + res.fnName = localFunc->local->name.value; + fnIdentifierBegin = localFunc->location.begin; + } + else + { + return {}; + } + + auto mainModule = frontend->moduleResolverForAutocomplete.getModule(scriptName); + if (!mainModule) + { + return {}; + } + + std::optional type = findTypeAtPosition(*mainModule, *mainSource, fnIdentifierBegin); + if (!type.has_value()) + { + return {}; + } + + TypeId id = follow(type.value()); + FunctionType const* fnType = get(id); + if (!fnType) + { + return {}; + } + + auto [argTypes, argVariadicPack] = flatten(fnType->argTypes); + for (size_t i = 0; i < argTypes.size(); i++) + { + // TODO: Find out if there's a way to get a type prefix. + // Like, if you import a module then name it ModuleImport.Type + // how can I find out what ModuleImport is called here? + FunctionParameter param = {}; + if (i < fnType->argNames.size() && fnType->argNames[i].has_value()) + { + param.name = fnType->argNames[i]->name; + } + + TypeId argType = follow(argTypes[i]); + + if (auto* asError = get(argType); asError) + { + param.stringifiedType = "T"; + res.parameters.emplace_back(param); + } + else + { + param.stringifiedType = toString(argType); + res.parameters.emplace_back(param); + } + } + + auto [retTypes, retVariadicPack] = flatten(fnType->retTypes); + for (size_t i = 0; i < retTypes.size(); i++) + { + res.returnTypes.emplace_back(toString(retTypes[i])); + } + + // Stringify the function info then parse it to get syntax highlight info + std::string stringifiedFunctionType = ""; + stringifiedFunctionType += "type " + res.fnName + " = ("; + + for (size_t i = 0; i < res.parameters.size(); i++) + { + if (res.parameters[i].name.has_value()) + { + stringifiedFunctionType += res.parameters[i].name.value() + ": "; + } + stringifiedFunctionType += res.parameters[i].stringifiedType; + + if (i < res.parameters.size() - 1) + { + stringifiedFunctionType += ", "; + } + } + + stringifiedFunctionType += "): ("; + for (size_t i = 0; i < res.returnTypes.size(); i++) + { + stringifiedFunctionType += res.returnTypes[i]; + + if (i < res.returnTypes.size() - 1) + { + stringifiedFunctionType += ", "; + } + } + + stringifiedFunctionType += ")"; + + auto const& highlighter = CodeEditorPanelManager::getHighlighter(); + auto const& theme = CodeEditorPanelManager::getTheme(); + res.highlightInfo = highlighter.parse(stringifiedFunctionType.c_str(), stringifiedFunctionType.size(), theme); + + return res; + } + + static std::optional nullCallback(std::string /*tag*/, std::optional /*ptr*/, std::optional /*contents*/) + { + return std::nullopt; + } + + static void sortSuggestionsByRankAndKind(std::vector& suggestions) + { + std::sort(suggestions.begin(), suggestions.end(), [](AutocompleteSuggestion const& a, AutocompleteSuggestion const& b) + { + if (a.data.kind == b.data.kind) + { + return a.rank > b.rank; + } + + // If they're different kinds of stuff, rank by kind of suggestion. + // For example, suggestions for variable properties should rank higher than keywords + return autocompleteKindPrecedence[(int)a.data.kind] < autocompleteKindPrecedence[(int)b.data.kind]; + }); + } + + std::vector ScriptAnalyzer::getSuggestions(std::string const& sourceCode, std::string const& scriptName, uint32 line, uint32 column) + { + if (!fileResolver || !configResolver || !frontend) + { + static bool displayWarning = true; + if (displayWarning) + { + g_logger_warning("Tried to sandbox a script, but the script analyzer was not initialized properly. Suppressing this message now."); + displayWarning = false; + } + return {}; + } + + ScriptFileResolver* scriptFileResolver = dynamic_cast(fileResolver); + scriptFileResolver->setAnonymousFile(sourceCode, scriptName); + + CheckResult cr; + frontend->markDirty(scriptName); + frontend->check(scriptName); + + FrontendOptions opts; + opts.forAutocomplete = true; + cr = frontend->check(scriptName, opts); + + auto autocompleteRes = autocomplete( + *frontend, + scriptName, + Position(line - 1, column - 1), + nullCallback + ); + + std::vector suggestions = {}; + for (auto& [key, val] : autocompleteRes.entryMap) + { + AutocompleteSuggestion suggestion = {}; + suggestion.text = key; + suggestion.data = val; + suggestion.rank = 0.0f; + suggestions.emplace_back(suggestion); + } + + frontend->clear(); + + sortSuggestionsByRankAndKind(suggestions); + + return suggestions; + } + + void ScriptAnalyzer::sortSuggestionsByQuery(std::string const& query, std::vector& suggestions, std::vector& visibleSuggestions) + { + // Re-sort suggestions + sortSuggestionsByRankAndKind(suggestions); + + // Then rank the suggestions + std::string lowercaseQuery{}; + lowercaseQuery.reserve(query.size()); + for (size_t i = 0; i < query.size(); i++) + { + lowercaseQuery += (char)std::tolower(query[i]); + } + + visibleSuggestions.clear(); + + // Re-rank all suggestions according to new query + int index = 0; + for (auto& suggestion : suggestions) + { + std::string lowercaseSuggestion{}; + lowercaseSuggestion.reserve(suggestion.text.size()); + for (size_t i = 0; i < suggestion.text.size(); i++) + { + lowercaseSuggestion += (char)std::tolower(suggestion.text[i]); + } + + suggestion.rank = (float)rapidfuzz::fuzz::partial_ratio(lowercaseQuery, lowercaseSuggestion); + + // Only do more expensive string checks if ranking is similar + if (suggestion.rank > 0.0f) + { + suggestion.containsQuery = lowercaseSuggestion.find(lowercaseQuery) != std::string::npos; + + if (suggestion.containsQuery) + { + visibleSuggestions.push_back(index); + } + } + else if (query == "") + { + visibleSuggestions.push_back(index); + } + + index++; + } + } + void ScriptAnalyzer::free() { g_memory_delete(fileResolver); @@ -207,11 +500,11 @@ void ScriptFileResolver::setAnonymousFile(const std::string& source, const std:: anonymousName = name; } -std::optional ScriptFileResolver::readSource(const Luau::ModuleName& name) +std::optional ScriptFileResolver::readSource(const ModuleName& name) { if (name == "math-anim" || name == "math-anim.luau") { - Luau::SourceCode res; + SourceCode res; res.type = res.Module; res.source = MathAnim::MathAnimGlobals::getMathAnimModule(); return res; @@ -220,7 +513,7 @@ std::optional ScriptFileResolver::readSource(const Luau::Modul std::string scriptPath = (scriptDirectory / name).string(); if (!MathAnim::Platform::fileExists(scriptPath.c_str()) && anonymousName == name) { - Luau::SourceCode res; + SourceCode res; res.source = anonymousSource; res.type = res.Module; anonymousName = "UNDEFINED"; @@ -228,7 +521,7 @@ std::optional ScriptFileResolver::readSource(const Luau::Modul return res; } - Luau::SourceCode res; + SourceCode res; res.type = res.Module; FILE* fp = fopen(scriptPath.c_str(), "rb"); @@ -261,18 +554,18 @@ std::optional ScriptFileResolver::readSource(const Luau::Modul return res; } -std::optional ScriptFileResolver::resolveModule(const Luau::ModuleInfo*, Luau::AstExpr* node) +std::optional ScriptFileResolver::resolveModule(const ModuleInfo*, AstExpr* node) { - if (Luau::AstExprConstantString* expr = node->as()) + if (AstExprConstantString* expr = node->as()) { - Luau::ModuleName name = std::string(expr->value.data, expr->value.size) + ".luau"; + ModuleName name = std::string(expr->value.data, expr->value.size) + ".luau"; return { {name} }; } return std::nullopt; } -std::string ScriptFileResolver::getHumanReadableModuleName(const Luau::ModuleName& name) const +std::string ScriptFileResolver::getHumanReadableModuleName(const ModuleName& name) const { if (name == "-") return "stdin"; @@ -282,25 +575,27 @@ std::string ScriptFileResolver::getHumanReadableModuleName(const Luau::ModuleNam // ------------------------------- Config Resolver ------------------------------- ScriptConfigResolver::ScriptConfigResolver() { - defaultConfig.mode = Luau::Mode::Strict; + defaultConfig.mode = Mode::Strict; + defaultConfig.enabledLint.warningMask = ~0ull; + defaultConfig.parseOptions.captureComments = true; } -const Luau::Config& ScriptConfigResolver::getConfig(const Luau::ModuleName&) const +const Config& ScriptConfigResolver::getConfig(const ModuleName&) const { return defaultConfig; } // ------------------------------- Internal Functions ------------------------------- -static void reportError(const Luau::Frontend* frontend, const char* filepath, ReportFormat format, const Luau::TypeError& error) +static void reportError(const Frontend* frontend, const char* filepath, ReportFormat format, const TypeError& error) { - if (const Luau::SyntaxError* syntaxError = Luau::get_if(&error.data)) + if (const SyntaxError* syntaxError = get_if(&error.data)) report(format, filepath, error.location, "SyntaxError", syntaxError->message.c_str()); else report(format, filepath, error.location, "TypeError", - Luau::toString(error, Luau::TypeErrorToStringOptions{ frontend->fileResolver }).c_str()); + toString(error, TypeErrorToStringOptions{ frontend->fileResolver }).c_str()); } -static void report(ReportFormat format, const char* filepath, const Luau::Location& loc, const char* type, const char* message) +static void report(ReportFormat format, const char* filepath, const Location& loc, const char* type, const char* message) { switch (format) { diff --git a/Animations/vendor/CMakeLists.txt b/Animations/vendor/CMakeLists.txt index 54eb8505..f95e6ca6 100644 --- a/Animations/vendor/CMakeLists.txt +++ b/Animations/vendor/CMakeLists.txt @@ -145,3 +145,9 @@ set(OPTICK_INSTALL_TARGETS OFF) set(OPTICK_ENABLED ON) add_subdirectory(optick) + + +############## +# Rapid Fuzz # +############## +add_subdirectory(rapidFuzz) diff --git a/Animations/vendor/luau b/Animations/vendor/luau index fb2f1461..bac85116 160000 --- a/Animations/vendor/luau +++ b/Animations/vendor/luau @@ -1 +1 @@ -Subproject commit fb2f146123dcb423de6bc789d4f7d01f4981170c +Subproject commit bac85116f641de2ebac101fa30b95600b66705e6 diff --git a/Animations/vendor/rapidFuzz b/Animations/vendor/rapidFuzz new file mode 160000 index 00000000..ef899934 --- /dev/null +++ b/Animations/vendor/rapidFuzz @@ -0,0 +1 @@ +Subproject commit ef8999342dfd7b8d4603cda73c1da0df847782f9