diff --git a/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf b/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf index 15cb6fee..f56224ba 100644 --- a/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf +++ b/BeefLibs/Beefy2D/src/theme/dark/DarkTheme.bf @@ -185,13 +185,14 @@ namespace Beefy.theme.dark COUNT }; - public static uint32 COLOR_TEXT = 0xFFFFFFFF; - public static uint32 COLOR_WINDOW = 0xFF595962; - public static uint32 COLOR_BKG = 0xFF26262A; - public static uint32 COLOR_SELECTED_OUTLINE = 0xFFCFAE11; - public static uint32 COLOR_MENU_FOCUSED = 0xFFE5A910; - public static uint32 COLOR_MENU_SELECTED = 0xFFCB9B80; - public static uint32 COLOR_CURRENT_LINE_HILITE = 0xFF4C4C54; + public static uint32 COLOR_TEXT = 0xFFFFFFFF; + public static uint32 COLOR_WINDOW = 0xFF595962; + public static uint32 COLOR_BKG = 0xFF26262A; + public static uint32 COLOR_SELECTED_OUTLINE = 0xFFCFAE11; + public static uint32 COLOR_MENU_FOCUSED = 0xFFE5A910; + public static uint32 COLOR_MENU_SELECTED = 0xFFCB9B80; + public static uint32 COLOR_CURRENT_LINE_HILITE = 0xFF4C4C54; + public static uint32 COLOR_MATCHING_PARENS_HILITE = 0x28FFFFFF; public static float sScale = 1.0f; public static int32 sSrcImgScale = 1; diff --git a/BeefLibs/Beefy2D/src/widgets/EditWidget.bf b/BeefLibs/Beefy2D/src/widgets/EditWidget.bf index 2880a9ae..7e3d35ec 100644 --- a/BeefLibs/Beefy2D/src/widgets/EditWidget.bf +++ b/BeefLibs/Beefy2D/src/widgets/EditWidget.bf @@ -2661,6 +2661,12 @@ namespace Beefy.widgets GetLineAndColumnAtCoord(x, y, out line2, out column); } + public void GetLineColumnAtIdx(int idx, out int line, out int column) + { + GetLineCharAtIdx(idx, out line, var lineChar); + GetColumnAtLineChar(line, lineChar, out column); + } + public virtual void GetLineCharAtIdx(int idx, out int line, out int theChar) { int lineA; diff --git a/IDE/src/Settings.bf b/IDE/src/Settings.bf index 89dd0f2c..2338f38c 100644 --- a/IDE/src/Settings.bf +++ b/IDE/src/Settings.bf @@ -326,6 +326,7 @@ namespace IDE public Color mVisibleWhiteSpace = 0xFF9090C0; public Color mCurrentLineHilite = 0xFF4C4C54; public Color mCurrentLineNumberHilite = 0x18FFFFFF; + public Color mMatchingParensHilite = 0x28FFFFFF; public void Deserialize(StructuredData sd) { @@ -386,6 +387,7 @@ namespace IDE GetColor("VisibleWhiteSpace", ref mVisibleWhiteSpace); GetColor("CurrentLineHilite", ref mCurrentLineHilite); GetColor("CurrentLineNumberHilite", ref mCurrentLineNumberHilite); + GetColor("MatchingParensHilite", ref mMatchingParensHilite); } public void Apply() @@ -417,6 +419,7 @@ namespace IDE DarkTheme.COLOR_MENU_FOCUSED = mMenuFocused; DarkTheme.COLOR_MENU_SELECTED = mMenuSelected; DarkTheme.COLOR_CURRENT_LINE_HILITE = mCurrentLineHilite; + DarkTheme.COLOR_MATCHING_PARENS_HILITE = mMatchingParensHilite; } } diff --git a/IDE/src/ui/SourceEditWidgetContent.bf b/IDE/src/ui/SourceEditWidgetContent.bf index f9363bf5..4c248b44 100644 --- a/IDE/src/ui/SourceEditWidgetContent.bf +++ b/IDE/src/ui/SourceEditWidgetContent.bf @@ -183,6 +183,14 @@ namespace IDE.ui OnlyShowInvoke = 4 } + enum HiliteMatchingParensPositionCache + { + //TODO: Better naming? + case NeedToRecalculate; + case UnmatchedParens; + case Valid(float x1, float y1, float x2, float y2); + } + public delegate void(char32, AutoCompleteOptions) mOnGenerateAutocomplete ~ delete _; public Action mOnFinishAsyncAutocomplete ~ delete _; public Action mOnCancelAsyncAutocomplete ~ delete _; @@ -228,6 +236,7 @@ namespace IDE.ui bool mHasCustomColors; FastCursorState mFastCursorState ~ delete _; public HashSet mCurParenPairIdSet = new .() ~ delete _; + HiliteMatchingParensPositionCache mMatchingParensPositionCache = .NeedToRecalculate; public List PersistentTextPositions { @@ -3047,6 +3056,7 @@ namespace IDE.ui { base.ContentChanged(); mCursorStillTicks = 0; + mMatchingParensPositionCache = .NeedToRecalculate; if (mSourceViewPanel != null) { if (mSourceViewPanel.mProjectSource != null) @@ -4557,6 +4567,7 @@ namespace IDE.ui base.PhysCursorMoved(moveKind); mCursorStillTicks = 0; + mMatchingParensPositionCache = .NeedToRecalculate; if ((mSourceViewPanel != null) && (mSourceViewPanel.mHoverWatch != null)) mSourceViewPanel.mHoverWatch.Close(); @@ -4703,6 +4714,107 @@ namespace IDE.ui { base.Draw(g); + // Highlight matching parenthesis under cursor + if (mEditWidget.mHasFocus && !HasSelection()) + { + if (mMatchingParensPositionCache case .NeedToRecalculate) + { + mMatchingParensPositionCache = .UnmatchedParens; + + bool IsParenthesisAt(int textIndex) + { + switch (SafeGetChar(textIndex)) + { + // Ignore parentheses in comments. + case '(',')', '[',']', '{','}': return (SourceElementType)mData.mText[textIndex].mDisplayTypeId != .Comment; + default: return false; + } + } + + bool IsOpenParenthesis(char8 c) + { + switch (c) + { + case '(', '{', '[': return true; + default: return false; + } + } + + Result GetMatchingParenthesis(char8 paren) + { + switch (paren) + { + case '(': return ')'; + case '{': return '}'; + case '[': return ']'; + + case ')': return '('; + case '}': return '{'; + case ']': return '['; + + default: return .Err; + } + } + + int parenIndex = -1; + // If there is a parenthesis to the right of the cursor, match that one. + // Otherwise, if there is one to the left of the cursor, match that. + // This is what Visual Studio and VS Code do. + // Notepad++ tries to match to the left of the cursor first, but I think the other way makes more sense. + if (IsParenthesisAt(CursorTextPos)) + parenIndex = CursorTextPos; + else if (IsParenthesisAt(CursorTextPos - 1)) + parenIndex = CursorTextPos - 1; + + if (parenIndex != -1) + { + char8 paren = mData.mText[parenIndex].mChar; + char8 matchingParen = GetMatchingParenthesis(paren); + int dir = IsOpenParenthesis(paren) ? +1 : -1; + + int matchingParenIndex = -1; + int stackCount = 1; + for (int i = parenIndex + dir; i >= 0 && i < mData.mTextLength; i += dir) + { + if ((SourceElementType)mData.mText[i].mDisplayTypeId != .Comment) + { + char8 char = mData.mText[i].mChar; + if (char == paren) + ++stackCount; + else if (char == matchingParen) + --stackCount; + + if (stackCount == 0) + { + matchingParenIndex = i; + break; + } + } + } + + if (matchingParenIndex != -1) + { + GetLineColumnAtIdx(parenIndex, var line1, var column1); + GetLineColumnAtIdx(matchingParenIndex, var line2, var column2); + GetTextCoordAtLineAndColumn(line1, column1, var x1, var y1); + GetTextCoordAtLineAndColumn(line2, column2, var x2, var y2); + mMatchingParensPositionCache = .Valid(x1, y1, x2, y2); + } + } + } + + if (mMatchingParensPositionCache case .Valid(let x1, let y1, let x2, let y2)) + { + let width = mFont.GetWidth(' '); + let height = mFont.GetHeight() + GS!(2); + using (g.PushColor(DarkTheme.COLOR_MATCHING_PARENS_HILITE)) + { + g.FillRect(x1, y1, width, height); + g.FillRect(x2, y2, width, height); + } + } + } + using (g.PushTranslate(mTextInsets.mLeft, mTextInsets.mTop)) { for (var queuedUnderline in mQueuedUnderlines)