{"TotalCount":37,"Files":[{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/CodeView/CodeCanvas.cs","FileName":"CodeCanvas.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\r\nusing Editor;\r\nusing System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\n\r\n/// \u003Csummary\u003E\r\n/// The painted surface inside a \u003Csee cref=\u0022CodeView\u0022/\u003E. Its size is the size of the whole\r\n/// document; the scroll area moves it around and we only draw the rows in view.\r\n///\r\n/// Text is laid out in visual rows: one per line, or several when word wrap splits a line.\r\n/// \u003C/summary\u003E\r\npublic class CodeCanvas : Widget\r\n{\r\n\tconst float TextPadding = 8;\r\n\tprivate static float FontSize =\u003E CodeView.FontSize;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Openers that get their closer typed for them, and the characters they may be followed by.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static readonly Dictionary\u003Cchar, char\u003E AutoClosePairs = new() { [\u0027(\u0027] = \u0027)\u0027, [\u0027[\u0027] = \u0027]\u0027, [\u0027{\u0027] = \u0027}\u0027, [\u0027\u0022\u0027] = \u0027\u0022\u0027, [\u0027\\\u0027\u0027] = \u0027\\\u0027\u0027 };\r\n\tprivate const string AutoCloseBefore = \u0022;:.,=}])\u003E \\t\u0022;\r\n\r\n\tpublic TextBuffer Buffer { get; } = new();\r\n\tpublic UndoHistory History { get; } = new();\r\n\r\n\tprivate readonly CodeView _owner;\r\n\tprivate readonly ScrollArea _scroller;\r\n\tprivate readonly SyntaxLanguage _language;\r\n\r\n\t// Caret and selection. The selection runs from Anchor to Caret; no anchor means no selection.\r\n\tprivate TextPos _caret;\r\n\tprivate TextPos? _anchor;\r\n\tprivate float? _preferredColumn;\r\n\r\n\t// Font metrics, measured on first paint\r\n\tprivate float _charWidth = 8;\r\n\tprivate float _lineHeight = 18;\r\n\tprivate bool _measured;\r\n\r\n\t// Highlighting: the lexer state each line starts in, recomputed after edits\r\n\tprivate readonly List\u003CLexState\u003E _lineStates = new();\r\n\tprivate bool _statesDirty = true;\r\n\r\n\t// Row layout\r\n\tprivate readonly List\u003Cint[]\u003E _rowStarts = new();\r\n\tprivate int[] _lineFirstRow = { 0, 1 };\r\n\tprivate int _longestLine;\r\n\tprivate bool _layoutDirty = true;\r\n\tprivate bool _wrapApplied;\r\n\tprivate int _wrapColumns;\r\n\r\n\t// Find highlights\r\n\tprivate IReadOnlyList\u003CSearchMatch\u003E _matches = Array.Empty\u003CSearchMatch\u003E();\r\n\tprivate int _currentMatch = -1;\r\n\r\n\t// Compile errors / warnings, underlined\r\n\tprivate IReadOnlyList\u003CEditorMarker\u003E _markers = Array.Empty\u003CEditorMarker\u003E();\r\n\r\n\t// Word under the mouse while Ctrl is held, drawn like a link for Ctrl\u002BClick\r\n\tprivate (TextPos Start, TextPos End)? _link;\r\n\r\n\t// Autocomplete: everything Roslyn suggested for the word starting at (line, column); the popup shows the filtered list\r\n\tprivate CompletionPopup _popup;\r\n\tprivate IReadOnlyList\u003CCompletionItem\u003E _completionItems = Array.Empty\u003CCompletionItem\u003E();\r\n\tprivate int _completionLine = -1;\r\n\tprivate int _completionStart;\r\n\r\n\t// Parameter hints for the call the caret is in\r\n\tprivate SignaturePopup _signaturePopup;\r\n\tprivate SignatureHelpResult _signature;\r\n\tprivate bool _signaturePicked;\r\n\r\n\tprivate bool _ensureCaretVisible;\r\n\tprivate bool _dragging;\r\n\tprivate double _caretBlinkStart;\r\n\tprivate Vector2 _lastScroll;\r\n\tprivate bool _lastCaretOn;\r\n\tprivate int _settingsVersion = HotCodeSettings.Version;\r\n\r\n\tprivate string _indentUnit = \u0022\\t\u0022;\r\n\r\n\tpublic CodeCanvas( CodeView owner, ScrollArea scroller, SyntaxLanguage language ) : base( scroller )\r\n\t{\r\n\t\t_owner = owner;\r\n\t\t_scroller = scroller;\r\n\t\t_language = language;\r\n\r\n\t\tFocusMode = FocusMode.Click;\r\n\t\tCursor = CursorShape.IBeam;\r\n\t\tMouseTracking = true;\r\n\t}\r\n\r\n\tpublic TextPos Caret =\u003E _caret;\r\n\tpublic string IndentUnit =\u003E _indentUnit;\r\n\tpublic string LanguageName =\u003E _language.Name;\r\n\tpublic bool HasSelection =\u003E _anchor.HasValue \u0026\u0026 _anchor.Value != _caret;\r\n\tpublic TextPos SelStart =\u003E HasSelection ? TextPos.Min( _anchor.Value, _caret ) : _caret;\r\n\tpublic TextPos SelEnd =\u003E HasSelection ? TextPos.Max( _anchor.Value, _caret ) : _caret;\r\n\tpublic string SelectedText =\u003E HasSelection ? Buffer.GetText( SelStart, SelEnd ) : \u0022\u0022;\r\n\r\n\tprivate bool WordWrap =\u003E CodeView.WordWrap;\r\n\tprivate bool AutoClose =\u003E !_language.IsPlain \u0026\u0026 HotCodeSettings.AutoCloseBrackets;\r\n\r\n\tpublic void SetText( string text )\r\n\t{\r\n\t\tBuffer.Text = text;\r\n\t\t_indentUnit = DetectIndent();\r\n\t\t_caret = default;\r\n\t\t_anchor = null;\r\n\t\tHistory.Clear();\r\n\t\tOnBufferChanged( notify: false );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Swaps in new text (changed on disk) while keeping the caret roughly where it was.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void ReloadText( string text )\r\n\t{\r\n\t\tvar caret = _caret;\r\n\t\tBuffer.Text = text;\r\n\t\t_caret = Buffer.Clamp( caret );\r\n\t\t_anchor = null;\r\n\t\tHistory.Clear();\r\n\t\tOnBufferChanged( notify: false );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Tabs unless the file clearly indents with spaces.\r\n\t/// \u003C/summary\u003E\r\n\tprivate string DetectIndent()\r\n\t{\r\n\t\tint tabs = 0, spaces = 0;\r\n\t\tforeach ( var line in Buffer.Lines.Take( 2000 ) )\r\n\t\t{\r\n\t\t\tif ( line.StartsWith( \u0027\\t\u0027 ) ) tabs\u002B\u002B;\r\n\t\t\telse if ( line.StartsWith( \u0022    \u0022 ) ) spaces\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tif ( spaces == tabs ) return HotCodeSettings.IndentWithSpaces ? new string( \u0027 \u0027, TextLayout.TabSize ) : \u0022\\t\u0022;\r\n\t\treturn spaces \u003E tabs ? new string( \u0027 \u0027, TextLayout.TabSize ) : \u0022\\t\u0022;\r\n\t}\r\n\r\n\t// --- LAYOUT ---\r\n\r\n\tprivate float GutterWidth =\u003E HotCodeSettings.ShowLineNumbers ? (Math.Max( 3, Buffer.LineCount.ToString().Length ) \u002B 2) * _charWidth : 0;\r\n\tprivate float TextLeft =\u003E GutterWidth \u002B TextPadding;\r\n\r\n\tprivate Vector2 Scroll =\u003E new( _scroller.HorizontalScrollbar.Value, _scroller.VerticalScrollbar.Value );\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The visible part of the scroll area (roughly; scrollbars eat a little).\r\n\t/// \u003C/summary\u003E\r\n\tprivate Vector2 ViewportSize =\u003E _scroller.Size - 16;\r\n\r\n\tprivate int TotalRows =\u003E _lineFirstRow[^1];\r\n\r\n\tpublic void InvalidateLayout()\r\n\t{\r\n\t\t_layoutDirty = true;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprivate void UpdateLayout()\r\n\t{\r\n\t\tif ( !_layoutDirty ) return;\r\n\t\t_layoutDirty = false;\r\n\r\n\t\t_wrapApplied = WordWrap;\r\n\t\t_wrapColumns = _wrapApplied ? Math.Max( 20, (int)((ViewportSize.x - TextLeft - TextPadding) / _charWidth) ) : 0;\r\n\r\n\t\t_rowStarts.Clear();\r\n\t\t_lineFirstRow = new int[Buffer.LineCount \u002B 1];\r\n\t\t_longestLine = 0;\r\n\r\n\t\tvar rows = 0;\r\n\t\tfor ( int i = 0; i \u003C Buffer.LineCount; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar line = Buffer.Lines[i];\r\n\t\t\t_lineFirstRow[i] = rows;\r\n\r\n\t\t\tvar starts = TextLayout.Wrap( line, _wrapColumns );\r\n\t\t\t_rowStarts.Add( starts );\r\n\t\t\trows \u002B= starts.Length;\r\n\r\n\t\t\tif ( !_wrapApplied ) _longestLine = Math.Max( _longestLine, TextLayout.Width( line ) );\r\n\t\t}\r\n\r\n\t\t_lineFirstRow[Buffer.LineCount] = rows;\r\n\r\n\t\t// Extra space at the bottom so the last line can be scrolled up the screen\r\n\t\tvar width = _wrapApplied ? _scroller.Width - 2 : MathF.Max( TextLeft \u002B (_longestLine \u002B 4) * _charWidth, _scroller.Width - 2 );\r\n\t\tvar height = MathF.Max( rows * _lineHeight \u002B MathF.Max( 0, _scroller.Height - _lineHeight * 3 ), _scroller.Height - 2 );\r\n\r\n\t\tMinimumSize = new Vector2( width, height );\r\n\t\tMaximumSize = _wrapApplied ? new Vector2( width, 1_000_000 ) : new Vector2( 1_000_000, 1_000_000 );\r\n\t\tSize = new Vector2( width, height );\r\n\t}\r\n\r\n\tprivate int RowEnd( int line, int row )\r\n\t{\r\n\t\tvar starts = _rowStarts[line];\r\n\t\treturn row \u002B 1 \u003C starts.Length ? starts[row \u002B 1] : Buffer.Lines[line].Length;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Which row of its line a position is on. A position exactly at a wrap point is on the next row.\r\n\t/// \u003C/summary\u003E\r\n\tprivate int RowInLine( TextPos p )\r\n\t{\r\n\t\tvar starts = _rowStarts[p.Line];\r\n\t\tvar row = 0;\r\n\t\twhile ( row \u002B 1 \u003C starts.Length \u0026\u0026 starts[row \u002B 1] \u003C= p.Col ) row\u002B\u002B;\r\n\t\treturn row;\r\n\t}\r\n\r\n\tprivate int LineOfRow( int row )\r\n\t{\r\n\t\tvar i = Array.BinarySearch( _lineFirstRow, 0, Buffer.LineCount, row );\r\n\t\tif ( i \u003C 0 ) i = ~i - 1;\r\n\r\n\t\t// Several lines can\u0027t share a first row, but guard against empty layouts\r\n\t\treturn Math.Clamp( i, 0, Buffer.LineCount - 1 );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Position in a row nearest to a column. On wrapped rows (not the last) the end is the\r\n\t/// character before the break, so the caret doesn\u0027t jump to the next row.\r\n\t/// \u003C/summary\u003E\r\n\tprivate TextPos PosInRow( int globalRow, float column )\r\n\t{\r\n\t\tglobalRow = Math.Clamp( globalRow, 0, TotalRows - 1 );\r\n\t\tvar line = LineOfRow( globalRow );\r\n\t\tvar row = globalRow - _lineFirstRow[line];\r\n\t\tvar text = Buffer.Lines[line];\r\n\t\tvar start = _rowStarts[line][row];\r\n\t\tvar end = RowEnd( line, row );\r\n\t\tvar isLastRow = row == _rowStarts[line].Length - 1;\r\n\r\n\t\tvar index = TextLayout.IndexAtColumn( text, start, end, column );\r\n\t\tif ( !isLastRow \u0026\u0026 index \u003E= end \u0026\u0026 end \u003E start ) index = end - 1;\r\n\r\n\t\treturn new TextPos( line, index );\r\n\t}\r\n\r\n\tprivate TextPos PosAt( Vector2 local ) =\u003E PosInRow( (int)MathF.Floor( local.y / _lineHeight ), (local.x - TextLeft) / _charWidth );\r\n\r\n\tprivate int GlobalRow( TextPos p ) =\u003E _lineFirstRow[p.Line] \u002B RowInLine( p );\r\n\r\n\tprivate float ColumnInRow( TextPos p ) =\u003E TextLayout.ColumnOf( Buffer.Lines[p.Line], _rowStarts[p.Line][RowInLine( p )], p.Col );\r\n\r\n\tprivate Rect CaretRect( TextPos p ) =\u003E new( TextLeft \u002B ColumnInRow( p ) * _charWidth, GlobalRow( p ) * _lineHeight, 2, _lineHeight );\r\n\r\n\t// --- SELECTION / CARET ---\r\n\r\n\tprivate void MoveCaret( TextPos to, bool extendSelection, bool keepPreferredColumn = false )\r\n\t{\r\n\t\tif ( extendSelection ) _anchor ??= _caret;\r\n\t\telse _anchor = null;\r\n\r\n\t\t_caret = Buffer.Clamp( to );\r\n\t\tif ( !keepPreferredColumn ) _preferredColumn = null;\r\n\r\n\t\tHistory.Break();\r\n\t\tCaretMoved();\r\n\t}\r\n\r\n\tprivate void CaretMoved()\r\n\t{\r\n\t\t_caretBlinkStart = RealTime.Now;\r\n\t\t_ensureCaretVisible = true;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tpublic void Select( TextPos start, TextPos end )\r\n\t{\r\n\t\t_anchor = Buffer.Clamp( start );\r\n\t\t_caret = Buffer.Clamp( end );\r\n\t\t_preferredColumn = null;\r\n\t\tHistory.Break();\r\n\t\tCaretMoved();\r\n\t}\r\n\r\n\tpublic void SelectAll() =\u003E Select( new TextPos( 0, 0 ), Buffer.End );\r\n\r\n\tpublic void SetMarkers( IReadOnlyList\u003CEditorMarker\u003E markers )\r\n\t{\r\n\t\t_markers = markers ?? Array.Empty\u003CEditorMarker\u003E();\r\n\t\tUpdate();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Puts the caret at a position and scrolls it into view.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void GoTo( TextPos pos )\r\n\t{\r\n\t\t_anchor = null;\r\n\t\t_caret = Buffer.Clamp( pos );\r\n\t\t_preferredColumn = null;\r\n\t\tHistory.Break();\r\n\t\tCaretMoved();\r\n\t\tCenterOnCaret();\r\n\t}\r\n\r\n\tpublic void SetFindHighlights( IReadOnlyList\u003CSearchMatch\u003E matches, int current )\r\n\t{\r\n\t\t_matches = matches ?? Array.Empty\u003CSearchMatch\u003E();\r\n\t\t_currentMatch = current;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\t// --- EDITING ---\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Replaces the range a..b with text, records undo, and puts the caret after the new text\r\n\t/// (or at \u003Cparamref name=\u0022caretAfter\u0022/\u003E).\r\n\t/// \u003C/summary\u003E\r\n\tpublic void Replace( TextPos a, TextPos b, string text, EditKind kind = EditKind.Other, TextPos? caretAfter = null )\r\n\t{\r\n\t\tvar start = Buffer.Clamp( TextPos.Min( a, b ) );\r\n\t\tvar end = Buffer.Clamp( TextPos.Max( a, b ) );\r\n\t\ttext = TextBuffer.Normalize( text );\r\n\r\n\t\tvar removed = Buffer.GetText( start, end );\r\n\t\tif ( removed.Length == 0 \u0026\u0026 text.Length == 0 ) return;\r\n\r\n\t\tvar caretBefore = _caret;\r\n\t\tvar anchorBefore = _anchor;\r\n\r\n\t\tBuffer.Delete( start, end );\r\n\t\tvar newEnd = Buffer.Insert( start, text );\r\n\r\n\t\t_caret = Buffer.Clamp( caretAfter ?? newEnd );\r\n\t\t_anchor = null;\r\n\t\t_preferredColumn = null;\r\n\r\n\t\tHistory.Record( new BufferEdit( start, removed, text ), kind, caretBefore, anchorBefore, _caret, RealTime.Now );\r\n\t\tOnBufferChanged( notify: true );\r\n\t}\r\n\r\n\tpublic void BeginTransaction() =\u003E History.BeginTransaction( _caret, _anchor, RealTime.Now );\r\n\tpublic void EndTransaction() =\u003E History.EndTransaction( _caret, _anchor );\r\n\r\n\tpublic void Undo()\r\n\t{\r\n\t\tCloseCompletions();\r\n\t\tCloseSignatureHelp();\r\n\t\tvar group = History.Undo( Buffer );\r\n\t\tif ( group is null ) return;\r\n\r\n\t\t_caret = Buffer.Clamp( group.CaretBefore );\r\n\t\t_anchor = group.AnchorBefore.HasValue ? Buffer.Clamp( group.AnchorBefore.Value ) : null;\r\n\t\tOnBufferChanged( notify: true );\r\n\t}\r\n\r\n\tpublic void Redo()\r\n\t{\r\n\t\tCloseCompletions();\r\n\t\tCloseSignatureHelp();\r\n\t\tvar group = History.Redo( Buffer );\r\n\t\tif ( group is null ) return;\r\n\r\n\t\t_caret = Buffer.Clamp( group.CaretAfter );\r\n\t\t_anchor = group.AnchorAfter.HasValue ? Buffer.Clamp( group.AnchorAfter.Value ) : null;\r\n\t\tOnBufferChanged( notify: true );\r\n\t}\r\n\r\n\tprivate void OnBufferChanged( bool notify )\r\n\t{\r\n\t\t_statesDirty = true;\r\n\t\t_layoutDirty = true;\r\n\t\tUpdateLayout();\r\n\t\tCaretMoved();\r\n\r\n\t\tif ( notify ) _owner.NotifyTextChanged();\r\n\t}\r\n\r\n\tprivate char CharAt( TextPos p ) =\u003E p.Col \u003C Buffer.Lines[p.Line].Length ? Buffer.Lines[p.Line][p.Col] : \u0027\\0\u0027;\r\n\tprivate char CharBefore( TextPos p ) =\u003E p.Col \u003E 0 ? Buffer.Lines[p.Line][p.Col - 1] : \u0027\\0\u0027;\r\n\r\n\tprivate void TypeText( string text )\r\n\t{\r\n\t\tif ( text.Length == 1 \u0026\u0026 AutoClose \u0026\u0026 TryAutoClose( text[0] ) )\r\n\t\t\treturn;\r\n\r\n\t\t// Typing \u0022}\u0022 on a blank line drops one indent level to line up with its \u0022{\u0022\r\n\t\tif ( text == \u0022}\u0022 \u0026\u0026 !HasSelection )\r\n\t\t{\r\n\t\t\tvar before = Buffer.Lines[_caret.Line][.._caret.Col];\r\n\t\t\tif ( before.Length \u003E 0 \u0026\u0026 string.IsNullOrWhiteSpace( before ) )\r\n\t\t\t{\r\n\t\t\t\tReplace( new TextPos( _caret.Line, 0 ), _caret, Unindent( before ) \u002B \u0022}\u0022, EditKind.Typing );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tReplace( SelStart, SelEnd, text, EditKind.Typing );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Brackets and quotes: wrap a selection, type over a closer that\u0027s already there,\r\n\t/// or insert the closer along with the opener. Returns false to type normally.\r\n\t/// \u003C/summary\u003E\r\n\tprivate bool TryAutoClose( char c )\r\n\t{\r\n\t\tvar isQuote = c is \u0027\u0022\u0027 or \u0027\\\u0027\u0027;\r\n\r\n\t\tif ( HasSelection )\r\n\t\t{\r\n\t\t\tif ( !AutoClosePairs.TryGetValue( c, out var closer ) ) return false;\r\n\r\n\t\t\t// Wrap the selection and keep it selected inside the new pair\r\n\t\t\tvar start = SelStart;\r\n\t\t\tvar inner = SelectedText;\r\n\t\t\tReplace( start, SelEnd, c \u002B inner \u002B closer );\r\n\r\n\t\t\tvar innerStart = start with { Col = start.Col \u002B 1 };\r\n\t\t\tSelect( innerStart, TextBuffer.EndOf( innerStart, inner ) );\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tvar next = CharAt( _caret );\r\n\t\tvar prev = CharBefore( _caret );\r\n\r\n\t\t// Type over a closer instead of doubling it: (|)  -\u003E  ()|\r\n\t\tif ( next == c \u0026\u0026 (c is \u0027)\u0027 or \u0027]\u0027 or \u0027}\u0027 || isQuote) )\r\n\t\t{\r\n\t\t\tif ( !isQuote || IsInString( _caret ) )\r\n\t\t\t{\r\n\t\t\t\t_caret = _caret with { Col = _caret.Col \u002B 1 };\r\n\t\t\t\t_preferredColumn = null;\r\n\t\t\t\tCaretMoved();\r\n\t\t\t\treturn true;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( !AutoClosePairs.TryGetValue( c, out var close ) ) return false;\r\n\t\tif ( next != \u0027\\0\u0027 \u0026\u0026 !AutoCloseBefore.Contains( next ) ) return false;\r\n\t\tif ( IsInCommentOrString( _caret ) ) return false;\r\n\r\n\t\t// Don\u0027t pair quotes after a word (it\u0027s probably an apostrophe or closing an existing string)\r\n\t\tif ( isQuote \u0026\u0026 (char.IsLetterOrDigit( prev ) || prev == \u0027_\u0027 || prev == c) ) return false;\r\n\t\tif ( c == \u0027\\\u0027\u0027 \u0026\u0026 !_language.CSharpStrings \u0026\u0026 !_language.SingleQuoteStrings ) return false;\r\n\r\n\t\tReplace( _caret, _caret, $\u0022{c}{close}\u0022, EditKind.Typing, _caret with { Col = _caret.Col \u002B 1 } );\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The highlighter\u0027s token around a position, if any.\r\n\t/// \u003C/summary\u003E\r\n\tprivate Token? TokenAt( TextPos p )\r\n\t{\r\n\t\tUpdateLineStates();\r\n\r\n\t\tvar tokens = new List\u003CToken\u003E();\r\n\t\tvar line = Buffer.Lines[p.Line];\r\n\t\t_language.Tokenize( line, _lineStates[p.Line], tokens );\r\n\r\n\t\tforeach ( var t in tokens )\r\n\t\t{\r\n\t\t\tvar end = t.Start \u002B t.Length;\r\n\t\t\tif ( p.Col \u003E t.Start \u0026\u0026 p.Col \u003C end ) return t;\r\n\r\n\t\t\t// At the very end of a comment, or of a string that hasn\u0027t been closed yet\r\n\t\t\tif ( p.Col == end \u0026\u0026 end == line.Length \u0026\u0026 t.Kind == TokenKind.Comment ) return t;\r\n\t\t\tif ( p.Col == end \u0026\u0026 t.Kind == TokenKind.String \u0026\u0026 !(t.Length \u003E= 2 \u0026\u0026 line[end - 1] is \u0027\u0022\u0027 or \u0027\\\u0027\u0027) ) return t;\r\n\t\t}\r\n\r\n\t\treturn null;\r\n\t}\r\n\r\n\tprivate bool IsInString( TextPos p ) =\u003E TokenAt( p ) is { Kind: TokenKind.String };\r\n\tprivate bool IsInCommentOrString( TextPos p ) =\u003E TokenAt( p ) is { Kind: TokenKind.String or TokenKind.Comment };\r\n\r\n\tprivate void NewLine()\r\n\t{\r\n\t\tvar line = Buffer.Lines[SelStart.Line];\r\n\t\tvar indent = new string( line.TakeWhile( c =\u003E c == \u0027 \u0027 || c == \u0027\\t\u0027 ).ToArray() );\r\n\t\tif ( indent.Length \u003E SelStart.Col ) indent = indent[..SelStart.Col];\r\n\r\n\t\tvar before = line[..SelStart.Col].TrimEnd();\r\n\t\tvar after = Buffer.Lines[SelEnd.Line][SelEnd.Col..].TrimStart();\r\n\r\n\t\tif ( before.EndsWith( \u0027{\u0027 ) || before.EndsWith( \u0027[\u0027 ) || before.EndsWith( \u0027(\u0027 ) )\r\n\t\t{\r\n\t\t\tif ( after.StartsWith( \u0027}\u0027 ) || after.StartsWith( \u0027]\u0027 ) || after.StartsWith( \u0027)\u0027 ) )\r\n\t\t\t{\r\n\t\t\t\t// {|}  -\u003E  {\\n\\t|\\n}\r\n\t\t\t\tvar caret = new TextPos( SelStart.Line \u002B 1, indent.Length \u002B _indentUnit.Length );\r\n\t\t\t\tReplace( SelStart, SelEnd, \u0022\\n\u0022 \u002B indent \u002B _indentUnit \u002B \u0022\\n\u0022 \u002B indent, EditKind.Other, caret );\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\r\n\t\t\tif ( before.EndsWith( \u0027{\u0027 ) ) indent \u002B= _indentUnit;\r\n\t\t}\r\n\r\n\t\tReplace( SelStart, SelEnd, \u0022\\n\u0022 \u002B indent );\r\n\t}\r\n\r\n\tprivate string Unindent( string whitespace )\r\n\t{\r\n\t\tif ( whitespace.EndsWith( \u0027\\t\u0027 ) ) return whitespace[..^1];\r\n\t\tvar spaces = Math.Min( whitespace.Length - whitespace.TrimEnd( \u0027 \u0027 ).Length, TextLayout.TabSize );\r\n\t\treturn whitespace[..^spaces];\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Tab / Shift\u002BTab. Indents whole lines when the selection spans lines, otherwise inserts an indent.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void Indent( bool outdent )\r\n\t{\r\n\t\tvar multiLine = HasSelection \u0026\u0026 SelStart.Line != SelEnd.Line;\r\n\r\n\t\tif ( !multiLine \u0026\u0026 !outdent )\r\n\t\t{\r\n\t\t\tReplace( SelStart, SelEnd, _indentUnit );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar first = SelStart.Line;\r\n\t\tvar last = SelEnd.Line;\r\n\t\tif ( multiLine \u0026\u0026 SelEnd.Col == 0 ) last--;\r\n\r\n\t\tvar lines = new List\u003Cstring\u003E();\r\n\t\tfor ( int i = first; i \u003C= last; i\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar line = Buffer.Lines[i];\r\n\t\t\tif ( !outdent )\r\n\t\t\t{\r\n\t\t\t\tlines.Add( line.Length \u003E 0 ? _indentUnit \u002B line : line );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tvar ws = new string( line.TakeWhile( c =\u003E c == \u0027 \u0027 || c == \u0027\\t\u0027 ).ToArray() );\r\n\t\t\tlines.Add( ws.Length \u003E 0 ? Unindent( ws ) \u002B line[ws.Length..] : line );\r\n\t\t}\r\n\r\n\t\tvar start = new TextPos( first, 0 );\r\n\t\tvar end = new TextPos( last, Buffer.Lines[last].Length );\r\n\t\tvar text = string.Join( \u0022\\n\u0022, lines );\r\n\t\tif ( text == Buffer.GetText( start, end ) ) return;\r\n\r\n\t\tReplace( start, end, text );\r\n\r\n\t\tif ( multiLine )\r\n\t\t\tSelect( start, new TextPos( last, Buffer.Lines[last].Length ) );\r\n\t}\r\n\r\n\tprivate void Backspace( bool word )\r\n\t{\r\n\t\tif ( HasSelection ) { Replace( SelStart, SelEnd, \u0022\u0022, EditKind.Deleting ); return; }\r\n\t\tif ( _caret == default ) return;\r\n\r\n\t\t// Deleting the opener of an empty pair deletes both: (|)\r\n\t\tif ( !word \u0026\u0026 AutoClose \u0026\u0026 AutoClosePairs.TryGetValue( CharBefore( _caret ), out var closer ) \u0026\u0026 CharAt( _caret ) == closer )\r\n\t\t{\r\n\t\t\tReplace( _caret with { Col = _caret.Col - 1 }, _caret with { Col = _caret.Col \u002B 1 }, \u0022\u0022, EditKind.Deleting );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar from = word ? WordLeft( _caret ) : Left( _caret );\r\n\r\n\t\t// In leading spaces, delete back to the previous indent stop\r\n\t\tif ( !word \u0026\u0026 _indentUnit != \u0022\\t\u0022 )\r\n\t\t{\r\n\t\t\tvar before = Buffer.Lines[_caret.Line][.._caret.Col];\r\n\t\t\tif ( before.Length \u003E 0 \u0026\u0026 before.Trim().Length == 0 \u0026\u0026 !before.Contains( \u0027\\t\u0027 ) )\r\n\t\t\t\tfrom = new TextPos( _caret.Line, (before.Length - 1) / TextLayout.TabSize * TextLayout.TabSize );\r\n\t\t}\r\n\r\n\t\tReplace( from, _caret, \u0022\u0022, EditKind.Deleting );\r\n\t}\r\n\r\n\tprivate void DeleteForward( bool word )\r\n\t{\r\n\t\tif ( HasSelection ) { Replace( SelStart, SelEnd, \u0022\u0022, EditKind.Deleting ); return; }\r\n\t\tReplace( _caret, word ? WordRight( _caret ) : Right( _caret ), \u0022\u0022, EditKind.Deleting );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Trims trailing whitespace and/or adds a final newline as one undo step, leaving the caret and selection put.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void FormatForSave( bool trim, bool finalNewline )\r\n\t{\r\n\t\tvar caret = _caret;\r\n\t\tvar anchor = _anchor;\r\n\r\n\t\tBeginTransaction();\r\n\r\n\t\tif ( trim )\r\n\t\t{\r\n\t\t\tfor ( int i = Buffer.LineCount - 1; i \u003E= 0; i-- )\r\n\t\t\t{\r\n\t\t\t\tvar line = Buffer.Lines[i];\r\n\t\t\t\tvar trailing = line.Length - line.TrimEnd( \u0027 \u0027, \u0027\\t\u0027 ).Length;\r\n\t\t\t\tif ( trailing \u003E 0 ) Replace( new TextPos( i, line.Length - trailing ), new TextPos( i, line.Length ), \u0022\u0022 );\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( finalNewline \u0026\u0026 Buffer.Lines[^1].Length \u003E 0 )\r\n\t\t\tReplace( Buffer.End, Buffer.End, \u0022\\n\u0022 );\r\n\r\n\t\tEndTransaction();\r\n\r\n\t\t_caret = Buffer.Clamp( caret );\r\n\t\t_anchor = anchor.HasValue ? Buffer.Clamp( anchor.Value ) : null;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\t// --- LINE COMMANDS ---\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The lines the selection touches (a selection ending at column 0 doesn\u0027t include that line).\r\n\t/// \u003C/summary\u003E\r\n\tprivate (int First, int Last) SelectedLines()\r\n\t{\r\n\t\tvar first = SelStart.Line;\r\n\t\tvar last = SelEnd.Line;\r\n\t\tif ( HasSelection \u0026\u0026 last \u003E first \u0026\u0026 SelEnd.Col == 0 ) last--;\r\n\t\treturn (first, last);\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Ctrl\u002B/: comments the selected lines out with the language\u0027s line comment, or back in\r\n\t/// if they\u0027re all commented already. Comments go at the shallowest indent so they line up.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void ToggleComment()\r\n\t{\r\n\t\tvar prefix = _language.LineComment;\r\n\t\tif ( string.IsNullOrEmpty( prefix ) ) return;\r\n\r\n\t\tvar (first, last) = SelectedLines();\r\n\t\tvar lines = Buffer.Lines.GetRange( first, last - first \u002B 1 );\r\n\t\tvar nonBlank = lines.Where( l =\u003E l.Trim().Length \u003E 0 ).ToList();\r\n\t\tif ( nonBlank.Count == 0 ) return;\r\n\r\n\t\tvar allCommented = nonBlank.All( l =\u003E l.TrimStart().StartsWith( prefix ) );\r\n\t\tvar indent = nonBlank.Min( l =\u003E l.Length - l.TrimStart().Length );\r\n\r\n\t\tvar changed = lines.Select( l =\u003E\r\n\t\t{\r\n\t\t\tif ( l.Trim().Length == 0 ) return l;\r\n\t\t\tif ( !allCommented ) return l[..indent] \u002B prefix \u002B \u0022 \u0022 \u002B l[indent..];\r\n\r\n\t\t\tvar at = l.IndexOf( prefix, StringComparison.Ordinal );\r\n\t\t\tvar after = at \u002B prefix.Length;\r\n\t\t\tif ( after \u003C l.Length \u0026\u0026 l[after] == \u0027 \u0027 ) after\u002B\u002B;\r\n\t\t\treturn l[..at] \u002B l[after..];\r\n\t\t} );\r\n\r\n\t\tReplaceLines( first, last, changed.ToList(), first, last );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Ctrl\u002BD: copies the selected lines (or the caret\u0027s line) below themselves.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void DuplicateLines()\r\n\t{\r\n\t\tvar (first, last) = SelectedLines();\r\n\t\tvar block = Buffer.Lines.GetRange( first, last - first \u002B 1 );\r\n\t\tvar count = block.Count;\r\n\r\n\t\tReplaceLines( first, last, block.Concat( block ).ToList(), first \u002B count, last \u002B count );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Alt\u002BUp / Alt\u002BDown: moves the selected lines past the line above or below.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void MoveLines( int direction )\r\n\t{\r\n\t\tvar (first, last) = SelectedLines();\r\n\t\tif ( direction \u003C 0 \u0026\u0026 first == 0 ) return;\r\n\t\tif ( direction \u003E 0 \u0026\u0026 last \u003E= Buffer.LineCount - 1 ) return;\r\n\r\n\t\tvar block = Buffer.Lines.GetRange( first, last - first \u002B 1 );\r\n\t\tif ( direction \u003C 0 )\r\n\t\t{\r\n\t\t\tvar above = Buffer.Lines[first - 1];\r\n\t\t\tReplaceLines( first - 1, last, block.Append( above ).ToList(), first - 1, last - 1 );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar below = Buffer.Lines[last \u002B 1];\r\n\t\t\tReplaceLines( first, last \u002B 1, block.Prepend( below ).ToList(), first \u002B 1, last \u002B 1 );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Replaces lines first..last with new ones as one undo step, then selects lines\r\n\t/// selectFirst..selectLast (or puts the caret on that line when nothing was selected).\r\n\t/// \u003C/summary\u003E\r\n\tprivate void ReplaceLines( int first, int last, List\u003Cstring\u003E lines, int selectFirst, int selectLast )\r\n\t{\r\n\t\tvar hadSelection = HasSelection;\r\n\t\tvar caretColumn = _caret.Col;\r\n\t\tvar caretOffset = _caret.Line - SelectedLines().First;\r\n\r\n\t\tvar start = new TextPos( first, 0 );\r\n\t\tvar end = new TextPos( last, Buffer.Lines[last].Length );\r\n\t\tvar text = string.Join( \u0022\\n\u0022, lines );\r\n\t\tif ( text == Buffer.GetText( start, end ) ) return;\r\n\r\n\t\tReplace( start, end, text );\r\n\r\n\t\tif ( hadSelection )\r\n\t\t{\r\n\t\t\tSelect( new TextPos( selectFirst, 0 ), new TextPos( selectLast, Buffer.Lines[selectLast].Length ) );\r\n\t\t}\r\n\t\telse\r\n\t\t{\r\n\t\t\tvar line = Math.Clamp( selectFirst \u002B caretOffset, 0, Buffer.LineCount - 1 );\r\n\t\t\t_caret = Buffer.Clamp( new TextPos( line, caretColumn ) );\r\n\t\t\tCaretMoved();\r\n\t\t}\r\n\t}\r\n\r\n\tpublic void Copy()\r\n\t{\r\n\t\tif ( HasSelection ) EditorUtility.Clipboard.Copy( SelectedText.Replace( \u0022\\n\u0022, Buffer.LineEnding ) );\r\n\t}\r\n\r\n\tpublic void Cut()\r\n\t{\r\n\t\tif ( !HasSelection ) return;\r\n\t\tCopy();\r\n\t\tReplace( SelStart, SelEnd, \u0022\u0022 );\r\n\t}\r\n\r\n\tpublic void Paste()\r\n\t{\r\n\t\tvar text = EditorUtility.Clipboard.Paste();\r\n\t\tif ( !string.IsNullOrEmpty( text ) ) Replace( SelStart, SelEnd, text );\r\n\t}\r\n\r\n\t// --- CARET MOVEMENT ---\r\n\r\n\tprivate TextPos Left( TextPos p ) =\u003E p.Col \u003E 0 ? p with { Col = p.Col - 1 } : p.Line \u003E 0 ? new TextPos( p.Line - 1, Buffer.Lines[p.Line - 1].Length ) : p;\r\n\tprivate TextPos Right( TextPos p ) =\u003E p.Col \u003C Buffer.Lines[p.Line].Length ? p with { Col = p.Col \u002B 1 } : p.Line \u003C Buffer.LineCount - 1 ? new TextPos( p.Line \u002B 1, 0 ) : p;\r\n\r\n\tprivate static int CharClass( char c ) =\u003E char.IsWhiteSpace( c ) ? 0 : char.IsLetterOrDigit( c ) || c == \u0027_\u0027 ? 1 : 2;\r\n\r\n\tprivate TextPos WordLeft( TextPos p )\r\n\t{\r\n\t\tif ( p.Col == 0 ) return Left( p );\r\n\r\n\t\tvar line = Buffer.Lines[p.Line];\r\n\t\tvar i = p.Col;\r\n\t\twhile ( i \u003E 0 \u0026\u0026 CharClass( line[i - 1] ) == 0 ) i--;\r\n\t\tif ( i \u003E 0 )\r\n\t\t{\r\n\t\t\tvar cls = CharClass( line[i - 1] );\r\n\t\t\twhile ( i \u003E 0 \u0026\u0026 CharClass( line[i - 1] ) == cls ) i--;\r\n\t\t}\r\n\r\n\t\treturn p with { Col = i };\r\n\t}\r\n\r\n\tprivate TextPos WordRight( TextPos p )\r\n\t{\r\n\t\tvar line = Buffer.Lines[p.Line];\r\n\t\tif ( p.Col \u003E= line.Length ) return Right( p );\r\n\r\n\t\tvar i = p.Col;\r\n\t\tvar cls = CharClass( line[i] );\r\n\t\twhile ( i \u003C line.Length \u0026\u0026 CharClass( line[i] ) == cls ) i\u002B\u002B;\r\n\t\twhile ( i \u003C line.Length \u0026\u0026 CharClass( line[i] ) == 0 ) i\u002B\u002B;\r\n\r\n\t\treturn p with { Col = i };\r\n\t}\r\n\r\n\tprivate (TextPos Start, TextPos End) WordAt( TextPos p )\r\n\t{\r\n\t\tvar line = Buffer.Lines[p.Line];\r\n\t\tif ( line.Length == 0 ) return (p, p);\r\n\r\n\t\tvar i = Math.Min( p.Col, line.Length - 1 );\r\n\t\tvar cls = CharClass( line[i] );\r\n\t\tvar start = i;\r\n\t\tvar end = i;\r\n\t\twhile ( start \u003E 0 \u0026\u0026 CharClass( line[start - 1] ) == cls ) start--;\r\n\t\twhile ( end \u003C line.Length \u0026\u0026 CharClass( line[end] ) == cls ) end\u002B\u002B;\r\n\r\n\t\treturn (p with { Col = start }, p with { Col = end });\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Home toggles between the first non-whitespace character and column 0.\r\n\t/// \u003C/summary\u003E\r\n\tprivate TextPos SmartHome( TextPos p )\r\n\t{\r\n\t\tvar line = Buffer.Lines[p.Line];\r\n\t\tvar firstText = line.Length - line.TrimStart().Length;\r\n\t\treturn p with { Col = p.Col == firstText ? 0 : firstText };\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Up/down by visual rows, keeping the column the caret started in.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void MoveVertical( int rows, bool extend )\r\n\t{\r\n\t\t_preferredColumn ??= ColumnInRow( _caret );\r\n\r\n\t\tvar current = GlobalRow( _caret );\r\n\t\tvar target = current \u002B rows;\r\n\r\n\t\tTextPos pos;\r\n\t\tif ( target \u003C 0 ) pos = new TextPos( 0, 0 );\r\n\t\telse if ( target \u003E= TotalRows ) pos = Buffer.End;\r\n\t\telse pos = PosInRow( target, _preferredColumn.Value );\r\n\r\n\t\tMoveCaret( pos, extend, keepPreferredColumn: true );\r\n\t}\r\n\r\n\t// --- INPUT ---\r\n\r\n\tprotected override void OnKeyPress( KeyEvent e )\r\n\t{\r\n\t\tif ( CompletionOpen \u0026\u0026 HandleCompletionKey( e ) )\r\n\t\t{\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( SignatureOpen \u0026\u0026 HandleSignatureKey( e ) )\r\n\t\t{\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar before = (_caret, Buffer.LineCount, Buffer.Lines[_caret.Line].Length);\r\n\t\tHandleKey( e );\r\n\r\n\t\t// Typing, deleting or moving the caret keeps the popup\u0027s list in step with the word\r\n\t\tif ( CompletionOpen ) RefilterCompletions();\r\n\r\n\t\t// ...and the hints in step with the argument (or gone, once the caret leaves the brackets)\r\n\t\tif ( SignatureOpen \u0026\u0026 before != (_caret, Buffer.LineCount, Buffer.Lines[_caret.Line].Length) )\r\n\t\t\t_owner.RequestSignatureHelp( _caret );\r\n\t}\r\n\r\n\tprivate void HandleKey( KeyEvent e )\r\n\t{\r\n\t\tvar ctrl = e.HasCtrl;\r\n\t\tvar shift = e.HasShift;\r\n\t\tvar pageRows = Math.Max( 1, (int)(ViewportSize.y / _lineHeight) - 1 );\r\n\r\n\t\tswitch ( e.Key )\r\n\t\t{\r\n\t\t\tcase KeyCode.Space when ctrl \u0026\u0026 shift: _owner.RequestSignatureHelp( _caret ); break;\r\n\t\t\tcase KeyCode.Space when ctrl: _owner.RequestCompletion( _caret ); break;\r\n\t\t\tcase KeyCode.Up when e.HasAlt \u0026\u0026 !ctrl: MoveLines( -1 ); break;\r\n\t\t\tcase KeyCode.Down when e.HasAlt \u0026\u0026 !ctrl: MoveLines( 1 ); break;\r\n\t\t\tcase KeyCode.Slash when ctrl: ToggleComment(); break;\r\n\t\t\tcase KeyCode.D when ctrl \u0026\u0026 !shift: DuplicateLines(); break;\r\n\t\t\tcase KeyCode.Equal or KeyCode.Plus when ctrl: CodeView.FontSize \u002B= 1; break;\r\n\t\t\tcase KeyCode.Minus when ctrl: CodeView.FontSize -= 1; break;\r\n\t\t\tcase KeyCode.Left when e.HasAlt \u0026\u0026 !ctrl: _owner.RequestNavigate( back: true ); break;\r\n\t\t\tcase KeyCode.Right when e.HasAlt \u0026\u0026 !ctrl: _owner.RequestNavigate( back: false ); break;\r\n\t\t\tcase KeyCode.F12: _owner.RequestDefinition( _caret ); break;\r\n\t\t\tcase KeyCode.Left: MoveCaret( !shift \u0026\u0026 HasSelection \u0026\u0026 !ctrl ? SelStart : ctrl ? WordLeft( _caret ) : Left( _caret ), shift ); break;\r\n\t\t\tcase KeyCode.Right: MoveCaret( !shift \u0026\u0026 HasSelection \u0026\u0026 !ctrl ? SelEnd : ctrl ? WordRight( _caret ) : Right( _caret ), shift ); break;\r\n\t\t\tcase KeyCode.Up: MoveVertical( -1, shift ); break;\r\n\t\t\tcase KeyCode.Down: MoveVertical( 1, shift ); break;\r\n\t\t\tcase KeyCode.PageUp: MoveVertical( -pageRows, shift ); break;\r\n\t\t\tcase KeyCode.PageDown: MoveVertical( pageRows, shift ); break;\r\n\t\t\tcase KeyCode.Home: MoveCaret( ctrl ? new TextPos( 0, 0 ) : SmartHome( _caret ), shift ); break;\r\n\t\t\tcase KeyCode.End: MoveCaret( ctrl ? Buffer.End : _caret with { Col = Buffer.Lines[_caret.Line].Length }, shift ); break;\r\n\t\t\tcase KeyCode.Backspace: Backspace( ctrl ); break;\r\n\t\t\tcase KeyCode.Delete: DeleteForward( ctrl ); break;\r\n\t\t\tcase KeyCode.Return:\r\n\t\t\tcase KeyCode.Enter: NewLine(); break;\r\n\t\t\tcase KeyCode.Tab: Indent( outdent: shift ); break;\r\n\t\t\tcase KeyCode.Backtab: Indent( outdent: true ); break;\r\n\t\t\tcase KeyCode.Escape when _owner.IsFindOpen: _owner.CloseFind(); break;\r\n\t\t\tcase KeyCode.Escape when HasSelection: _anchor = null; Update(); break;\r\n\t\t\tcase KeyCode.A when ctrl: SelectAll(); break;\r\n\t\t\tcase KeyCode.C when ctrl: Copy(); break;\r\n\t\t\tcase KeyCode.X when ctrl: Cut(); break;\r\n\t\t\tcase KeyCode.V when ctrl: Paste(); break;\r\n\t\t\tcase KeyCode.Z when ctrl \u0026\u0026 shift: Redo(); break;\r\n\t\t\tcase KeyCode.Z when ctrl: Undo(); break;\r\n\t\t\tcase KeyCode.Y when ctrl: Redo(); break;\r\n\t\t\tcase KeyCode.F when ctrl: _owner.OpenFind( replace: false ); break;\r\n\t\t\tcase KeyCode.H when ctrl: _owner.OpenFind( replace: true ); break;\r\n\t\t\tcase KeyCode.F3: _owner.FindNext( backwards: shift ); break;\r\n\t\t\tcase KeyCode.Z when e.HasAlt: CodeView.WordWrap = !CodeView.WordWrap; break;\r\n\r\n\t\t\tdefault:\r\n\t\t\t\t// Ctrl\u002BAlt is AltGr on Windows keyboards, which types characters like { and }\r\n\t\t\t\tvar text = e.Text;\r\n\t\t\t\tvar typesCharacter = !string.IsNullOrEmpty( text ) \u0026\u0026 !char.IsControl( text[0] ) \u0026\u0026 (!ctrl || e.HasAlt);\r\n\t\t\t\tif ( !typesCharacter )\r\n\t\t\t\t{\r\n\t\t\t\t\tbase.OnKeyPress( e );\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tTypeText( text );\r\n\t\t\t\tMaybeTriggerCompletion( text );\r\n\t\t\t\tif ( text is \u0022(\u0022 or \u0022,\u0022 \u0026\u0026 !SignatureOpen \u0026\u0026 HotCodeSettings.ParameterHints ) _owner.RequestSignatureHelp( _caret );\r\n\t\t\t\tbreak;\r\n\t\t}\r\n\r\n\t\te.Accepted = true;\r\n\t}\r\n\r\n\t// --- PARAMETER HINTS ---\r\n\r\n\tprivate bool SignatureOpen =\u003E _signaturePopup is not null \u0026\u0026 _signaturePopup.Visible;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Hints arrived for the caret position; null means the caret isn\u0027t in a call any more.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void ShowSignatureHelp( SignatureHelpResult result )\r\n\t{\r\n\t\tif ( result is null || result.Signatures.Count == 0 )\r\n\t\t{\r\n\t\t\tCloseSignatureHelp();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t// Same call as before: keep the overload the user picked with Up/Down\r\n\t\tvar sameCall = _signature is not null \u0026\u0026 _signature.OpenParenLine == result.OpenParenLine\r\n\t\t\t\u0026\u0026 _signature.OpenParenColumn == result.OpenParenColumn \u0026\u0026 _signature.Signatures.Count == result.Signatures.Count;\r\n\t\tif ( !sameCall ) _signaturePicked = false;\r\n\r\n\t\tvar index = _signaturePicked ? _signaturePopup.Index : result.ActiveSignature;\r\n\t\t_signature = result;\r\n\r\n\t\t_signaturePopup ??= new SignaturePopup( this );\r\n\t\t_signaturePopup.SetResult( result, index );\r\n\t\tPositionSignature();\r\n\t\t_signaturePopup.Visible = true;\r\n\t\t_signaturePopup.Raise();\r\n\t}\r\n\r\n\tpublic void CloseSignatureHelp()\r\n\t{\r\n\t\tif ( _signaturePopup is not null ) _signaturePopup.Visible = false;\r\n\t\t_signature = null;\r\n\t\t_signaturePicked = false;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Above the caret\u0027s row so it doesn\u0027t cover what\u0027s being typed (or below if there\u0027s no room).\r\n\t/// \u003C/summary\u003E\r\n\tprivate void PositionSignature()\r\n\t{\r\n\t\tvar anchor = CaretRect( new TextPos( _signature.OpenParenLine, _signature.OpenParenColumn ) );\r\n\t\tvar row = CaretRect( _caret );\r\n\t\tvar scroll = Scroll;\r\n\t\tvar viewport = ViewportSize;\r\n\r\n\t\tvar x = MathF.Max( scroll.x, MathF.Min( anchor.Left - _charWidth * 4, scroll.x \u002B viewport.x - _signaturePopup.Width ) );\r\n\t\tvar y = row.Top - _signaturePopup.Height - 2;\r\n\t\tif ( y \u003C scroll.y ) y = row.Bottom \u002B 2 \u002B (CompletionOpen ? _popup.Height \u002B 2 : 0);\r\n\r\n\t\t_signaturePopup.Position = new Vector2( x, y );\r\n\t}\r\n\r\n\tprivate bool HandleSignatureKey( KeyEvent e )\r\n\t{\r\n\t\tswitch ( e.Key )\r\n\t\t{\r\n\t\t\tcase KeyCode.Up when _signaturePopup.Count \u003E 1 \u0026\u0026 !e.HasShift \u0026\u0026 !e.HasCtrl \u0026\u0026 !e.HasAlt:\r\n\t\t\t\t_signaturePopup.Cycle( -1 );\r\n\t\t\t\t_signaturePicked = true;\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase KeyCode.Down when _signaturePopup.Count \u003E 1 \u0026\u0026 !e.HasShift \u0026\u0026 !e.HasCtrl \u0026\u0026 !e.HasAlt:\r\n\t\t\t\t_signaturePopup.Cycle( 1 );\r\n\t\t\t\t_signaturePicked = true;\r\n\t\t\t\treturn true;\r\n\r\n\t\t\tcase KeyCode.Escape:\r\n\t\t\t\tCloseSignatureHelp();\r\n\t\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\t// --- AUTOCOMPLETE ---\r\n\r\n\tprivate bool CompletionOpen =\u003E _popup is not null \u0026\u0026 _popup.Visible;\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Asks for suggestions after \u0022.\u0022 or when a new word starts. While the popup is open,\r\n\t/// further typing just filters what\u0027s already there.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void MaybeTriggerCompletion( string typed )\r\n\t{\r\n\t\tif ( CompletionOpen || typed.Length != 1 || !HotCodeSettings.SuggestWhileTyping ) return;\r\n\r\n\t\tvar c = typed[0];\r\n\t\tvar wordStarts = (char.IsLetter( c ) || c == \u0027_\u0027) \u0026\u0026 CharClass( CharBefore( _caret with { Col = _caret.Col - 1 } ) ) != 1;\r\n\r\n\t\tif ( c == \u0027.\u0027 || wordStarts )\r\n\t\t\t_owner.RequestCompletion( _caret );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Suggestions arrived. They\u0027re dropped if the caret has since left the word they\u0027re for.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void ShowCompletions( CompletionResult result )\r\n\t{\r\n\t\tif ( result.Line != _caret.Line || _caret.Col \u003C result.StartColumn || result.Items.Count == 0 )\r\n\t\t\treturn;\r\n\r\n\t\t_completionItems = result.Items;\r\n\t\t_completionLine = result.Line;\r\n\t\t_completionStart = result.StartColumn;\r\n\t\tRefilterCompletions();\r\n\t}\r\n\r\n\tprivate void RefilterCompletions()\r\n\t{\r\n\t\tvar line = Buffer.Lines[_caret.Line];\r\n\t\tif ( _caret.Line != _completionLine || _caret.Col \u003C _completionStart || _completionStart \u003E line.Length )\r\n\t\t{\r\n\t\t\tCloseCompletions();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar typed = line[_completionStart.._caret.Col];\r\n\t\tif ( typed.Any( ch =\u003E CharClass( ch ) != 1 ) )\r\n\t\t{\r\n\t\t\tCloseCompletions();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tvar matches = CompletionMatcher.Filter( _completionItems, typed );\r\n\r\n\t\t// Nothing left, or the only match is exactly what\u0027s typed already\r\n\t\tif ( matches.Count == 0 || (matches.Count == 1 \u0026\u0026 matches[0].Name == typed) )\r\n\t\t{\r\n\t\t\tCloseCompletions();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif ( _popup is null )\r\n\t\t{\r\n\t\t\t_popup = new CompletionPopup( this );\r\n\t\t\t_popup.Committed = CommitCompletion;\r\n\t\t}\r\n\r\n\t\t_popup.SetItems( matches.Count \u003E 500 ? matches.GetRange( 0, 500 ) : matches, _lineHeight );\r\n\t\tPositionPopup();\r\n\t\t_popup.Visible = true;\r\n\t\t_popup.Raise();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Under the word, or above it when there isn\u0027t room below.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void PositionPopup()\r\n\t{\r\n\t\tvar anchor = CaretRect( new TextPos( _completionLine, _completionStart ) );\r\n\t\tvar scroll = Scroll;\r\n\t\tvar viewport = ViewportSize;\r\n\r\n\t\tvar x = MathF.Min( anchor.Left, scroll.x \u002B viewport.x - _popup.Width );\r\n\t\tvar y = anchor.Bottom \u002B 2;\r\n\t\tif ( y \u002B _popup.Height \u003E scroll.y \u002B viewport.y \u0026\u0026 anchor.Top - _popup.Height - 2 \u003E= scroll.y )\r\n\t\t\ty = anchor.Top - _popup.Height - 2;\r\n\r\n\t\t_popup.Position = new Vector2( MathF.Max( scroll.x, x ), y );\r\n\t}\r\n\r\n\tpublic void CloseCompletions()\r\n\t{\r\n\t\tif ( _popup is not null ) _popup.Visible = false;\r\n\t\t_completionLine = -1;\r\n\t}\r\n\r\n\tprivate void CommitCompletion( CompletionItem item )\r\n\t{\r\n\t\tif ( item is null ) return;\r\n\r\n\t\tvar start = new TextPos( _completionLine, _completionStart );\r\n\t\tCloseCompletions();\r\n\t\tReplace( start, _caret, item.Name );\r\n\t\tFocus( false );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Keys the popup takes while it\u0027s open. Returns false to let the editor handle the key as usual.\r\n\t/// \u003C/summary\u003E\r\n\tprivate bool HandleCompletionKey( KeyEvent e )\r\n\t{\r\n\t\tswitch ( e.Key )\r\n\t\t{\r\n\t\t\tcase KeyCode.Up: _popup.Move( -1 ); return true;\r\n\t\t\tcase KeyCode.Down: _popup.Move( 1 ); return true;\r\n\t\t\tcase KeyCode.PageUp: _popup.Move( -CompletionPopup.PageSize ); return true;\r\n\t\t\tcase KeyCode.PageDown: _popup.Move( CompletionPopup.PageSize ); return true;\r\n\t\t\tcase KeyCode.Return or KeyCode.Enter or KeyCode.Tab when !e.HasShift \u0026\u0026 !e.HasCtrl:\r\n\t\t\t\tCommitCompletion( _popup.Selected );\r\n\t\t\t\treturn true;\r\n\t\t\tcase KeyCode.Escape:\r\n\t\t\t\tCloseCompletions();\r\n\t\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\treturn false;\r\n\t}\r\n\r\n\tprotected override void OnMousePress( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMousePress( e );\r\n\t\tCloseCompletions();\r\n\t\tCloseSignatureHelp();\r\n\t\tif ( !e.LeftMouseButton ) return;\r\n\r\n\t\t// Ctrl\u002BClick: go to definition\r\n\t\tif ( e.HasCtrl \u0026\u0026 WordUnder( e.LocalPosition ) is { } word )\r\n\t\t{\r\n\t\t\tFocus( false );\r\n\t\t\tMoveCaret( word.Start, extendSelection: false );\r\n\t\t\tSetLink( null );\r\n\t\t\t_owner.RequestDefinition( word.Start );\r\n\t\t\te.Accepted = true;\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tFocus( false );\r\n\t\t_dragging = true;\r\n\t\tMoveCaret( PosAt( e.LocalPosition ), e.HasShift );\r\n\t\t_anchor ??= _caret;\r\n\t\te.Accepted = true;\r\n\t}\r\n\r\n\tprotected override void OnMouseMove( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseMove( e );\r\n\r\n\t\tif ( !_dragging )\r\n\t\t{\r\n\t\t\tUpdateMarkerTooltip( e.LocalPosition );\r\n\t\t\tSetLink( e.HasCtrl ? WordUnder( e.LocalPosition ) : null );\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\t_anchor ??= _caret;\r\n\t\t_caret = PosAt( e.LocalPosition );\r\n\t\tCaretMoved();\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The identifier under a point, if the point is over text.\r\n\t/// \u003C/summary\u003E\r\n\tprivate (TextPos Start, TextPos End)? WordUnder( Vector2 local )\r\n\t{\r\n\t\tvar row = (int)MathF.Floor( local.y / _lineHeight );\r\n\t\tif ( row \u003C 0 || row \u003E= TotalRows || local.x \u003C TextLeft ) return null;\r\n\r\n\t\tvar pos = PosAt( local );\r\n\t\tvar line = Buffer.Lines[pos.Line];\r\n\t\tvar col = Math.Min( pos.Col, line.Length - 1 );\r\n\t\tif ( col \u003C 0 || CharClass( line[col] ) != 1 ) return null;\r\n\r\n\t\tvar (start, end) = WordAt( pos with { Col = col } );\r\n\t\treturn end \u003E start ? (start, end) : null;\r\n\t}\r\n\r\n\tprivate void SetLink( (TextPos Start, TextPos End)? link )\r\n\t{\r\n\t\tif ( _link == link ) return;\r\n\t\t_link = link;\r\n\t\tCursor = link is null ? CursorShape.IBeam : CursorShape.Finger;\r\n\t\tUpdate();\r\n\t}\r\n\r\n\tprotected override void OnMouseWheel( WheelEvent e )\r\n\t{\r\n\t\tif ( e.HasCtrl )\r\n\t\t{\r\n\t\t\tCodeView.FontSize \u002B= e.Delta \u003E 0 ? 1 : -1;\r\n\t\t\te.Accept();\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tbase.OnMouseWheel( e );\r\n\t}\r\n\r\n\tprotected override void OnKeyRelease( KeyEvent e )\r\n\t{\r\n\t\tbase.OnKeyRelease( e );\r\n\t\tif ( e.Key == KeyCode.Control ) SetLink( null );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Hovering an underlined error shows its message.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void UpdateMarkerTooltip( Vector2 local )\r\n\t{\r\n\t\tstring tip = null;\r\n\r\n\t\tif ( _markers.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar row = (int)MathF.Floor( local.y / _lineHeight );\r\n\t\t\tif ( row \u003E= 0 \u0026\u0026 row \u003C TotalRows )\r\n\t\t\t{\r\n\t\t\t\tvar pos = PosAt( local );\r\n\t\t\t\tvar marker = _markers.FirstOrDefault( m =\u003E pos \u003E= m.Start \u0026\u0026 pos \u003C= m.End );\r\n\t\t\t\tif ( marker is not null ) tip = marker.Message;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( ToolTip != tip ) ToolTip = tip;\r\n\t}\r\n\r\n\tprotected override void OnMouseReleased( MouseEvent e )\r\n\t{\r\n\t\tbase.OnMouseReleased( e );\r\n\t\t_dragging = false;\r\n\t\tif ( _anchor == _caret ) _anchor = null;\r\n\t}\r\n\r\n\tprotected override void OnDoubleClick( MouseEvent e )\r\n\t{\r\n\t\tbase.OnDoubleClick( e );\r\n\t\tif ( !e.LeftMouseButton ) return;\r\n\r\n\t\tvar (start, end) = WordAt( PosAt( e.LocalPosition ) );\r\n\t\t_dragging = false;\r\n\t\tSelect( start, end );\r\n\t}\r\n\r\n\tprotected override void OnContextMenu( ContextMenuEvent e )\r\n\t{\r\n\t\tvar menu = new ContextMenu( this );\r\n\t\tvar clicked = PosAt( FromScreen( Editor.Application.CursorPosition ) );\r\n\t\tmenu.AddOption( \u0022Go to Definition (F12)\u0022, \u0022start\u0022, () =\u003E _owner.RequestDefinition( HasSelection ? SelStart : clicked ) );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Cut\u0022, \u0022content_cut\u0022, Cut );\r\n\t\tmenu.AddOption( \u0022Copy\u0022, \u0022content_copy\u0022, Copy );\r\n\t\tmenu.AddOption( \u0022Paste\u0022, \u0022content_paste\u0022, Paste );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Undo\u0022, \u0022undo\u0022, Undo );\r\n\t\tmenu.AddOption( \u0022Redo\u0022, \u0022redo\u0022, Redo );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( \u0022Find...\u0022, \u0022search\u0022, () =\u003E _owner.OpenFind( replace: false ) );\r\n\t\tmenu.AddOption( \u0022Replace...\u0022, \u0022find_replace\u0022, () =\u003E _owner.OpenFind( replace: true ) );\r\n\t\tmenu.AddOption( \u0022Select All\u0022, \u0022select_all\u0022, SelectAll );\r\n\t\tmenu.AddSeparator();\r\n\t\tmenu.AddOption( WordWrap ? \u0022Turn Word Wrap Off\u0022 : \u0022Turn Word Wrap On\u0022, \u0022wrap_text\u0022, () =\u003E CodeView.WordWrap = !CodeView.WordWrap );\r\n\t\tmenu.OpenAtCursor();\r\n\t\te.Accepted = true;\r\n\t}\r\n\r\n\tprotected override void OnFocus( FocusChangeReason reason )\r\n\t{\r\n\t\tbase.OnFocus( reason );\r\n\t\tCaretMoved();\r\n\t}\r\n\r\n\tprotected override void OnBlur( FocusChangeReason reason )\r\n\t{\r\n\t\tbase.OnBlur( reason );\r\n\t\tCloseCompletions();\r\n\t\tCloseSignatureHelp();\r\n\t\tUpdate();\r\n\t}\r\n\r\n\t[EditorEvent.Frame]\r\n\tpublic void Frame()\r\n\t{\r\n\t\tif ( !Visible ) return;\r\n\r\n\t\tif ( _settingsVersion != HotCodeSettings.Version )\r\n\t\t{\r\n\t\t\t_settingsVersion = HotCodeSettings.Version;\r\n\t\t\t_measured = false;\r\n\t\t\tInvalidateLayout();\r\n\t\t}\r\n\r\n\t\tif ( _wrapApplied != WordWrap ) InvalidateLayout();\r\n\t\tif ( _layoutDirty \u0026\u0026 _measured ) UpdateLayout();\r\n\r\n\t\tif ( _ensureCaretVisible \u0026\u0026 _measured )\r\n\t\t{\r\n\t\t\t_ensureCaretVisible = false;\r\n\t\t\tScrollToCaret();\r\n\t\t}\r\n\r\n\t\tif ( CompletionOpen ) PositionPopup();\r\n\t\tif ( SignatureOpen ) PositionSignature();\r\n\r\n\t\t// Repaint on scroll (the gutter is pinned to the left edge) and when the caret blinks\r\n\t\tvar scroll = Scroll;\r\n\t\tvar caretOn = CaretVisible;\r\n\t\tif ( scroll != _lastScroll || caretOn != _lastCaretOn )\r\n\t\t{\r\n\t\t\t_lastScroll = scroll;\r\n\t\t\t_lastCaretOn = caretOn;\r\n\t\t\tUpdate();\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Solid right after moving, then blinks.\r\n\t/// \u003C/summary\u003E\r\n\tprivate bool CaretVisible =\u003E IsFocused \u0026\u0026 (!HotCodeSettings.CursorBlink || ((RealTime.Now - _caretBlinkStart) % 1.0) \u003C 0.55);\r\n\r\n\tpublic void ScrollToCaret()\r\n\t{\r\n\t\tUpdateLayout();\r\n\r\n\t\tvar rect = CaretRect( _caret );\r\n\t\tvar scroll = Scroll;\r\n\t\tvar viewport = ViewportSize;\r\n\r\n\t\tif ( rect.Top \u003C scroll.y ) _scroller.VerticalScrollbar.Value = (int)rect.Top;\r\n\t\telse if ( rect.Bottom \u003E scroll.y \u002B viewport.y ) _scroller.VerticalScrollbar.Value = (int)(rect.Bottom - viewport.y);\r\n\r\n\t\tif ( _wrapApplied ) return;\r\n\r\n\t\tif ( rect.Left \u003C scroll.x \u002B TextLeft ) _scroller.HorizontalScrollbar.Value = (int)MathF.Max( 0, rect.Left - TextLeft - _charWidth * 4 );\r\n\t\telse if ( rect.Right \u003E scroll.x \u002B viewport.x ) _scroller.HorizontalScrollbar.Value = (int)(rect.Right - viewport.x \u002B _charWidth * 4);\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Scrolls so the caret sits about a third of the way down, for jumping to search results.\r\n\t/// \u003C/summary\u003E\r\n\tpublic void CenterOnCaret()\r\n\t{\r\n\t\tUpdateLayout();\r\n\t\tvar rect = CaretRect( _caret );\r\n\t\tvar viewport = ViewportSize;\r\n\t\tif ( rect.Top \u003C Scroll.y || rect.Bottom \u003E Scroll.y \u002B viewport.y )\r\n\t\t\t_scroller.VerticalScrollbar.Value = (int)MathF.Max( 0, rect.Top - viewport.y / 3 );\r\n\t\t_ensureCaretVisible = true;\r\n\t}\r\n\r\n\t// --- PAINTING ---\r\n\r\n\tprivate void Measure()\r\n\t{\r\n\t\tPaint.SetFont( HotCodeSettings.CodeFont, FontSize );\r\n\t\tvar size = Paint.MeasureText( new string( \u0027M\u0027, 100 ) );\r\n\t\tvar charWidth = size.x / 100f;\r\n\t\tvar lineHeight = MathF.Ceiling( size.y * HotCodeSettings.LineSpacing );\r\n\r\n\t\tif ( charWidth \u003E 0 \u0026\u0026 lineHeight \u003E 0 \u0026\u0026 (!_measured || charWidth != _charWidth || lineHeight != _lineHeight) )\r\n\t\t{\r\n\t\t\t_charWidth = charWidth;\r\n\t\t\t_lineHeight = lineHeight;\r\n\t\t\t_measured = true;\r\n\t\t\t_layoutDirty = true;\r\n\t\t}\r\n\t}\r\n\r\n\tprivate void UpdateLineStates()\r\n\t{\r\n\t\tif ( !_statesDirty ) return;\r\n\t\t_statesDirty = false;\r\n\r\n\t\t_lineStates.Clear();\r\n\t\tvar state = LexState.Normal;\r\n\t\tvar scratch = new List\u003CToken\u003E();\r\n\t\tforeach ( var line in Buffer.Lines )\r\n\t\t{\r\n\t\t\t_lineStates.Add( state );\r\n\t\t\tscratch.Clear();\r\n\t\t\tstate = _language.Tokenize( line, state, scratch );\r\n\t\t}\r\n\t}\r\n\r\n\tprotected override void OnPaint()\r\n\t{\r\n\t\tMeasure();\r\n\t\tUpdateLayout();\r\n\t\tUpdateLineStates();\r\n\r\n\t\tvar scroll = Scroll;\r\n\t\tvar viewport = _scroller.Size;\r\n\t\tvar firstRow = Math.Max( 0, (int)(scroll.y / _lineHeight) );\r\n\t\tvar lastRow = Math.Min( TotalRows - 1, (int)((scroll.y \u002B viewport.y) / _lineHeight) \u002B 1 );\r\n\t\tvar firstLine = LineOfRow( firstRow );\r\n\t\tvar lastLine = LineOfRow( lastRow );\r\n\r\n\t\t// Background\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( SyntaxColors.Background );\r\n\t\tPaint.DrawRect( new Rect( scroll, viewport ) );\r\n\r\n\t\t// Current row\r\n\t\tif ( !HasSelection \u0026\u0026 HotCodeSettings.HighlightCurrentLine )\r\n\t\t{\r\n\t\t\tPaint.SetBrush( SyntaxColors.CurrentLine );\r\n\t\t\tPaint.DrawRect( new Rect( scroll.x, GlobalRow( _caret ) * _lineHeight, viewport.x, _lineHeight ) );\r\n\t\t}\r\n\r\n\t\tif ( HasSelection )\r\n\t\t\tDrawRange( SelStart, SelEnd, SyntaxColors.Selection, firstLine, lastLine );\r\n\r\n\t\t// Find matches: binary search to the first visible one, then walk until past the view\r\n\t\tif ( _matches.Count \u003E 0 )\r\n\t\t{\r\n\t\t\tvar i = FirstMatchFrom( firstLine );\r\n\t\t\tfor ( ; i \u003C _matches.Count \u0026\u0026 _matches[i].Start.Line \u003C= lastLine; i\u002B\u002B )\r\n\t\t\t\tDrawRange( _matches[i].Start, _matches[i].End, i == _currentMatch ? SyntaxColors.FindCurrent : SyntaxColors.FindMatch, firstLine, lastLine );\r\n\t\t}\r\n\r\n\t\t// Text\r\n\t\tPaint.SetFont( HotCodeSettings.CodeFont, FontSize );\r\n\t\tvar tokens = new List\u003CToken\u003E();\r\n\t\tvar tokensLine = -1;\r\n\t\tfor ( int row = firstRow; row \u003C= lastRow; row\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar line = LineOfRow( row );\r\n\t\t\tif ( line != tokensLine )\r\n\t\t\t{\r\n\t\t\t\ttokens.Clear();\r\n\t\t\t\t_language.Tokenize( Buffer.Lines[line], _lineStates[line], tokens );\r\n\t\t\t\ttokensLine = line;\r\n\t\t\t}\r\n\r\n\t\t\tvar r = row - _lineFirstRow[line];\r\n\t\t\tDrawRow( row, line, _rowStarts[line][r], RowEnd( line, r ), tokens );\r\n\t\t}\r\n\r\n\t\tforeach ( var marker in HotCodeSettings.UnderlineErrors ? _markers : Array.Empty\u003CEditorMarker\u003E() )\r\n\t\t{\r\n\t\t\tif ( marker.End.Line \u003C firstLine || marker.Start.Line \u003E lastLine ) continue;\r\n\t\t\tDrawSquiggle( marker, firstLine, lastLine );\r\n\t\t}\r\n\r\n\t\tif ( _link is { } link )\r\n\t\t{\r\n\t\t\tvar text = Buffer.Lines[link.Start.Line];\r\n\t\t\tvar r = RowInLine( link.Start );\r\n\t\t\tvar rowStart = _rowStarts[link.Start.Line][r];\r\n\t\t\tvar x0 = TextLeft \u002B TextLayout.ColumnOf( text, rowStart, link.Start.Col ) * _charWidth;\r\n\t\t\tvar x1 = TextLeft \u002B TextLayout.ColumnOf( text, rowStart, link.End.Col ) * _charWidth;\r\n\t\t\tvar y = (GlobalRow( link.Start ) \u002B 1) * _lineHeight - 3;\r\n\t\t\tPaint.SetPen( SyntaxColors.Link, 1 );\r\n\t\t\tPaint.DrawLine( new Vector2( x0, y ), new Vector2( x1, y ) );\r\n\t\t}\r\n\r\n\t\tif ( CaretVisible )\r\n\t\t{\r\n\t\t\tPaint.ClearPen();\r\n\t\t\tPaint.SetBrush( SyntaxColors.Caret );\r\n\t\t\tPaint.DrawRect( CaretRect( _caret ) );\r\n\t\t}\r\n\r\n\t\t// Gutter, drawn last and pinned to the left edge so it stays put when scrolling sideways\r\n\t\tif ( !HotCodeSettings.ShowLineNumbers ) return;\r\n\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( SyntaxColors.Gutter );\r\n\t\tPaint.DrawRect( new Rect( scroll.x, scroll.y, GutterWidth, viewport.y ) );\r\n\r\n\t\tfor ( int line = firstLine; line \u003C= lastLine; line\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar row = _lineFirstRow[line];\r\n\t\t\tif ( row \u003C firstRow ) continue;\r\n\r\n\t\t\tPaint.SetPen( line == _caret.Line ? SyntaxColors.LineNumberActive : SyntaxColors.LineNumber );\r\n\t\t\tvar rect = new Rect( scroll.x, row * _lineHeight, GutterWidth - _charWidth, _lineHeight );\r\n\t\t\tPaint.DrawText( rect, (line \u002B 1).ToString(), TextFlag.RightCenter );\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Wavy underline under a marker\u0027s range, row by row. Zero-width markers underline one character.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void DrawSquiggle( EditorMarker marker, int firstLine, int lastLine )\r\n\t{\r\n\t\tvar start = Buffer.Clamp( marker.Start );\r\n\t\tvar end = Buffer.Clamp( marker.End );\r\n\t\tif ( end \u003C start ) end = start;\r\n\r\n\t\tPaint.SetPen( marker.IsError ? SyntaxColors.Error : SyntaxColors.Warning, 1 );\r\n\r\n\t\tfor ( int line = Math.Max( firstLine, start.Line ); line \u003C= Math.Min( lastLine, end.Line ); line\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar text = Buffer.Lines[line];\r\n\t\t\tvar from = line == start.Line ? start.Col : 0;\r\n\t\t\tvar to = line == end.Line ? end.Col : text.Length;\r\n\t\t\tvar starts = _rowStarts[line];\r\n\r\n\t\t\tfor ( int r = 0; r \u003C starts.Length; r\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar a = Math.Max( from, starts[r] );\r\n\t\t\t\tvar b = Math.Min( to, RowEnd( line, r ) );\r\n\r\n\t\t\t\tif ( b \u003C a ) continue;\r\n\r\n\t\t\t\t// Zero-width errors (like a missing \u0027;\u0027) still get one character\u0027s worth of underline\r\n\t\t\t\tvar x0 = TextLeft \u002B TextLayout.ColumnOf( text, starts[r], a ) * _charWidth;\r\n\t\t\t\tvar x1 = b \u003E a ? TextLeft \u002B TextLayout.ColumnOf( text, starts[r], b ) * _charWidth : x0 \u002B _charWidth;\r\n\r\n\t\t\t\tvar y = (_lineFirstRow[line] \u002B r \u002B 1) * _lineHeight - 3;\r\n\t\t\t\tvar points = new List\u003CVector2\u003E();\r\n\t\t\t\tfor ( var x = x0; x \u003C= x1; x \u002B= 2 )\r\n\t\t\t\t\tpoints.Add( new Vector2( x, y \u002B (((int)((x - x0) / 2)) % 2 == 0 ? 0 : 2) ) );\r\n\t\t\t\tif ( points.Count \u003E 1 ) Paint.DrawLine( points );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\tprivate int FirstMatchFrom( int line )\r\n\t{\r\n\t\tint lo = 0, hi = _matches.Count;\r\n\t\twhile ( lo \u003C hi )\r\n\t\t{\r\n\t\t\tvar mid = (lo \u002B hi) / 2;\r\n\t\t\tif ( _matches[mid].End.Line \u003C line ) lo = mid \u002B 1;\r\n\t\t\telse hi = mid;\r\n\t\t}\r\n\r\n\t\treturn lo;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Fills the area covered by a text range, row by row.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void DrawRange( TextPos start, TextPos end, Color color, int firstLine, int lastLine )\r\n\t{\r\n\t\tPaint.ClearPen();\r\n\t\tPaint.SetBrush( color );\r\n\r\n\t\tfor ( int line = Math.Max( firstLine, start.Line ); line \u003C= Math.Min( lastLine, end.Line ); line\u002B\u002B )\r\n\t\t{\r\n\t\t\tvar text = Buffer.Lines[line];\r\n\t\t\tvar from = line == start.Line ? start.Col : 0;\r\n\t\t\tvar to = line == end.Line ? end.Col : text.Length;\r\n\t\t\tvar starts = _rowStarts[line];\r\n\r\n\t\t\tfor ( int r = 0; r \u003C starts.Length; r\u002B\u002B )\r\n\t\t\t{\r\n\t\t\t\tvar rowStart = starts[r];\r\n\t\t\t\tvar rowEnd = RowEnd( line, r );\r\n\t\t\t\tvar isLastRow = r == starts.Length - 1;\r\n\r\n\t\t\t\tvar a = Math.Max( from, rowStart );\r\n\t\t\t\tvar b = Math.Min( to, rowEnd );\r\n\r\n\t\t\t\t// Selection running past the end of a line shows a little block for the newline\r\n\t\t\t\tvar newline = isLastRow \u0026\u0026 line \u003C end.Line;\r\n\t\t\t\tif ( a \u003E b || (a == b \u0026\u0026 !newline) ) continue;\r\n\t\t\t\tif ( !isLastRow \u0026\u0026 a == rowEnd ) continue;\r\n\r\n\t\t\t\tvar x0 = TextLayout.ColumnOf( text, rowStart, a );\r\n\t\t\t\tvar x1 = TextLayout.ColumnOf( text, rowStart, b ) \u002B (newline ? 1 : 0);\r\n\t\t\t\tvar y = (_lineFirstRow[line] \u002B r) * _lineHeight;\r\n\t\t\t\tPaint.DrawRect( new Rect( TextLeft \u002B x0 * _charWidth, y, (x1 - x0) * _charWidth, _lineHeight ) );\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Draws one row as runs of same-coloured text. Tabs are expanded so columns line up.\r\n\t/// \u003C/summary\u003E\r\n\tprivate void DrawRow( int row, int lineIndex, int rowStart, int rowEnd, List\u003CToken\u003E tokens )\r\n\t{\r\n\t\tif ( rowEnd \u003C= rowStart ) return;\r\n\r\n\t\tvar line = Buffer.Lines[lineIndex];\r\n\t\tvar y = row * _lineHeight;\r\n\r\n\t\tvar pos = rowStart;\r\n\t\tforeach ( var token in tokens )\r\n\t\t{\r\n\t\t\tvar tokenEnd = token.Start \u002B token.Length;\r\n\t\t\tif ( tokenEnd \u003C= rowStart ) continue;\r\n\t\t\tif ( token.Start \u003E= rowEnd ) break;\r\n\r\n\t\t\tvar start = Math.Max( token.Start, rowStart );\r\n\t\t\tvar end = Math.Min( tokenEnd, rowEnd );\r\n\t\t\tif ( start \u003E pos ) DrawRun( line, rowStart, pos, start, TokenKind.Text, y );\r\n\t\t\tDrawRun( line, rowStart, start, end, token.Kind, y );\r\n\t\t\tpos = end;\r\n\t\t}\r\n\r\n\t\tif ( pos \u003C rowEnd ) DrawRun( line, rowStart, pos, rowEnd, TokenKind.Text, y );\r\n\t}\r\n\r\n\tprivate void DrawRun( string line, int rowStart, int start, int end, TokenKind kind, float y )\r\n\t{\r\n\t\tvar text = line.Substring( start, end - start );\r\n\t\tif ( string.IsNullOrWhiteSpace( text ) ) return;\r\n\r\n\t\tvar startCol = TextLayout.ColumnOf( line, rowStart, start );\r\n\t\tvar display = text.Contains( \u0027\\t\u0027 ) ? ExpandTabs( text, startCol ) : text;\r\n\r\n\t\tPaint.SetPen( SyntaxColors.For( kind ) );\r\n\t\tvar rect = new Rect( TextLeft \u002B startCol * _charWidth, y, (display.Length \u002B 2) * _charWidth, _lineHeight );\r\n\t\tPaint.DrawText( rect, display, TextFlag.LeftCenter | TextFlag.SingleLine );\r\n\t}\r\n\r\n\tprivate static string ExpandTabs( string text, int startCol )\r\n\t{\r\n\t\tvar sb = new System.Text.StringBuilder();\r\n\t\tvar col = startCol;\r\n\t\tforeach ( var c in text )\r\n\t\t{\r\n\t\t\tif ( c == \u0027\\t\u0027 )\r\n\t\t\t{\r\n\t\t\t\tvar next = (col / TextLayout.TabSize \u002B 1) * TextLayout.TabSize;\r\n\t\t\t\tsb.Append( \u0027 \u0027, next - col );\r\n\t\t\t\tcol = next;\r\n\t\t\t}\r\n\t\t\telse\r\n\t\t\t{\r\n\t\t\t\tsb.Append( c );\r\n\t\t\t\tcol\u002B\u002B;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn sb.ToString();\r\n\t}\r\n}\r\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/Navigation/LatestOnlyWorker.cs","FileName":"LatestOnlyWorker.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Concurrent;\nusing System.Threading.Tasks;\n\n/// \u003Csummary\u003E\n/// Runs one background job at a time and only cares about the newest request: if more come in\n/// while a job runs, the last one runs next and the ones in between are skipped. Results are\n/// handed back on the main thread by \u003Csee cref=\u0022Pump\u0022/\u003E (call it every frame).\n///\n/// For things like autocomplete, where only the answer for the current keystroke matters.\n/// \u003C/summary\u003E\npublic class LatestOnlyWorker\u003CT\u003E\n{\n\tprivate bool _busy;\n\tprivate (Func\u003CT\u003E Work, Action\u003CT\u003E Apply)? _next;\n\tprivate readonly ConcurrentQueue\u003C(T Result, Exception Error, Action\u003CT\u003E Apply)\u003E _done = new();\n\n\t/// \u003Csummary\u003E\n\t/// Called on the main thread when a job throws.\n\t/// \u003C/summary\u003E\n\tpublic Action\u003CException\u003E OnError;\n\n\tpublic void Request( Func\u003CT\u003E work, Action\u003CT\u003E apply )\n\t{\n\t\tif ( _busy )\n\t\t{\n\t\t\t_next = (work, apply);\n\t\t\treturn;\n\t\t}\n\n\t\tStart( work, apply );\n\t}\n\n\tprivate void Start( Func\u003CT\u003E work, Action\u003CT\u003E apply )\n\t{\n\t\t_busy = true;\n\t\tTask.Run( work ).ContinueWith( t =\u003E _done.Enqueue( t.IsFaulted\n\t\t\t? (default, t.Exception?.InnerException ?? t.Exception, apply)\n\t\t\t: (t.Result, null, apply) ) );\n\t}\n\n\tpublic void Pump()\n\t{\n\t\twhile ( _done.TryDequeue( out var done ) )\n\t\t{\n\t\t\t_busy = false;\n\n\t\t\t// A newer request is waiting, so this result is already out of date\n\t\t\tif ( _next is { } next )\n\t\t\t{\n\t\t\t\t_next = null;\n\t\t\t\tStart( next.Work, next.Apply );\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tif ( done.Error is not null ) OnError?.Invoke( done.Error );\n\t\t\telse done.Apply( done.Result );\n\t\t}\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/EditorDocument.cs","FileName":"EditorDocument.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"/// \u003Csummary\u003E\r\n/// A file open in a tab. Each document keeps its own CodeView so undo history and\r\n/// scroll position survive switching tabs.\r\n/// \u003C/summary\u003E\r\npublic class EditorDocument\r\n{\r\n\t/// \u003Csummary\u003E\r\n\t/// Where the file lives. Changes when it\u0027s renamed or moved from the file tree.\r\n\t/// \u003C/summary\u003E\r\n\tpublic string Path { get; set; }\r\n\tpublic CodeView Editor { get; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Has edits that aren\u0027t written to disk yet.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool IsDirty { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Opened with a single click; gets replaced by the next single-clicked file\r\n\t/// unless it\u0027s edited or double-clicked.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool IsPreview { get; set; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// The file started with a UTF-8 byte order mark, so keep writing one.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool HasBom { get; init; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// RealTime.Now of the last edit, for auto save.\r\n\t/// \u003C/summary\u003E\r\n\tpublic double LastEdit { get; set; }\r\n\r\n\tpublic string Title =\u003E System.IO.Path.GetFileName( Path );\r\n\r\n\tpublic EditorDocument( string path, CodeView editor )\r\n\t{\r\n\t\tPath = path;\r\n\t\tEditor = editor;\r\n\t}\r\n}\r\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/Launcher/HotCodeInstaller.cs","FileName":"HotCodeInstaller.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\n\n/// \u003Csummary\u003E\n/// Adds HotCode to other projects by copying it into their Libraries folder, which is where\n/// s\u0026amp;box\u0027s Library Manager puts libraries too. Next time that project is opened in s\u0026amp;box,\n/// HotCode loads with it.\n///\n/// Until HotCode is published, this is the way to get it into another project; afterwards the\n/// Library Manager can install (and update) it as well.\n/// \u003C/summary\u003E\npublic static class HotCodeInstaller\n{\n\tpublic const string Ident = \u0022hotcode\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// A file only HotCode has, so it can be recognised whatever org it\u0027s published under.\n\t/// \u003C/summary\u003E\n\tstatic readonly string MarkerFile = Path.Combine( \u0022Editor\u0022, \u0022Launcher\u0022, \u0022HotCodeInstaller.cs\u0022 );\n\n\t/// \u003Csummary\u003E\n\t/// \u0022org.hotcode\u0022, using the org from HotCode\u0027s own .sbproj (the same naming the Library Manager uses).\n\t/// \u003C/summary\u003E\n\tpublic static string FolderName( string sourceRoot )\n\t{\n\t\ttry\n\t\t{\n\t\t\tusing var doc = JsonDocument.Parse( File.ReadAllText( Path.Combine( sourceRoot, Ident \u002B \u0022.sbproj\u0022 ) ) );\n\t\t\tif ( doc.RootElement.TryGetProperty( \u0022Org\u0022, out var org ) \u0026\u0026 org.GetString() is { Length: \u003E 0 } name )\n\t\t\t\treturn $\u0022{name.ToLowerInvariant()}.{Ident}\u0022;\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or JsonException or UnauthorizedAccessException )\n\t\t{\n\t\t}\n\n\t\treturn Ident;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Folders never copied: build output, editor state, and the logo source script.\n\t/// \u003C/summary\u003E\n\tstatic readonly HashSet\u003Cstring\u003E SkipFolders = new( StringComparer.OrdinalIgnoreCase ) { \u0022obj\u0022, \u0022bin\u0022, \u0022.vs\u0022, \u0022.git\u0022, \u0022.idea\u0022, \u0022logo\u0022, \u0022Properties\u0022 };\n\n\t/// \u003Csummary\u003E\n\t/// Files never copied: s\u0026amp;box generates these per project, with paths that only make sense where they were made.\n\t/// \u003C/summary\u003E\n\tstatic readonly HashSet\u003Cstring\u003E SkipExtensions = new( StringComparer.OrdinalIgnoreCase ) { \u0022.csproj\u0022, \u0022.sln\u0022, \u0022.slnx\u0022, \u0022.user\u0022 };\n\n\t/// \u003Csummary\u003E\n\t/// Where HotCode is installed in a project (any folder under Libraries whose .sbproj is HotCode\u0027s), or null.\n\t/// \u003C/summary\u003E\n\tpublic static string FindInstalled( string projectRoot )\n\t{\n\t\tvar libraries = Path.Combine( projectRoot, \u0022Libraries\u0022 );\n\t\tif ( !Directory.Exists( libraries ) ) return null;\n\n\t\tforeach ( var folder in Directory.EnumerateDirectories( libraries ) )\n\t\t{\n\t\t\tforeach ( var sbproj in Directory.EnumerateFiles( folder, \u0022*.sbproj\u0022 ) )\n\t\t\t\tif ( IsHotCode( sbproj ) ) return folder;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// True when an .sbproj is HotCode\u0027s own: ident \u0022hotcode\u0022 and HotCode\u0027s files next to it.\n\t/// Doesn\u0027t depend on the org, so renaming or changing the org doesn\u0027t break anything.\n\t/// \u003C/summary\u003E\n\tpublic static bool IsHotCode( string sbproj )\n\t{\n\t\ttry\n\t\t{\n\t\t\tif ( !File.Exists( Path.Combine( Path.GetDirectoryName( sbproj ), MarkerFile ) ) ) return false;\n\n\t\t\tusing var doc = JsonDocument.Parse( File.ReadAllText( sbproj ) );\n\t\t\treturn doc.RootElement.TryGetProperty( \u0022Ident\u0022, out var ident ) \u0026\u0026 string.Equals( ident.GetString(), Ident, StringComparison.OrdinalIgnoreCase );\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or JsonException or UnauthorizedAccessException )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Copies HotCode from \u003Cparamref name=\u0022sourceRoot\u0022/\u003E into the project. An existing copy is moved\n\t/// to the Recycle Bin first (via \u003Cparamref name=\u0022recycle\u0022/\u003E), so updating never loses anything for good.\n\t/// Returns the folder HotCode now lives in. Throws \u003Csee cref=\u0022InvalidOperationException\u0022/\u003E with a readable message.\n\t/// \u003C/summary\u003E\n\tpublic static string Install( string sourceRoot, string projectRoot, Action\u003Cstring\u003E recycle )\n\t{\n\t\tsourceRoot = Path.GetFullPath( sourceRoot ).TrimEnd( \u0027\\\\\u0027, \u0027/\u0027 );\n\t\tprojectRoot = Path.GetFullPath( projectRoot ).TrimEnd( \u0027\\\\\u0027, \u0027/\u0027 );\n\n\t\tif ( !File.Exists( Path.Combine( sourceRoot, Ident \u002B \u0022.sbproj\u0022 ) ) )\n\t\t\tthrow new InvalidOperationException( \u0022Couldn\u0027t find HotCode\u0027s own files to copy.\u0022 );\n\n\t\tif ( ProjectFiles.IsUnder( projectRoot, sourceRoot ) || ProjectFiles.IsUnder( sourceRoot, projectRoot ) )\n\t\t\tthrow new InvalidOperationException( \u0022That\u0027s the project HotCode is being developed in - it already has it.\u0022 );\n\n\t\tif ( !Directory.Exists( projectRoot ) || !Directory.EnumerateFiles( projectRoot, \u0022*.sbproj\u0022 ).Any() )\n\t\t\tthrow new InvalidOperationException( \u0022That folder isn\u0027t an s\u0026box project (no .sbproj file).\u0022 );\n\n\t\t// Replace an existing copy wherever it is, otherwise use the standard folder name\n\t\tvar target = FindInstalled( projectRoot ) ?? Path.Combine( projectRoot, \u0022Libraries\u0022, FolderName( sourceRoot ) );\n\t\tif ( ProjectFiles.IsUnder( target, sourceRoot ) )\n\t\t\tthrow new InvalidOperationException( \u0022Refusing to overwrite HotCode\u0027s own source folder.\u0022 );\n\n\t\t// Copy into a staging folder first, so a failed copy never leaves a half-installed library\n\t\tvar staging = target \u002B \u0022.installing\u0022;\n\t\tif ( Directory.Exists( staging ) ) Directory.Delete( staging, recursive: true );\n\t\tCopyFolder( sourceRoot, staging );\n\n\t\tif ( Directory.Exists( target ) ) recycle( target );\n\t\tDirectory.Move( staging, target );\n\n\t\treturn target;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Removes HotCode from a project (to the Recycle Bin). Returns false if it wasn\u0027t installed.\n\t/// \u003C/summary\u003E\n\tpublic static bool Uninstall( string sourceRoot, string projectRoot, Action\u003Cstring\u003E recycle )\n\t{\n\t\tvar installed = FindInstalled( projectRoot );\n\t\tif ( installed is null ) return false;\n\n\t\tif ( ProjectFiles.IsUnder( installed, sourceRoot ) )\n\t\t\tthrow new InvalidOperationException( \u0022That\u0027s HotCode\u0027s own source folder - not removing it.\u0022 );\n\n\t\trecycle( installed );\n\t\treturn true;\n\t}\n\n\tprivate static void CopyFolder( string from, string to )\n\t{\n\t\tDirectory.CreateDirectory( to );\n\n\t\tforeach ( var file in Directory.EnumerateFiles( from ) )\n\t\t{\n\t\t\tif ( SkipExtensions.Contains( Path.GetExtension( file ) ) ) continue;\n\t\t\tFile.Copy( file, Path.Combine( to, Path.GetFileName( file ) ), overwrite: true );\n\t\t}\n\n\t\tforeach ( var folder in Directory.EnumerateDirectories( from ) )\n\t\t{\n\t\t\tif ( SkipFolders.Contains( Path.GetFileName( folder ) ) ) continue;\n\t\t\tCopyFolder( folder, Path.Combine( to, Path.GetFileName( folder ) ) );\n\t\t}\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/CodeFileTree.cs","FileName":"CodeFileTree.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// Whether a file is open in a tab, and whether it\u0027s the one being edited.\n/// \u003C/summary\u003E\npublic enum FileState\n{\n\tClosed,\n\tOpen,\n\tActive\n}\n\n/// \u003Csummary\u003E\n/// A lazily-built tree node for a file or folder on disk. Only shows code/text files\n/// and skips build output folders.\n/// \u003C/summary\u003E\npublic class CodeFileNode : TreeNode\n{\n\tpublic static readonly HashSet\u003Cstring\u003E HiddenFolders = new( StringComparer.OrdinalIgnoreCase ) { \u0022bin\u0022, \u0022obj\u0022, \u0022.vs\u0022, \u0022.git\u0022, \u0022.idea\u0022, \u0022.sbox\u0022, \u0022node_modules\u0022 };\n\n\tstatic readonly HashSet\u003Cstring\u003E CodeExtensions = new( StringComparer.OrdinalIgnoreCase )\n\t{\n\t\t\u0022.cs\u0022, \u0022.razor\u0022, \u0022.scss\u0022, \u0022.css\u0022, \u0022.json\u0022, \u0022.sbproj\u0022, \u0022.csproj\u0022, \u0022.sln\u0022, \u0022.slnx\u0022,\n\t\t\u0022.md\u0022, \u0022.txt\u0022, \u0022.shader\u0022, \u0022.hlsl\u0022, \u0022.editorconfig\u0022, \u0022.gitignore\u0022, \u0022.xml\u0022, \u0022.yml\u0022, \u0022.yaml\u0022\n\t};\n\n\tpublic FileSystemInfo Info { get; }\n\tpublic bool IsFolder =\u003E Info is DirectoryInfo;\n\tpublic string FullPath =\u003E Info.FullName;\n\n\tprivate readonly HotCodeEditor _host;\n\n\tpublic CodeFileNode( HotCodeEditor host, FileSystemInfo info ) : base( info.FullName )\n\t{\n\t\t_host = host;\n\t\tInfo = info;\n\t\t_host.RegisterNode( this );\n\t}\n\n\tpublic override string Name =\u003E Info.Name;\n\tpublic override bool HasChildren =\u003E IsFolder;\n\tpublic override string GetTooltip() =\u003E Info.FullName;\n\n\tpublic static bool IsCodeFile( string path ) =\u003E CodeExtensions.Contains( Path.GetExtension( path ) ) || CodeExtensions.Contains( Path.GetFileName( path ) );\n\n\t/// \u003Csummary\u003E\n\t/// True for paths inside bin/obj/.git etc, which the tree never shows.\n\t/// \u003C/summary\u003E\n\tpublic static bool IsInHiddenFolder( string path ) =\u003E path.Split( Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar ).Any( HiddenFolders.Contains );\n\n\tpublic override void OnPaint( VirtualWidget item )\n\t{\n\t\tPaintSelection( item );\n\n\t\tvar state = IsFolder ? FileState.Closed : _host.StateOf( FullPath );\n\n\t\t// Folders in s\u0026box\u0027s folder colour; files coloured by type\n\t\tvar open = IsFolder \u0026\u0026 item.IsOpen;\n\t\tPaint.SetPen( IsFolder ? Theme.Folder : HotCodeStyle.FileIconColor( FullPath ) );\n\t\tPaint.DrawIcon( item.Rect, IsFolder ? (open ? \u0022folder_open\u0022 : \u0022folder\u0022) : HotCodeStyle.FileIcon( FullPath ), 15, TextFlag.LeftCenter );\n\n\t\t// The file you\u0027re editing is bold; other open files are bright, the rest are dimmer\n\t\tPaint.SetDefaultFont( HotCodeStyle.FontSize, state == FileState.Active ? 700 : 400 );\n\t\tPaint.SetPen( IsFolder || state != FileState.Closed || item.Selected ? Theme.Text : Theme.TextLight );\n\t\tPaint.DrawText( item.Rect.Shrink( 22, 0, 0, 0 ), Info.Name, TextFlag.LeftCenter );\n\n\t\tif ( state != FileState.Closed \u0026\u0026 _host.IsUnsaved( FullPath ) )\n\t\t{\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.Text );\n\t\t\tPaint.DrawCircle( new Vector2( item.Rect.Right - 10, item.Rect.Center.y ), 6 );\n\t\t}\n\t}\n\n\tpublic override bool OnContextMenu()\n\t{\n\t\t_host.ShowFileContextMenu( this );\n\t\treturn true;\n\t}\n\n\n\tprotected override void BuildChildren()\n\t{\n\t\tif ( Info is not DirectoryInfo dir )\n\t\t\treturn;\n\n\t\tClear();\n\n\t\tIEnumerable\u003CFileSystemInfo\u003E entries;\n\t\ttry\n\t\t{\n\t\t\tdir.Refresh();\n\t\t\tentries = dir.GetFileSystemInfos();\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or UnauthorizedAccessException )\n\t\t{\n\t\t\tLog.Warning( $\u0022[HotCode] Can\u0027t read {dir.FullName}: {e.Message}\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar folders = entries.OfType\u003CDirectoryInfo\u003E()\n\t\t\t.Where( x =\u003E !HiddenFolders.Contains( x.Name ) \u0026\u0026 ShouldShowFolder( x ) )\n\t\t\t.OrderBy( x =\u003E x.Name, StringComparer.OrdinalIgnoreCase );\n\n\t\tvar files = entries.OfType\u003CFileInfo\u003E()\n\t\t\t.Where( x =\u003E IsCodeFile( x.Name ) )\n\t\t\t.OrderBy( x =\u003E x.Name, StringComparer.OrdinalIgnoreCase );\n\n\t\tAddItems( folders.Select( x =\u003E new CodeFileNode( _host, x ) ) );\n\t\tAddItems( files.Select( x =\u003E new CodeFileNode( _host, x ) ) );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Hide folders full of things we\u0027d never open (e.g. Assets full of textures),\n\t/// but keep empty ones so a folder you just made shows up.\n\t/// \u003C/summary\u003E\n\tstatic bool ShouldShowFolder( DirectoryInfo dir )\n\t{\n\t\ttry\n\t\t{\n\t\t\tvar any = false;\n\t\t\tforeach ( var f in dir.EnumerateFiles( \u0022*\u0022, SearchOption.AllDirectories ) )\n\t\t\t{\n\t\t\t\tif ( IsInHiddenFolder( f.FullName ) ) continue;\n\t\t\t\tif ( IsCodeFile( f.Name ) ) return true;\n\t\t\t\tany = true;\n\t\t\t}\n\n\t\t\treturn !any;\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or UnauthorizedAccessException )\n\t\t{\n\t\t\treturn false;\n\t\t}\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/CodeView/TextLayout.cs","FileName":"TextLayout.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// Column maths for monospaced text: tab expansion and word wrapping.\n/// A line is shown as one or more rows; each row starts at a character index.\n/// Columns inside a row count from the start of that row.\n/// \u003C/summary\u003E\npublic static class TextLayout\n{\n\t/// \u003Csummary\u003E\n\t/// Columns per tab. Set from HotCode\u0027s settings.\n\t/// \u003C/summary\u003E\n\tpublic static int TabSize { get; set; } = 4;\n\n\tprivate static readonly int[] SingleRow = { 0 };\n\n\tprivate static int Advance( char c, int col ) =\u003E c == \u0027\\t\u0027 ? (col / TabSize \u002B 1) * TabSize : col \u002B 1;\n\n\t/// \u003Csummary\u003E\n\t/// Column that character \u003Cparamref name=\u0022index\u0022/\u003E lands on, counting from \u003Cparamref name=\u0022rowStart\u0022/\u003E.\n\t/// \u003C/summary\u003E\n\tpublic static int ColumnOf( string line, int rowStart, int index )\n\t{\n\t\tint col = 0;\n\t\tfor ( int i = rowStart; i \u003C index \u0026\u0026 i \u003C line.Length; i\u002B\u002B )\n\t\t\tcol = Advance( line[i], col );\n\t\treturn col;\n\t}\n\n\tpublic static int Width( string line ) =\u003E ColumnOf( line, 0, line.Length );\n\n\t/// \u003Csummary\u003E\n\t/// Character index in [rowStart, rowEnd] nearest to a (possibly fractional) column.\n\t/// \u003C/summary\u003E\n\tpublic static int IndexAtColumn( string line, int rowStart, int rowEnd, float column )\n\t{\n\t\tint col = 0;\n\t\tfor ( int i = rowStart; i \u003C rowEnd \u0026\u0026 i \u003C line.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar next = Advance( line[i], col );\n\t\t\tif ( column \u003C (col \u002B next) / 2f ) return i;\n\t\t\tcol = next;\n\t\t}\n\n\t\treturn rowEnd;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Where each row of a line starts when wrapped to \u003Cparamref name=\u0022columns\u0022/\u003E. Breaks after\n\t/// whitespace when it can, mid-word when a word is longer than a row. Columns \u0026lt;= 0 means no wrapping.\n\t/// \u003C/summary\u003E\n\tpublic static int[] Wrap( string line, int columns )\n\t{\n\t\tif ( columns \u003C= 0 || line.Length == 0 || Width( line ) \u003C= columns )\n\t\t\treturn SingleRow;\n\n\t\tvar starts = new List\u003Cint\u003E { 0 };\n\t\tint rowStart = 0;\n\t\tint col = 0;\n\t\tint lastBreak = -1;\n\t\tvar rowHasText = false;\n\n\t\tfor ( int i = 0; i \u003C line.Length; i\u002B\u002B )\n\t\t{\n\t\t\tvar next = Advance( line[i], col );\n\n\t\t\t// Loops at most twice: breaking at the last space can still leave this character\n\t\t\t// (say, a tab) too wide, and then it moves to a row of its own start\n\t\t\twhile ( next \u003E columns \u0026\u0026 i \u003E rowStart )\n\t\t\t{\n\t\t\t\t// Whitespace stays at the end of the row it follows, so a row never starts with the break\n\t\t\t\tvar breakAt = lastBreak \u003E rowStart \u0026\u0026 lastBreak \u003C= i ? lastBreak : i;\n\t\t\t\tstarts.Add( breakAt );\n\t\t\t\trowStart = breakAt;\n\t\t\t\tlastBreak = -1;\n\t\t\t\trowHasText = false;\n\n\t\t\t\tcol = ColumnOf( line, rowStart, i );\n\t\t\t\tnext = Advance( line[i], col );\n\t\t\t}\n\n\t\t\tcol = next;\n\n\t\t\tvar isSpace = line[i] == \u0027 \u0027 || line[i] == \u0027\\t\u0027;\n\t\t\tif ( !isSpace ) rowHasText = true;\n\n\t\t\t// Leading indentation isn\u0027t a break point, or we\u0027d get rows of nothing but indent\n\t\t\telse if ( rowHasText ) lastBreak = i \u002B 1;\n\t\t}\n\n\t\treturn starts.ToArray();\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/Ui/HotCodeLogo.cs","FileName":"HotCodeLogo.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// The HotCode flame: red outer flame, orange middle, yellow core, each with a vertical gradient.\n/// The outlines are generated by logo/flame.py (HotCodeLogo.Shapes.cs), which also renders the\n/// PNGs next to it in logo/, so the painted logo and the images always match.\n/// \u003C/summary\u003E\npublic static partial class HotCodeLogo\n{\n\t/// \u003Csummary\u003E\n\t/// Material icon name for places that only take an icon name (EditorApp, menus).\n\t/// \u003C/summary\u003E\n\tpublic const string IconName = \u0022local_fire_department\u0022;\n\n\tprivate static readonly (Func\u003CVector2[]\u003E Shape, Color Top, Color Bottom)[] Layers =\n\t{\n\t\t(() =\u003E Outer, Hex( \u0022#E21D12\u0022 ), Hex( \u0022#FF5A14\u0022 )),\n\t\t(() =\u003E Middle, Hex( \u0022#FF6A10\u0022 ), Hex( \u0022#FFA01C\u0022 )),\n\t\t(() =\u003E Core, Hex( \u0022#FFC82A\u0022 ), Hex( \u0022#FFF19A\u0022 )),\n\t};\n\n\tpublic static readonly Color BadgeBackground = Hex( \u0022#1c1c1c\u0022 );\n\tpublic static readonly Color Glow = Hex( \u0022#FF5810\u0022 );\n\n\t/// \u003Csummary\u003E\n\t/// Draws just the flame, filling \u003Cparamref name=\u0022rect\u0022/\u003E (keep it square).\n\t/// \u003C/summary\u003E\n\tpublic static void PaintFlame( Rect rect )\n\t{\n\t\tPaint.Antialiasing = true;\n\t\tPaint.ClearPen();\n\n\t\tforeach ( var (shape, top, bottom) in Layers )\n\t\t{\n\t\t\tvar points = shape().Select( p =\u003E new Vector2( rect.Left \u002B p.x * rect.Width, rect.Top \u002B p.y * rect.Height ) ).ToArray();\n\t\t\tPaint.SetBrushLinear( new Vector2( rect.Left, rect.Top ), new Vector2( rect.Left, rect.Bottom ), top, bottom );\n\t\t\tPaint.DrawPolygon( points );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The flame on a dark rounded square with a warm glow, like the package thumbnail.\n\t/// \u003C/summary\u003E\n\tpublic static void PaintBadge( Rect rect )\n\t{\n\t\tPaint.Antialiasing = true;\n\t\tPaint.ClearPen();\n\t\tPaint.SetBrush( BadgeBackground );\n\t\tPaint.DrawRect( rect, rect.Width * 0.18f );\n\n\t\tvar glow = new Rect( rect.Left \u002B rect.Width * 0.2f, rect.Top \u002B rect.Height * 0.32f, rect.Width * 0.6f, rect.Height * 0.58f );\n\t\tPaint.SetBrushRadial( glow.Center, glow.Width * 0.5f, Glow.WithAlpha( 0.35f ), Glow.WithAlpha( 0 ) );\n\t\tPaint.DrawRect( glow );\n\n\t\tvar inset = rect.Width * 0.15f;\n\t\tPaintFlame( new Rect( rect.Left \u002B inset, rect.Top \u002B rect.Height * 0.13f, rect.Width - inset * 2, rect.Height - inset * 2 ) );\n\t}\n\n\tprivate static Pixmap _windowIcon;\n\n\t/// \u003Csummary\u003E\n\t/// The flame as a pixmap, for SetWindowIcon (window title bars and the taskbar).\n\t/// \u003C/summary\u003E\n\tpublic static Pixmap WindowIcon\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( _windowIcon is not null ) return _windowIcon;\n\n\t\t\t_windowIcon = new Pixmap( 64, 64 );\n\t\t\t_windowIcon.Clear( Color.Transparent );\n\t\t\tusing ( Paint.ToPixmap( _windowIcon ) )\n\t\t\t{\n\t\t\t\tPaintFlame( new Rect( 0, 0, 64, 64 ) );\n\t\t\t}\n\n\t\t\treturn _windowIcon;\n\t\t}\n\t}\n\n\tprivate static Color Hex( string hex ) =\u003E Color.Parse( hex ) ?? Color.White;\n}\n\n/// \u003Csummary\u003E\n/// A widget that just shows the logo: the badge, or the bare flame.\n/// \u003C/summary\u003E\npublic class LogoWidget : Widget\n{\n\tprivate readonly bool _badge;\n\n\tpublic LogoWidget( Widget parent, float size, bool badge ) : base( parent )\n\t{\n\t\t_badge = badge;\n\t\tFixedSize = new Vector2( size, size );\n\t\tTransparentForMouseEvents = true;\n\t}\n\n\tprotected override void OnPaint()\n\t{\n\t\tif ( _badge ) HotCodeLogo.PaintBadge( LocalRect );\n\t\telse HotCodeLogo.PaintFlame( LocalRect );\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/CodeView/TextSearch.cs","FileName":"TextSearch.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Text.RegularExpressions;\n\npublic readonly record struct SearchMatch( TextPos Start, TextPos End, Match RegexMatch );\n\npublic record struct SearchOptions( string Query, bool MatchCase, bool WholeWord, bool UseRegex );\n\n/// \u003Csummary\u003E\n/// Finding text in a \u003Csee cref=\u0022TextBuffer\u0022/\u003E. Plain searches go through Regex too (escaped)\n/// so every option behaves the same way.\n/// \u003C/summary\u003E\npublic static class TextSearch\n{\n\tpublic const int MaxMatches = 10000;\n\n\t/// \u003Csummary\u003E\n\t/// All matches in order. Returns an empty list and sets \u003Cparamref name=\u0022error\u0022/\u003E for a bad regex.\n\t/// \u003C/summary\u003E\n\tpublic static List\u003CSearchMatch\u003E FindAll( TextBuffer buffer, SearchOptions options, out string error )\n\t{\n\t\terror = null;\n\t\tvar results = new List\u003CSearchMatch\u003E();\n\t\tif ( string.IsNullOrEmpty( options.Query ) ) return results;\n\n\t\tvar regex = Build( options, out error );\n\t\tif ( regex is null ) return results;\n\n\t\tvar text = string.Join( \u0022\\n\u0022, buffer.Lines );\n\t\tvar lineStarts = new int[buffer.LineCount];\n\t\tfor ( int i = 1; i \u003C buffer.LineCount; i\u002B\u002B )\n\t\t\tlineStarts[i] = lineStarts[i - 1] \u002B buffer.Lines[i - 1].Length \u002B 1;\n\n\t\ttry\n\t\t{\n\t\t\tfor ( var m = regex.Match( text ); m.Success \u0026\u0026 results.Count \u003C MaxMatches; m = m.NextMatch() )\n\t\t\t{\n\t\t\t\t// Zero-length matches (like \u0022^\u0022) can\u0027t be selected or replaced sensibly\n\t\t\t\tif ( m.Length == 0 ) continue;\n\t\t\t\tresults.Add( new SearchMatch( ToPos( lineStarts, m.Index ), ToPos( lineStarts, m.Index \u002B m.Length ), m ) );\n\t\t\t}\n\t\t}\n\t\tcatch ( RegexMatchTimeoutException )\n\t\t{\n\t\t\terror = \u0022Search took too long\u0022;\n\t\t}\n\n\t\treturn results;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// What a match gets replaced with. With regex on, $1 / ${name} refer to groups.\n\t/// \u003C/summary\u003E\n\tpublic static string ReplacementFor( SearchMatch match, string replacement, SearchOptions options )\n\t{\n\t\treplacement ??= \u0022\u0022;\n\t\treturn options.UseRegex ? match.RegexMatch.Result( replacement ) : replacement;\n\t}\n\n\tprivate static Regex Build( SearchOptions options, out string error )\n\t{\n\t\terror = null;\n\n\t\tvar pattern = options.UseRegex ? options.Query : Regex.Escape( options.Query );\n\n\t\t// Like \\b, but also works when the query starts or ends with a symbol\n\t\tif ( options.WholeWord ) pattern = $@\u0022(?\u003C![\\w]){pattern}(?![\\w])\u0022;\n\n\t\tvar flags = RegexOptions.Multiline | RegexOptions.CultureInvariant;\n\t\tif ( !options.MatchCase ) flags |= RegexOptions.IgnoreCase;\n\n\t\ttry\n\t\t{\n\t\t\treturn new Regex( pattern, flags, TimeSpan.FromMilliseconds( 250 ) );\n\t\t}\n\t\tcatch ( ArgumentException e )\n\t\t{\n\t\t\terror = e.Message;\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate static TextPos ToPos( int[] lineStarts, int offset )\n\t{\n\t\tvar line = Array.BinarySearch( lineStarts, offset );\n\t\tif ( line \u003C 0 ) line = ~line - 1;\n\t\treturn new TextPos( line, offset - lineStarts[line] );\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/Settings/HotCodeSettings.cs","FileName":"HotCodeSettings.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\n\npublic enum AutoSaveMode\n{\n\t[Title( \u0022Off\u0022 )] Off,\n\t[Title( \u0022After a short pause\u0022 )] AfterDelay,\n\t[Title( \u0022When switching tabs\u0022 )] WhenSwitchingTabs\n}\n\npublic enum CodeTheme\n{\n\t[Title( \u0022Dark\u002B (VS Code)\u0022 )] DarkPlus,\n\t[Title( \u0022Monokai\u0022 )] Monokai,\n\t[Title( \u0022One Dark\u0022 )] OneDark\n}\n\n/// \u003Csummary\u003E\n/// HotCode\u0027s settings. Static properties so s\u0026amp;box\u0027s ControlSheet can edit them directly, the same\n/// way its own Editor Settings pages work. Saved as one editor cookie; \u003Csee cref=\u0022Version\u0022/\u003E goes up on\n/// every change so open editors know to re-measure and re-layout.\n/// \u003C/summary\u003E\npublic static class HotCodeSettings\n{\n\tconst string CookieKey = \u0022hotcode.settings\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// The stored values. A plain class so it round-trips through JSON.\n\t/// \u003C/summary\u003E\n\tpublic class Data\n\t{\n\t\tpublic float FontSize { get; set; } = 10;\n\t\tpublic string FontFamily { get; set; } = \u0022\u0022;\n\t\tpublic float LineSpacing { get; set; } = 1.35f;\n\t\tpublic int TabSize { get; set; } = 4;\n\t\tpublic bool IndentWithSpaces { get; set; }\n\t\tpublic bool WordWrap { get; set; }\n\t\tpublic bool ShowLineNumbers { get; set; } = true;\n\t\tpublic bool HighlightCurrentLine { get; set; } = true;\n\t\tpublic bool CursorBlink { get; set; } = true;\n\t\tpublic CodeTheme Theme { get; set; } = CodeTheme.DarkPlus;\n\n\t\tpublic bool AutoCloseBrackets { get; set; } = true;\n\t\tpublic bool SuggestWhileTyping { get; set; } = true;\n\t\tpublic bool ParameterHints { get; set; } = true;\n\t\tpublic bool UnderlineErrors { get; set; } = true;\n\n\t\tpublic AutoSaveMode AutoSave { get; set; } = AutoSaveMode.Off;\n\t\tpublic float AutoSaveDelay { get; set; } = 1.5f;\n\t\tpublic bool TrimTrailingWhitespace { get; set; }\n\t\tpublic bool InsertFinalNewline { get; set; }\n\t\tpublic bool ReloadChangedFiles { get; set; } = true;\n\t}\n\n\tprivate static Data _data;\n\n\tprivate static Data Current\n\t{\n\t\tget\n\t\t{\n\t\t\tif ( _data is not null ) return _data;\n\n\t\t\t_data = EditorCookie.Get\u003CData\u003E( CookieKey, null );\n\t\t\tif ( _data is null )\n\t\t\t{\n\t\t\t\t// First run: carry over the settings that used to be stored on their own\n\t\t\t\t_data = new Data\n\t\t\t\t{\n\t\t\t\t\tFontSize = EditorCookie.Get( \u0022hotcode.fontsize\u0022, 10f ),\n\t\t\t\t\tWordWrap = EditorCookie.Get( \u0022hotcode.wordwrap\u0022, false )\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tApply();\n\t\t\treturn _data;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Goes up every time any setting changes.\n\t/// \u003C/summary\u003E\n\tpublic static int Version { get; private set; }\n\n\tprivate static void Set( Action\u003CData\u003E change )\n\t{\n\t\tchange( Current );\n\t\tEditorCookie.Set( CookieKey, Current );\n\t\tApply();\n\t\tVersion\u002B\u002B;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Pushes settings into the places that read them as plain values.\n\t/// \u003C/summary\u003E\n\tprivate static void Apply()\n\t{\n\t\tTextLayout.TabSize = Math.Clamp( _data.TabSize, 1, 16 );\n\t\tSyntaxColors.Use( _data.Theme );\n\t}\n\n\tpublic static void ResetToDefaults() =\u003E Set( d =\u003E\n\t{\n\t\tvar defaults = new Data();\n\t\tforeach ( var property in typeof( Data ).GetProperties() )\n\t\t\tproperty.SetValue( d, property.GetValue( defaults ) );\n\t} );\n\n\t// --- EDITOR ---\n\n\t[Title( \u0022Font Size\u0022 ), Description( \u0022Code font size in points. Ctrl\u002B= / Ctrl\u002B- or Ctrl\u002Bmouse wheel change it too.\u0022 ), Range( 6, 28 ), Step( 1 )]\n\tpublic static float FontSize { get =\u003E Current.FontSize; set =\u003E Set( d =\u003E d.FontSize = Math.Clamp( value, 6, 28 ) ); }\n\n\t[Title( \u0022Font\u0022 ), Description( \u0022Code font family. Leave empty to use s\u0026box\u0027s monospace font. Use a monospaced font so columns line up.\u0022 )]\n\tpublic static string FontFamily { get =\u003E Current.FontFamily; set =\u003E Set( d =\u003E d.FontFamily = value?.Trim() ?? \u0022\u0022 ); }\n\n\t[Title( \u0022Line Spacing\u0022 ), Description( \u0022Line height as a multiple of the font height.\u0022 ), Range( 1, 2 ), Step( 0.05f )]\n\tpublic static float LineSpacing { get =\u003E Current.LineSpacing; set =\u003E Set( d =\u003E d.LineSpacing = Math.Clamp( value, 1, 2 ) ); }\n\n\t[Title( \u0022Tab Size\u0022 ), Description( \u0022How many columns a tab takes up.\u0022 ), Range( 1, 8 ), Step( 1 )]\n\tpublic static int TabSize { get =\u003E Current.TabSize; set =\u003E Set( d =\u003E d.TabSize = Math.Clamp( value, 1, 8 ) ); }\n\n\t[Title( \u0022Indent With Spaces\u0022 ), Description( \u0022For files whose indentation can\u0027t be detected. Existing files keep whatever they already use.\u0022 )]\n\tpublic static bool IndentWithSpaces { get =\u003E Current.IndentWithSpaces; set =\u003E Set( d =\u003E d.IndentWithSpaces = value ); }\n\n\t[Title( \u0022Word Wrap\u0022 ), Description( \u0022Wrap long lines to the width of the editor (Alt\u002BZ).\u0022 )]\n\tpublic static bool WordWrap { get =\u003E Current.WordWrap; set =\u003E Set( d =\u003E d.WordWrap = value ); }\n\n\t[Title( \u0022Line Numbers\u0022 )]\n\tpublic static bool ShowLineNumbers { get =\u003E Current.ShowLineNumbers; set =\u003E Set( d =\u003E d.ShowLineNumbers = value ); }\n\n\t[Title( \u0022Highlight Current Line\u0022 )]\n\tpublic static bool HighlightCurrentLine { get =\u003E Current.HighlightCurrentLine; set =\u003E Set( d =\u003E d.HighlightCurrentLine = value ); }\n\n\t[Title( \u0022Blinking Cursor\u0022 )]\n\tpublic static bool CursorBlink { get =\u003E Current.CursorBlink; set =\u003E Set( d =\u003E d.CursorBlink = value ); }\n\n\t[Title( \u0022Code Colours\u0022 ), Description( \u0022Syntax colours for the code area. The rest of HotCode follows s\u0026box\u0027s theme.\u0022 )]\n\tpublic static CodeTheme CodeColors { get =\u003E Current.Theme; set =\u003E Set( d =\u003E d.Theme = value ); }\n\n\t// --- CODE HELP ---\n\n\t[Title( \u0022Auto-Close Brackets and Quotes\u0022 ), Description( \u0022Typing ( [ { \\\u0022 or \u0027 adds the closing one.\u0022 )]\n\tpublic static bool AutoCloseBrackets { get =\u003E Current.AutoCloseBrackets; set =\u003E Set( d =\u003E d.AutoCloseBrackets = value ); }\n\n\t[Title( \u0022Suggestions While Typing\u0022 ), Description( \u0022Show autocomplete as you type. Ctrl\u002BSpace always works.\u0022 )]\n\tpublic static bool SuggestWhileTyping { get =\u003E Current.SuggestWhileTyping; set =\u003E Set( d =\u003E d.SuggestWhileTyping = value ); }\n\n\t[Title( \u0022Parameter Hints\u0022 ), Description( \u0022Show a method\u0027s parameters when typing ( or , in a call. Ctrl\u002BShift\u002BSpace always works.\u0022 )]\n\tpublic static bool ParameterHints { get =\u003E Current.ParameterHints; set =\u003E Set( d =\u003E d.ParameterHints = value ); }\n\n\t[Title( \u0022Underline Compile Errors\u0022 ), Description( \u0022Underline errors and warnings from s\u0026box\u0027s last compile in the code.\u0022 )]\n\tpublic static bool UnderlineErrors { get =\u003E Current.UnderlineErrors; set =\u003E Set( d =\u003E d.UnderlineErrors = value ); }\n\n\t// --- FILES ---\n\n\t[Title( \u0022Auto Save\u0022 ), Description( \u0022Saving triggers s\u0026box\u0027s hotload, so auto save means changes show up in the game as you type.\u0022 )]\n\tpublic static AutoSaveMode AutoSave { get =\u003E Current.AutoSave; set =\u003E Set( d =\u003E d.AutoSave = value ); }\n\n\t[Title( \u0022Auto Save Delay\u0022 ), Description( \u0022Seconds after you stop typing, when Auto Save is \\\u0022After a short pause\\\u0022.\u0022 ), Range( 0.5f, 10 ), Step( 0.5f )]\n\tpublic static float AutoSaveDelay { get =\u003E Current.AutoSaveDelay; set =\u003E Set( d =\u003E d.AutoSaveDelay = Math.Clamp( value, 0.5f, 10 ) ); }\n\n\t[Title( \u0022Trim Trailing Whitespace\u0022 ), Description( \u0022Remove spaces and tabs at the ends of lines when saving.\u0022 )]\n\tpublic static bool TrimTrailingWhitespace { get =\u003E Current.TrimTrailingWhitespace; set =\u003E Set( d =\u003E d.TrimTrailingWhitespace = value ); }\n\n\t[Title( \u0022Insert Final Newline\u0022 ), Description( \u0022Make sure files end with a newline when saving.\u0022 )]\n\tpublic static bool InsertFinalNewline { get =\u003E Current.InsertFinalNewline; set =\u003E Set( d =\u003E d.InsertFinalNewline = value ); }\n\n\t[Title( \u0022Reload Files Changed Elsewhere\u0022 ), Description( \u0022When another program changes an open file that has no unsaved edits here, reload it.\u0022 )]\n\tpublic static bool ReloadChangedFiles { get =\u003E Current.ReloadChangedFiles; set =\u003E Set( d =\u003E d.ReloadChangedFiles = value ); }\n\n\t/// \u003Csummary\u003E\n\t/// The code font to use: the chosen family, or s\u0026amp;box\u0027s monospace font.\n\t/// \u003C/summary\u003E\n\tpublic static string CodeFont =\u003E string.IsNullOrWhiteSpace( FontFamily ) ? Editor.Theme.MonospaceFont : FontFamily;\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/Ui/HotCodeStyle.cs","FileName":"HotCodeStyle.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\n\n/// \u003Csummary\u003E\n/// Shared drawing for HotCode\u0027s chrome so it matches the rest of the s\u0026amp;box editor.\n/// Colours come from \u003Csee cref=\u0022Theme\u0022/\u003E (s\u0026amp;box\u0027s tools/assets/styles/theme.json), so they\n/// follow s\u0026amp;box if it changes. Only the code area itself uses its own dark colours (\u003Csee cref=\u0022SyntaxColors\u0022/\u003E).\n/// \u003C/summary\u003E\npublic static class HotCodeStyle\n{\n\tpublic const float TabHeight = 26;\n\tpublic const float FontSize = 8;\n\n\t/// \u003Csummary\u003E\n\t/// Draws a tab the way s\u0026amp;box\u0027s QTabBar style does: rounded top corners, a 2px Primary line\n\t/// on the selected tab, light text when inactive, a surface highlight on hover.\n\t/// Returns the rect left over for content (after padding).\n\t/// \u003C/summary\u003E\n\tpublic static Rect DrawTab( Rect rect, bool selected, bool hovered )\n\t{\n\t\tif ( selected || hovered )\n\t\t{\n\t\t\t// Extend past the bottom so only the top corners show as rounded\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( selected ? Theme.TabBackground : Theme.SurfaceBackground );\n\t\t\tPaint.DrawRect( new Rect( rect.Left, rect.Top, rect.Width, rect.Height \u002B Theme.ControlRadius ), Theme.ControlRadius );\n\t\t}\n\n\t\tif ( selected )\n\t\t{\n\t\t\tPaint.SetBrush( Theme.Primary );\n\t\t\tPaint.DrawRect( new Rect( rect.Left \u002B 1, rect.Top, rect.Width - 2, 2 ), 1 );\n\t\t}\n\n\t\treturn rect.Shrink( 12, 2, 8, 0 );\n\t}\n\n\tpublic static Color TabTextColor( bool selected, bool hovered ) =\u003E selected || hovered ? Theme.Text : Theme.TextLight;\n\n\tpublic static void SetTabFont( bool italic = false ) =\u003E Paint.SetDefaultFont( FontSize, 500, italic );\n\n\t/// \u003Csummary\u003E\n\t/// Material icon for a file, by extension.\n\t/// \u003C/summary\u003E\n\tpublic static string FileIcon( string path ) =\u003E System.IO.Path.GetExtension( path ).ToLowerInvariant() switch\n\t{\n\t\t\u0022.cs\u0022 =\u003E \u0022code\u0022,\n\t\t\u0022.razor\u0022 =\u003E \u0022web\u0022,\n\t\t\u0022.scss\u0022 or \u0022.css\u0022 =\u003E \u0022palette\u0022,\n\t\t\u0022.json\u0022 =\u003E \u0022data_object\u0022,\n\t\t\u0022.sbproj\u0022 or \u0022.csproj\u0022 =\u003E \u0022settings\u0022,\n\t\t\u0022.sln\u0022 or \u0022.slnx\u0022 =\u003E \u0022account_tree\u0022,\n\t\t\u0022.shader\u0022 or \u0022.hlsl\u0022 =\u003E \u0022gradient\u0022,\n\t\t\u0022.md\u0022 or \u0022.txt\u0022 =\u003E \u0022notes\u0022,\n\t\t_ =\u003E \u0022description\u0022\n\t};\n\n\tpublic static Color FileIconColor( string path ) =\u003E System.IO.Path.GetExtension( path ).ToLowerInvariant() switch\n\t{\n\t\t\u0022.cs\u0022 =\u003E Theme.Blue,\n\t\t\u0022.razor\u0022 =\u003E Theme.Pink,\n\t\t\u0022.scss\u0022 or \u0022.css\u0022 =\u003E Theme.Pink,\n\t\t\u0022.json\u0022 =\u003E Theme.Yellow,\n\t\t\u0022.sbproj\u0022 or \u0022.csproj\u0022 or \u0022.sln\u0022 or \u0022.slnx\u0022 =\u003E Theme.Green,\n\t\t\u0022.shader\u0022 or \u0022.hlsl\u0022 =\u003E Theme.Green,\n\t\t_ =\u003E Theme.TextLight\n\t};\n}\n\n/// \u003Csummary\u003E\n/// A row of panel tabs (like \u0022Problems\u0022 / \u0022Output\u0022) painted to match s\u0026amp;box\u0027s tabs, with an optional\n/// count badge per tab.\n/// \u003C/summary\u003E\npublic class PanelTabBar : Widget\n{\n\tprivate readonly System.Collections.Generic.List\u003C(string Key, string Text, string Icon)\u003E _tabs = new();\n\tprivate readonly System.Collections.Generic.Dictionary\u003Cstring, (string Text, Color Color)\u003E _badges = new();\n\tprivate int _hovered = -1;\n\n\tpublic string Selected { get; private set; }\n\tpublic Action\u003Cstring\u003E OnSelected;\n\n\tpublic PanelTabBar( Widget parent ) : base( parent )\n\t{\n\t\tFixedHeight = HotCodeStyle.TabHeight;\n\t\tMouseTracking = true;\n\t}\n\n\tpublic void AddTab( string key, string text, string icon )\n\t{\n\t\t_tabs.Add( (key, text, icon) );\n\t\tSelected ??= key;\n\t\tUpdate();\n\t}\n\n\tpublic void Select( string key )\n\t{\n\t\tif ( Selected == key ) return;\n\t\tSelected = key;\n\t\tUpdate();\n\t\tOnSelected?.Invoke( key );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A small count shown after a tab\u0027s name (null or empty clears it).\n\t/// \u003C/summary\u003E\n\tpublic void SetBadge( string key, string text, Color color )\n\t{\n\t\tif ( string.IsNullOrEmpty( text ) ) _badges.Remove( key );\n\t\telse _badges[key] = (text, color);\n\t\tUpdate();\n\t}\n\n\tprivate float TabWidth( int i )\n\t{\n\t\tHotCodeStyle.SetTabFont();\n\t\tvar width = Paint.MeasureText( _tabs[i].Text ).x \u002B 20 \u002B 22;\n\t\tif ( _badges.TryGetValue( _tabs[i].Key, out var badge ) )\n\t\t{\n\t\t\tPaint.SetDefaultFont( 7, 600 );\n\t\t\twidth \u002B= Paint.MeasureText( badge.Text ).x \u002B 14;\n\t\t}\n\t\treturn width;\n\t}\n\n\tprivate int TabAt( Vector2 pos )\n\t{\n\t\tfloat x = 4;\n\t\tfor ( int i = 0; i \u003C _tabs.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar w = TabWidth( i );\n\t\t\tif ( pos.x \u003E= x \u0026\u0026 pos.x \u003C x \u002B w ) return i;\n\t\t\tx \u002B= w \u002B 2;\n\t\t}\n\t\treturn -1;\n\t}\n\n\tprotected override void OnMouseMove( MouseEvent e )\n\t{\n\t\tbase.OnMouseMove( e );\n\t\tvar i = TabAt( e.LocalPosition );\n\t\tif ( i != _hovered ) { _hovered = i; Update(); }\n\t}\n\n\tprotected override void OnMouseLeave()\n\t{\n\t\tbase.OnMouseLeave();\n\t\t_hovered = -1;\n\t\tUpdate();\n\t}\n\n\tprotected override void OnMousePress( MouseEvent e )\n\t{\n\t\tbase.OnMousePress( e );\n\t\tvar i = TabAt( e.LocalPosition );\n\t\tif ( i \u003E= 0 \u0026\u0026 e.LeftMouseButton ) Select( _tabs[i].Key );\n\t}\n\n\tprotected override void OnPaint()\n\t{\n\t\tPaint.ClearPen();\n\t\tPaint.SetBrush( Theme.TabBarBackground );\n\t\tPaint.DrawRect( LocalRect );\n\n\t\tfloat x = 4;\n\t\tfor ( int i = 0; i \u003C _tabs.Count; i\u002B\u002B )\n\t\t{\n\t\t\tvar tab = _tabs[i];\n\t\t\tvar width = TabWidth( i );\n\t\t\tvar selected = tab.Key == Selected;\n\t\t\tvar hovered = i == _hovered;\n\n\t\t\tvar content = HotCodeStyle.DrawTab( new Rect( x, 2, width, Height - 2 ), selected, hovered );\n\t\t\tvar color = HotCodeStyle.TabTextColor( selected, hovered );\n\n\t\t\tPaint.SetPen( color );\n\t\t\tPaint.DrawIcon( new Rect( content.Left, content.Top, 16, content.Height ), tab.Icon, 14, TextFlag.LeftCenter );\n\n\t\t\tHotCodeStyle.SetTabFont();\n\t\t\tvar textRect = new Rect( content.Left \u002B 20, content.Top, content.Width - 20, content.Height );\n\t\t\tPaint.DrawText( textRect, tab.Text, TextFlag.LeftCenter );\n\n\t\t\tif ( _badges.TryGetValue( tab.Key, out var badge ) )\n\t\t\t{\n\t\t\t\tvar textWidth = Paint.MeasureText( tab.Text ).x;\n\t\t\t\tPaint.SetDefaultFont( 7, 600 );\n\t\t\t\tvar badgeWidth = Paint.MeasureText( badge.Text ).x \u002B 10;\n\t\t\t\tvar badgeRect = new Rect( textRect.Left \u002B textWidth \u002B 6, content.Center.y - 7, badgeWidth, 14 );\n\n\t\t\t\tPaint.ClearPen();\n\t\t\t\tPaint.SetBrush( badge.Color.WithAlpha( 0.25f ) );\n\t\t\t\tPaint.DrawRect( badgeRect, 7 );\n\t\t\t\tPaint.SetPen( badge.Color );\n\t\t\t\tPaint.DrawText( badgeRect, badge.Text, TextFlag.Center );\n\t\t\t}\n\n\t\t\tx \u002B= width \u002B 2;\n\t\t}\n\t}\n}\n\n/// \u003Csummary\u003E\n/// A short vertical line between status bar items.\n/// \u003C/summary\u003E\npublic class StatusDivider : Widget\n{\n\tpublic StatusDivider( Widget parent ) : base( parent )\n\t{\n\t\tFixedWidth = 9;\n\t\tFixedHeight = 20;\n\t}\n\n\tprotected override void OnPaint()\n\t{\n\t\tPaint.SetPen( Theme.Border );\n\t\tPaint.DrawLine( new Vector2( 4, 4 ), new Vector2( 4, Height - 4 ) );\n\t}\n}\n\n/// \u003Csummary\u003E\n/// A clickable piece of text (with an optional icon) for the status bar.\n/// \u003C/summary\u003E\npublic class StatusItem : Widget\n{\n\tprivate string _text = \u0022\u0022;\n\tprivate string _icon;\n\tprivate Color? _iconColor;\n\n\tpublic Action Clicked;\n\n\tpublic StatusItem( Widget parent, string tooltip = null ) : base( parent )\n\t{\n\t\tToolTip = tooltip;\n\t\tMouseTracking = true;\n\t\tFixedHeight = 20;\n\t}\n\n\tpublic void Set( string text, string icon = null, Color? iconColor = null )\n\t{\n\t\tif ( _text == text \u0026\u0026 _icon == icon \u0026\u0026 Nullable.Equals( _iconColor, iconColor ) ) return;\n\n\t\t_text = text ?? \u0022\u0022;\n\t\t_icon = icon;\n\t\t_iconColor = iconColor;\n\n\t\tPaint.SetDefaultFont( HotCodeStyle.FontSize );\n\t\tFixedWidth = Paint.MeasureText( _text ).x \u002B (icon is null ? 0 : 18) \u002B 14;\n\t\tVisible = _text.Length \u003E 0 || icon is not null;\n\t\tUpdate();\n\t}\n\n\tprotected override void OnMouseEnter()\n\t{\n\t\tbase.OnMouseEnter();\n\t\tCursor = Clicked is null ? CursorShape.Arrow : CursorShape.Finger;\n\t\tUpdate();\n\t}\n\n\tprotected override void OnMouseLeave()\n\t{\n\t\tbase.OnMouseLeave();\n\t\tUpdate();\n\t}\n\n\tprotected override void OnMousePress( MouseEvent e )\n\t{\n\t\tbase.OnMousePress( e );\n\t\tif ( e.LeftMouseButton ) Clicked?.Invoke();\n\t}\n\n\tprotected override void OnPaint()\n\t{\n\t\tif ( Clicked is not null \u0026\u0026 IsUnderMouse )\n\t\t{\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.SurfaceBackground );\n\t\t\tPaint.DrawRect( LocalRect, Theme.ControlRadius );\n\t\t}\n\n\t\tvar x = 7f;\n\t\tif ( _icon is not null )\n\t\t{\n\t\t\tPaint.SetPen( _iconColor ?? Theme.TextLight );\n\t\t\tPaint.DrawIcon( new Rect( x, 0, 16, Height ), _icon, 13, TextFlag.LeftCenter );\n\t\t\tx \u002B= 18;\n\t\t}\n\n\t\tPaint.SetDefaultFont( HotCodeStyle.FontSize );\n\t\tPaint.SetPen( IsUnderMouse \u0026\u0026 Clicked is not null ? Theme.Text : Theme.TextLight );\n\t\tPaint.DrawText( new Rect( x, 0, Width - x, Height ), _text, TextFlag.LeftCenter );\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":".obj/__compiler_extra.cs","FileName":"__compiler_extra.cs","PackageType":"library","CodeKind":"Game","AssetVersionId":399294,"IsPrivate":false,"Code":"global using static Sandbox.Internal.GlobalGameNamespace;\r\nglobal using Microsoft.AspNetCore.Components;\r\nglobal using Microsoft.AspNetCore.Components.Rendering;\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonTitle\u0022, \u0022HotCode\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022AddonIdent\u0022, \u0022hotcode\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022OrgIdent\u0022, \u0022chrinteractive\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022Ident\u0022, \u0022chrinteractive.hotcode\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineVersion\u0022, \u002229\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022EngineMinorVersion\u0022, \u00221\u0022 )]\r\n\r\n[assembly: System.Runtime.Versioning.TargetFramework( \u0022.NETCoreApp,Version=v9.0\u0022, FrameworkDisplayName = \u0022.NET 9.0\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyMetadata( \u0022CompileTime\u0022, \u00222026-10-02T00:57:09.8934384Z\u0022 )]\r\n[assembly: global::System.Reflection.AssemblyVersion(\u00220.0.113.0\u0022)]\r\n[assembly: global::System.Reflection.AssemblyFileVersion(\u00220.0.113.0\u0022)]"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/HotCodeEditor.cs","FileName":"HotCodeEditor.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text;\nusing System.Threading.Tasks;\nusing Microsoft.CodeAnalysis;\n\n/// \u003Csummary\u003E\n/// The HotCode IDE window: file tree on the left, tabbed code editors on the right,\n/// problems and output underneath. Saving a file inside a project s\u0026amp;box has loaded\n/// lets s\u0026amp;box pick it up and hotload it.\n/// \u003C/summary\u003E\npublic class HotCodeEditor : Window\n{\n\tprivate readonly HotCodeEntry _entry;\n\tprivate readonly string _root;\n\tprivate readonly TreeView _tree;\n\tprivate readonly Widget _explorer;\n\tprivate CodeFileNode _rootNode;\n\tprivate readonly EditorTabBar _tabs;\n\tprivate readonly Widget _editorStack;\n\tprivate readonly WelcomePage _welcome;\n\n\t// Status bar\n\tprivate readonly StatusItem _buildStatus;\n\tprivate readonly StatusItem _position;\n\tprivate readonly StatusItem _indent;\n\tprivate readonly StatusItem _lineEnding;\n\tprivate readonly StatusItem _encoding;\n\tprivate readonly StatusItem _language;\n\tprivate bool _projectLoaded;\n\tprivate double _nextLoadedCheck;\n\tprivate readonly OutputPanel _output;\n\n\tprivate readonly List\u003CEditorDocument\u003E _documents = new();\n\tprivate EditorDocument _active;\n\n\t// Tree nodes by folder path, so file system changes can refresh the right folder\n\tprivate readonly Dictionary\u003Cstring, CodeFileNode\u003E _nodes = new( StringComparer.OrdinalIgnoreCase );\n\n\t// Go to definition runs in the background; results come back through this queue\n\tprivate readonly ConcurrentQueue\u003C(EditorDocument Doc, DefinitionResult Result)\u003E _definitionResults = new();\n\tprivate bool _definitionPending;\n\n\t// Autocomplete and parameter hints run in the background, one at a time, newest request wins\n\tprivate readonly LatestOnlyWorker\u003CCompletionResult\u003E _completions = new();\n\tprivate readonly LatestOnlyWorker\u003CSignatureHelpResult\u003E _signatures = new();\n\n\t// Alt\u002BLeft / Alt\u002BRight history of where we jumped from\n\tprivate record NavPoint( string Path, TextPos Pos );\n\tprivate readonly List\u003CNavPoint\u003E _navBack = new();\n\tprivate readonly List\u003CNavPoint\u003E _navForward = new();\n\n\tprivate readonly FileSystemWatcher _watcher;\n\t// (path, structural): structural changes (add/delete/rename) refresh the tree; edits only reload open files\n\tprivate readonly ConcurrentQueue\u003C(string Path, bool Structural)\u003E _changedPaths = new();\n\n\tpublic HotCodeEditor( HotCodeEntry entry )\n\t{\n\t\t_entry = entry;\n\t\t_root = Path.GetFullPath( entry.Directory );\n\n\t\tDeleteOnClose = true;\n\t\tSetWindowIcon( HotCodeLogo.WindowIcon );\n\t\tSize = new Vector2( 1200, 800 );\n\t\tMinimumSize = new Vector2( 600, 400 );\n\n\t\tCanvas = new Widget( this );\n\t\tCanvas.Layout = Layout.Column();\n\n\t\t// Top: tree | editors. Bottom: problems / output.\n\t\tvar vertical = new Splitter( Canvas );\n\t\tvertical.IsVertical = true;\n\t\tCanvas.Layout.Add( vertical, 1 );\n\n\t\tvar splitter = new Splitter( vertical );\n\t\tsplitter.IsHorizontal = true;\n\n\t\t// LEFT: explorer header \u002B file tree. Single click previews, double click opens for keeps.\n\t\t_explorer = new Widget( splitter );\n\t\t_explorer.Layout = Layout.Column();\n\t\t_explorer.Layout.Add( BuildExplorerHeader( _explorer, entry.Title ) );\n\t\t_explorer.Layout.AddSeparator( 1, Theme.Border );\n\n\t\t_tree = _explorer.Layout.Add( new TreeView( _explorer ), 1 );\n\t\t_tree.MinimumWidth = 200;\n\t\t_tree.ItemSelected \u002B= item =\u003E { if ( item is CodeFileNode { IsFolder: false } node ) OpenFile( node.FullPath, preview: true ); };\n\t\t_tree.ItemActivated \u002B= item =\u003E { if ( item is CodeFileNode { IsFolder: false } node ) OpenFile( node.FullPath, preview: false ); };\n\n\t\t_rootNode = new CodeFileNode( this, new DirectoryInfo( _root ) );\n\t\t_tree.AddItem( _rootNode );\n\t\t_tree.Open( _rootNode );\n\n\t\t// RIGHT: tabs \u002B editors\n\t\tvar right = new Widget( splitter );\n\t\tright.Layout = Layout.Column();\n\n\t\t_tabs = right.Layout.Add( new EditorTabBar( right ) );\n\t\tright.Layout.AddSeparator( 1, Theme.Border );\n\t\t_tabs.OnTabSelected = Activate;\n\t\t_tabs.OnTabCloseRequested = RequestClose;\n\t\t_tabs.OnTabPinned = doc =\u003E { doc.IsPreview = false; _tabs.Update(); };\n\t\t_tabs.GetTabTooltip = doc =\u003E Path.GetRelativePath( _root, doc.Path );\n\n\t\t_editorStack = right.Layout.Add( new Widget( right ), 1 );\n\t\t_editorStack.Layout = Layout.Column();\n\n\t\t_welcome = _editorStack.Layout.Add( new WelcomePage( _editorStack, entry.Title, _root, path =\u003E OpenFile( path, preview: false ) ), 1 );\n\t\t_welcome.SetRecentFiles( RecentFiles() );\n\n\t\tsplitter.AddWidget( _explorer );\n\t\tsplitter.AddWidget( right );\n\t\tsplitter.SetStretch( 0, 1 );\n\t\tsplitter.SetStretch( 1, 4 );\n\n\t\t// BOTTOM: problems / output\n\t\t_output = new OutputPanel( vertical, _root );\n\t\t_output.ProblemActivated = GoToProblem;\n\t\t_output.ProblemsChanged = _ =\u003E ApplyMarkers();\n\n\t\tvertical.AddWidget( splitter );\n\t\tvertical.AddWidget( _output );\n\t\tvertical.SetStretch( 0, 4 );\n\t\tvertical.SetStretch( 1, 1 );\n\n\t\t// Status bar: build state on the left, facts about the open file on the right\n\t\tif ( !StatusBar.IsValid() ) StatusBar = new StatusBar( this );\n\n\t\tvar flame = new LogoWidget( StatusBar, 14, badge: false ) { ToolTip = \u0022HotCode\u0022 };\n\t\tStatusBar.AddWidgetLeft( flame, 0 );\n\n\t\t_buildStatus = new StatusItem( StatusBar, \u0022Click to show problems\u0022 ) { Clicked = _output.ShowProblems };\n\t\tStatusBar.AddWidgetLeft( _buildStatus, 0 );\n\n\t\t_position = new StatusItem( StatusBar, \u0022Go to line (Ctrl\u002BG)\u0022 ) { Clicked = GoToLine };\n\t\t_indent = new StatusItem( StatusBar, \u0022Indentation, detected from the file\u0022 );\n\t\t_lineEnding = new StatusItem( StatusBar, \u0022Line endings, kept as they were when saving\u0022 );\n\t\t_encoding = new StatusItem( StatusBar, \u0022Encoding\u0022 );\n\t\t_language = new StatusItem( StatusBar, \u0022Language\u0022 );\n\n\t\tforeach ( var item in new[] { _position, _indent, _lineEnding, _encoding, _language } )\n\t\t{\n\t\t\tif ( item != _position ) StatusBar.AddWidgetRight( new StatusDivider( StatusBar ), 0 );\n\t\t\tStatusBar.AddWidgetRight( item, 0 );\n\t\t}\n\n\t\tBuildMenuBar();\n\n\t\t// Keep the tree (and unmodified open files) in sync with changes made outside HotCode\n\t\ttry\n\t\t{\n\t\t\t_watcher = new FileSystemWatcher( _root )\n\t\t\t{\n\t\t\t\tIncludeSubdirectories = true,\n\t\t\t\tNotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite\n\t\t\t};\n\t\t\t_watcher.Created \u002B= ( _, e ) =\u003E QueueChange( e.FullPath, structural: true );\n\t\t\t_watcher.Deleted \u002B= ( _, e ) =\u003E QueueChange( e.FullPath, structural: true );\n\t\t\t_watcher.Changed \u002B= ( _, e ) =\u003E QueueChange( e.FullPath, structural: false );\n\t\t\t_watcher.Renamed \u002B= ( _, e ) =\u003E { QueueChange( e.OldFullPath, structural: true ); QueueChange( e.FullPath, structural: true ); };\n\t\t\t_watcher.EnableRaisingEvents = true;\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or ArgumentException or UnauthorizedAccessException )\n\t\t{\n\t\t\tLog.Warning( $\u0022[HotCode] Can\u0027t watch {_root} for changes: {e.Message}\u0022 );\n\t\t}\n\n\t\tUpdateTitle();\n\t\tShow();\n\t}\n\n\tpublic override void OnDestroyed()\n\t{\n\t\t_watcher?.Dispose();\n\t\tbase.OnDestroyed();\n\t}\n\n\t// --- OPENING / SWITCHING ---\n\n\tprivate EditorDocument FindDocument( string path ) =\u003E _documents.FirstOrDefault( d =\u003E string.Equals( d.Path, path, StringComparison.OrdinalIgnoreCase ) );\n\n\tprivate EditorDocument OpenFile( string path, bool preview )\n\t{\n\t\tvar existing = FindDocument( path );\n\t\tif ( existing is not null )\n\t\t{\n\t\t\tif ( !preview ) existing.IsPreview = false;\n\t\t\tActivate( existing );\n\t\t\treturn existing;\n\t\t}\n\n\t\tif ( !TryReadFile( path, out var contents, out var hasBom ) )\n\t\t\treturn null;\n\n\t\tvar editor = new CodeView( _editorStack, SyntaxLanguage.FromPath( path ) );\n\t\teditor.Text = contents;\n\t\teditor.Visible = false;\n\t\t_editorStack.Layout.Add( editor, 1 );\n\n\t\tvar doc = new EditorDocument( path, editor ) { IsPreview = preview, HasBom = hasBom };\n\t\teditor.TextChanged \u002B= () =\u003E OnEdited( doc );\n\t\teditor.DefinitionRequested = pos =\u003E GoToDefinition( doc, pos );\n\t\teditor.NavigateRequested = Navigate;\n\t\teditor.CompletionRequested = pos =\u003E RequestCompletion( doc, pos );\n\t\teditor.SignatureHelpRequested = pos =\u003E RequestSignatureHelp( doc, pos );\n\n\t\t// A new preview takes the place of the old one, if the old one wasn\u0027t touched\n\t\tvar oldPreview = preview ? _documents.FirstOrDefault( d =\u003E d.IsPreview \u0026\u0026 !d.IsDirty ) : null;\n\t\tif ( oldPreview is not null )\n\t\t{\n\t\t\tvar index = _documents.IndexOf( oldPreview );\n\t\t\tRemoveDocument( oldPreview );\n\t\t\t_documents.Insert( index, doc );\n\t\t\t_tabs.Insert( index, doc );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_documents.Add( doc );\n\t\t\t_tabs.Add( doc );\n\t\t}\n\n\t\tApplyMarkers( doc );\n\t\tActivate( doc );\n\t\treturn doc;\n\t}\n\n\tprivate static bool TryReadFile( string path, out string contents, out bool hasBom )\n\t{\n\t\tcontents = null;\n\t\thasBom = false;\n\n\t\ttry\n\t\t{\n\t\t\tvar bytes = File.ReadAllBytes( path );\n\t\t\thasBom = bytes.Length \u003E= 3 \u0026\u0026 bytes[0] == 0xEF \u0026\u0026 bytes[1] == 0xBB \u0026\u0026 bytes[2] == 0xBF;\n\t\t\tcontents = new UTF8Encoding( false ).GetString( bytes, hasBom ? 3 : 0, bytes.Length - (hasBom ? 3 : 0) );\n\t\t\treturn true;\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or UnauthorizedAccessException )\n\t\t{\n\t\t\tLog.Warning( $\u0022[HotCode] Couldn\u0027t open {path}: {e.Message}\u0022 );\n\t\t\treturn false;\n\t\t}\n\t}\n\n\tprivate void Activate( EditorDocument doc )\n\t{\n\t\tif ( _active is not null \u0026\u0026 _active != doc \u0026\u0026 _active.IsDirty \u0026\u0026 HotCodeSettings.AutoSave == AutoSaveMode.WhenSwitchingTabs )\n\t\t\tSaveDocument( _active );\n\n\t\tif ( _active is not null \u0026\u0026 _active != doc )\n\t\t\t_active.Editor.Visible = false;\n\n\t\t_active = doc;\n\t\t_welcome.Visible = doc is null;\n\n\t\tif ( doc is not null )\n\t\t{\n\t\t\tdoc.Editor.Visible = true;\n\t\t\tdoc.Editor.FocusEditor();\n\t\t\tif ( !doc.IsPreview ) AddRecentFile( doc.Path );\n\t\t}\n\t\telse\n\t\t{\n\t\t\t_welcome.SetRecentFiles( RecentFiles() );\n\t\t}\n\n\t\t_tabs.SetActive( doc );\n\t\t_tree.Update();\n\t\tUpdateTitle();\n\t}\n\n\tprivate void OnEdited( EditorDocument doc )\n\t{\n\t\t// IsModified goes back to false when edits are undone back to the saved text\n\t\tvar dirty = doc.Editor.IsModified;\n\t\tvar changed = dirty != doc.IsDirty || (dirty \u0026\u0026 doc.IsPreview);\n\t\tdoc.LastEdit = RealTime.Now;\n\t\tdoc.IsDirty = dirty;\n\t\tif ( dirty ) doc.IsPreview = false;\n\n\t\tif ( changed )\n\t\t{\n\t\t\t_tabs.Update();\n\t\t\t_tree.Update();\n\t\t\tUpdateTitle();\n\t\t}\n\t}\n\n\t// --- PROBLEMS ---\n\n\tprivate void GoToProblem( ProblemItem item )\n\t{\n\t\tPushNavPoint();\n\t\tvar doc = OpenFile( item.Path, preview: false );\n\t\tdoc?.Editor.GoTo( item.Line, item.Column );\n\t}\n\n\tprivate void ApplyMarkers()\n\t{\n\t\tforeach ( var doc in _documents )\n\t\t\tApplyMarkers( doc );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Underlines this file\u0027s compile errors and warnings.\n\t/// \u003C/summary\u003E\n\tprivate void ApplyMarkers( EditorDocument doc )\n\t{\n\t\tvar markers = _output.Problems\n\t\t\t.Where( p =\u003E string.Equals( Path.GetFullPath( p.Path ), Path.GetFullPath( doc.Path ), StringComparison.OrdinalIgnoreCase ) )\n\t\t\t.Select( p =\u003E new EditorMarker( new TextPos( p.Line, p.Column ), new TextPos( p.EndLine, p.EndColumn ), p.Severity == DiagnosticSeverity.Error, $\u0022{p.Id}: {p.Message}\u0022 ) )\n\t\t\t.ToList();\n\n\t\tdoc.Editor.SetMarkers( markers );\n\t}\n\n\t// --- GO TO DEFINITION ---\n\n\tprivate void GoToDefinition( EditorDocument doc, TextPos pos )\n\t{\n\t\tif ( !doc.Path.EndsWith( \u0022.cs\u0022, StringComparison.OrdinalIgnoreCase ) )\n\t\t{\n\t\t\tdoc.Editor.ShowMessage( \u0022Go to Definition only works in C# files.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( _definitionPending ) return;\n\n\t\t// Read s\u0026box\u0027s compiler state here on the main thread; the slow Roslyn work happens in the background\n\t\tRoslynProject.Snapshot snapshot;\n\t\ttry\n\t\t{\n\t\t\tsnapshot = RoslynProject.TakeSnapshot( doc.Path, _root, _documents.Select( d =\u003E (d.Path, d.Editor.Text) ) );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tdoc.Editor.ShowMessage( $\u0022Couldn\u0027t look that up: {e.Message}\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\t_definitionPending = true;\n\t\tdoc.Editor.ShowMessage( \u0022Finding definition...\u0022, 60 );\n\n\t\tTask.Run( () =\u003E DefinitionFinder.Find( snapshot, pos.Line, pos.Col ) )\n\t\t\t.ContinueWith( t =\u003E _definitionResults.Enqueue( (doc, t.IsFaulted ? DefinitionResult.Fail( t.Exception?.InnerException?.Message ) : t.Result) ) );\n\t}\n\n\t// --- AUTOCOMPLETE ---\n\n\t/// \u003Csummary\u003E\n\t/// What Roslyn needs to know about the code right now. Read on the main thread, used in the background.\n\t/// \u003C/summary\u003E\n\tprivate RoslynProject.Snapshot TakeSnapshot( EditorDocument doc )\n\t{\n\t\tif ( !doc.Path.EndsWith( \u0022.cs\u0022, StringComparison.OrdinalIgnoreCase ) ) return null;\n\n\t\ttry\n\t\t{\n\t\t\treturn RoslynProject.TakeSnapshot( doc.Path, _root, _documents.Select( d =\u003E (d.Path, d.Editor.Text) ) );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022[HotCode] Couldn\u0027t read the project for code help: {e.Message}\u0022 );\n\t\t\treturn null;\n\t\t}\n\t}\n\n\tprivate void RequestCompletion( EditorDocument doc, TextPos pos )\n\t{\n\t\tvar snapshot = TakeSnapshot( doc );\n\t\tif ( snapshot is null ) return;\n\n\t\t_completions.Request( () =\u003E CompletionProvider.Get( snapshot, pos.Line, pos.Col ), result =\u003E\n\t\t{\n\t\t\tif ( result.Error is not null ) Log.Warning( $\u0022[HotCode] Autocomplete failed: {result.Error}\u0022 );\n\t\t\telse if ( _documents.Contains( doc ) ) doc.Editor.ShowCompletions( result );\n\t\t} );\n\t}\n\n\t// --- PARAMETER HINTS ---\n\n\tprivate void RequestSignatureHelp( EditorDocument doc, TextPos pos )\n\t{\n\t\tvar snapshot = TakeSnapshot( doc );\n\t\tif ( snapshot is null ) return;\n\n\t\t_signatures.Request( () =\u003E SignatureHelpProvider.Get( snapshot, pos.Line, pos.Col ), result =\u003E\n\t\t{\n\t\t\tif ( _documents.Contains( doc ) ) doc.Editor.ShowSignatureHelp( result );\n\t\t} );\n\t}\n\n\tprivate void ShowDefinitionResult( EditorDocument doc, DefinitionResult result )\n\t{\n\t\t_definitionPending = false;\n\t\tvar open = _documents.Contains( doc );\n\n\t\tif ( result.Error is not null )\n\t\t{\n\t\t\tif ( open ) doc.Editor.ShowMessage( result.Error );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( result.Locations.Count == 0 )\n\t\t{\n\t\t\tif ( open ) doc.Editor.ShowMessage( $\u0022\u0027{result.Symbol}\u0027 is defined in {result.Assembly ?? \u0022a compiled library\u0022}, which has no source here.\u0022, 6 );\n\t\t\treturn;\n\t\t}\n\n\t\tif ( open ) doc.Editor.ShowMessage( \u0022\u0022, 0 );\n\n\t\tif ( result.Locations.Count == 1 )\n\t\t{\n\t\t\tJumpTo( result.Locations[0] );\n\t\t\treturn;\n\t\t}\n\n\t\t// Several (partial classes): let the user pick\n\t\tvar menu = new ContextMenu( this );\n\t\tforeach ( var location in result.Locations )\n\t\t{\n\t\t\tvar label = $\u0022{Path.GetRelativePath( _root, location.Path )}:{location.Line \u002B 1}    {location.Preview}\u0022;\n\t\t\tmenu.AddOption( label, \u0022code\u0022, () =\u003E JumpTo( location ) );\n\t\t}\n\t\tmenu.OpenAtCursor();\n\t}\n\n\tprivate void JumpTo( DefinitionLocation location )\n\t{\n\t\tPushNavPoint();\n\t\tvar target = OpenFile( location.Path, preview: false );\n\t\ttarget?.Editor.GoTo( location.Line, location.Column );\n\t}\n\n\tprivate void PushNavPoint()\n\t{\n\t\tif ( _active is null ) return;\n\n\t\t_navBack.Add( new NavPoint( _active.Path, _active.Editor.CaretPosition ) );\n\t\tif ( _navBack.Count \u003E 100 ) _navBack.RemoveAt( 0 );\n\t\t_navForward.Clear();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Alt\u002BLeft / Alt\u002BRight: back to where we jumped from, and forward again.\n\t/// \u003C/summary\u003E\n\tprivate void Navigate( bool back )\n\t{\n\t\tvar from = back ? _navBack : _navForward;\n\t\tvar to = back ? _navForward : _navBack;\n\n\t\t// Skip spots in files that have since been deleted\n\t\twhile ( from.Count \u003E 0 \u0026\u0026 !File.Exists( from[^1].Path ) ) from.RemoveAt( from.Count - 1 );\n\t\tif ( from.Count == 0 ) return;\n\n\t\tvar target = from[^1];\n\t\tfrom.RemoveAt( from.Count - 1 );\n\t\tif ( _active is not null ) to.Add( new NavPoint( _active.Path, _active.Editor.CaretPosition ) );\n\n\t\tvar doc = OpenFile( target.Path, preview: false );\n\t\tdoc?.Editor.GoTo( target.Pos.Line, target.Pos.Col );\n\t}\n\n\t[Shortcut( \u0022hotcode.toggle-output\u0022, \u0022CTRL\u002BJ\u0022 )]\n\tpublic void ToggleOutput() =\u003E _output.Visible = !_output.Visible;\n\n\t// --- FILE TREE ---\n\n\tinternal FileState StateOf( string path )\n\t{\n\t\tvar doc = FindDocument( path );\n\t\treturn doc is null ? FileState.Closed : doc == _active ? FileState.Active : FileState.Open;\n\t}\n\n\tinternal bool IsUnsaved( string path ) =\u003E FindDocument( path )?.IsDirty == true;\n\n\t// --- MENU BAR ---\n\n\t/// \u003Csummary\u003E\n\t/// File / Edit / View / Go / Project / Help. Shortcuts are shown after a tab (\u0022Save\\tCtrl\u002BS\u0022), which\n\t/// Qt displays right-aligned without registering a second key binding next to the ones the editor handles.\n\t/// Menus that show on/off state rebuild each time they open.\n\t/// \u003C/summary\u003E\n\tprivate void BuildMenuBar()\n\t{\n\t\tMenuBar.Clear();\n\n\t\tvar file = MenuBar.AddMenu( \u0022File\u0022 );\n\t\tfile.AddOption( \u0022New File in Code Folder...\u0022, \u0022note_add\u0022, () =\u003E NewFileInCodeFolder( component: false ) );\n\t\tfile.AddOption( \u0022New C# Component...\u0022, \u0022code\u0022, () =\u003E NewFileInCodeFolder( component: true ) );\n\t\tfile.AddOption( \u0022Open File...\u0022, \u0022file_open\u0022, OpenFileDialog );\n\t\tfile.AddSeparator();\n\t\tfile.AddOption( \u0022Save\\tCtrl\u002BS\u0022, \u0022save\u0022, Save );\n\t\tfile.AddOption( \u0022Save All\\tCtrl\u002BShift\u002BS\u0022, \u0022save_as\u0022, SaveAll );\n\t\tfile.AddSeparator();\n\t\tfile.AddOption( \u0022Close Tab\\tCtrl\u002BW\u0022, \u0022close\u0022, CloseActiveTab );\n\t\tfile.AddOption( \u0022Close Other Tabs\u0022, \u0022tab_unselected\u0022, CloseOtherTabs );\n\t\tfile.AddOption( \u0022Close All Tabs\u0022, \u0022clear_all\u0022, CloseAllTabs );\n\t\tfile.AddSeparator();\n\t\tfile.AddOption( \u0022Reveal Current File in Explorer\u0022, \u0022folder_open\u0022, () =\u003E { if ( _active is not null ) EditorUtility.OpenFileFolder( _active.Path ); } );\n\t\tfile.AddOption( \u0022Copy Current File Path\u0022, \u0022content_copy\u0022, () =\u003E { if ( _active is not null ) EditorUtility.Clipboard.Copy( _active.Path ); } );\n\t\tfile.AddSeparator();\n\t\tfile.AddOption( \u0022Settings...\\tCtrl\u002B,\u0022, \u0022settings\u0022, HotCodeSettingsWindow.Open );\n\t\tfile.AddOption( \u0022HotCode Launcher\u0022, HotCodeLogo.IconName, () =\u003E new HotCodeLauncher() );\n\t\tfile.AddOption( \u0022Close Window\u0022, \u0022logout\u0022, () =\u003E Close() );\n\n\t\tvar edit = MenuBar.AddMenu( \u0022Edit\u0022 );\n\t\tedit.AddOption( \u0022Undo\\tCtrl\u002BZ\u0022, \u0022undo\u0022, () =\u003E _active?.Editor.Undo() );\n\t\tedit.AddOption( \u0022Redo\\tCtrl\u002BY\u0022, \u0022redo\u0022, () =\u003E _active?.Editor.Redo() );\n\t\tedit.AddSeparator();\n\t\tedit.AddOption( \u0022Cut\\tCtrl\u002BX\u0022, \u0022content_cut\u0022, () =\u003E _active?.Editor.Cut() );\n\t\tedit.AddOption( \u0022Copy\\tCtrl\u002BC\u0022, \u0022content_copy\u0022, () =\u003E _active?.Editor.Copy() );\n\t\tedit.AddOption( \u0022Paste\\tCtrl\u002BV\u0022, \u0022content_paste\u0022, () =\u003E _active?.Editor.Paste() );\n\t\tedit.AddOption( \u0022Select All\\tCtrl\u002BA\u0022, \u0022select_all\u0022, () =\u003E _active?.Editor.SelectAll() );\n\t\tedit.AddSeparator();\n\t\tedit.AddOption( \u0022Find\\tCtrl\u002BF\u0022, \u0022search\u0022, () =\u003E _active?.Editor.OpenFind( replace: false ) );\n\t\tedit.AddOption( \u0022Replace\\tCtrl\u002BH\u0022, \u0022find_replace\u0022, () =\u003E _active?.Editor.OpenFind( replace: true ) );\n\t\tedit.AddSeparator();\n\t\tedit.AddOption( \u0022Toggle Comment\\tCtrl\u002B/\u0022, \u0022comment\u0022, () =\u003E _active?.Editor.ToggleComment() );\n\t\tedit.AddOption( \u0022Duplicate Line\\tCtrl\u002BD\u0022, \u0022content_copy\u0022, () =\u003E _active?.Editor.DuplicateLines() );\n\t\tedit.AddOption( \u0022Move Line Up\\tAlt\u002BUp\u0022, \u0022arrow_upward\u0022, () =\u003E _active?.Editor.MoveLines( -1 ) );\n\t\tedit.AddOption( \u0022Move Line Down\\tAlt\u002BDown\u0022, \u0022arrow_downward\u0022, () =\u003E _active?.Editor.MoveLines( 1 ) );\n\t\tedit.AddSeparator();\n\t\tedit.AddOption( \u0022Trigger Suggestions\\tCtrl\u002BSpace\u0022, \u0022auto_awesome\u0022, () =\u003E _active?.Editor.RequestCompletionAtCaret() );\n\t\tedit.AddOption( \u0022Parameter Hints\\tCtrl\u002BShift\u002BSpace\u0022, \u0022info\u0022, () =\u003E _active?.Editor.RequestSignatureHelpAtCaret() );\n\n\t\tvar view = MenuBar.AddMenu( \u0022View\u0022 );\n\t\tview.AboutToShow \u002B= () =\u003E FillViewMenu( view );\n\n\t\tvar go = MenuBar.AddMenu( \u0022Go\u0022 );\n\t\tgo.AddOption( \u0022Go to Definition\\tF12\u0022, \u0022start\u0022, () =\u003E _active?.Editor.RequestDefinitionAtCaret() );\n\t\tgo.AddOption( \u0022Go to Line...\\tCtrl\u002BG\u0022, \u0022format_list_numbered\u0022, GoToLine );\n\t\tgo.AddSeparator();\n\t\tgo.AddOption( \u0022Back\\tAlt\u002BLeft\u0022, \u0022arrow_back\u0022, () =\u003E Navigate( back: true ) );\n\t\tgo.AddOption( \u0022Forward\\tAlt\u002BRight\u0022, \u0022arrow_forward\u0022, () =\u003E Navigate( back: false ) );\n\t\tgo.AddSeparator();\n\t\tgo.AddOption( \u0022Next Problem\\tF8\u0022, \u0022error\u0022, NextProblem );\n\t\tgo.AddOption( \u0022Previous Problem\\tShift\u002BF8\u0022, \u0022error_outline\u0022, PreviousProblem );\n\n\t\tvar project = MenuBar.AddMenu( \u0022Project\u0022 );\n\t\tproject.AboutToShow \u002B= () =\u003E FillProjectMenu( project );\n\n\t\tvar help = MenuBar.AddMenu( \u0022Help\u0022 );\n\t\thelp.AddOption( \u0022Welcome Page / Shortcuts\u0022, \u0022home\u0022, ShowWelcomePage );\n\t\thelp.AddOption( \u0022Settings...\u0022, \u0022settings\u0022, HotCodeSettingsWindow.Open );\n\t\thelp.AddOption( \u0022About HotCode\u0022, HotCodeLogo.IconName, () =\u003E\n\t\t\tnew PopupWindow( \u0022About HotCode\u0022, \u0022HotCode - a code editor built into s\u0026box.\\n\\nRoslyn-powered autocomplete, parameter hints and go to definition, with live compile errors from s\u0026box.\u0022, \u0022Close\u0022, new Dictionary\u003Cstring, Action\u003E() ).Show() );\n\t}\n\n\tprivate void FillProjectMenu( Menu project )\n\t{\n\t\tproject.Clear();\n\t\tproject.AddOption( \u0022Open Project Folder\u0022, \u0022folder_open\u0022, () =\u003E EditorUtility.OpenFolder( _root ) );\n\t\tproject.AddOption( \u0022Open in External Code Editor\u0022, \u0022launch\u0022, () =\u003E CodeEditor.OpenSolution() );\n\t\tproject.AddOption( \u0022Generate Solution\u0022, \u0022account_tree\u0022, () =\u003E\n\t\t{\n\t\t\tEditorUtility.Projects.GenerateSolution();\n\t\t\tLog.Info( \u0022[HotCode] Generated solution files\u0022 );\n\t\t} );\n\t\tproject.AddSeparator();\n\t\tproject.AddOption( \u0022Refresh File Tree\u0022, \u0022refresh\u0022, () =\u003E _rootNode.Dirty() );\n\t\tproject.AddSeparator();\n\n\t\tHotCodeInstallUi.AddMenuOptions( project, _entry, null );\n\t\tproject.AddOption( \u0022Add HotCode to Another Project...\u0022, HotCodeLogo.IconName, () =\u003E\n\t\t{\n\t\t\tvar fd = new FileDialog( null );\n\t\t\tfd.Title = \u0022Add HotCode to Project\u0022;\n\t\t\tfd.Directory = Path.GetDirectoryName( _root );\n\t\t\tfd.SetFindExistingFile();\n\t\t\tfd.SetModeOpen();\n\t\t\tfd.SetNameFilter( \u0022s\u0026box Project (*.sbproj)\u0022 );\n\t\t\tif ( fd.Execute() ) HotCodeInstallUi.AskInstall( HotCodeEntry.FromPath( fd.SelectedFile ) );\n\t\t} );\n\t}\n\n\tprivate void FillViewMenu( Menu view )\n\t{\n\t\tview.Clear();\n\n\t\tAddToggle( view, \u0022File Tree\u0022, \u0022account_tree\u0022, _explorer.Visible, on =\u003E _explorer.Visible = on );\n\t\tAddToggle( view, \u0022Bottom Panel\\tCtrl\u002BJ\u0022, \u0022dock_to_bottom\u0022, _output.Visible, on =\u003E _output.Visible = on );\n\t\tAddToggle( view, \u0022Status Bar\u0022, \u0022power_input\u0022, StatusBar.Visible, on =\u003E StatusBar.Visible = on );\n\t\tview.AddSeparator();\n\t\tAddToggle( view, \u0022Word Wrap\\tAlt\u002BZ\u0022, \u0022wrap_text\u0022, CodeView.WordWrap, on =\u003E CodeView.WordWrap = on );\n\t\tview.AddSeparator();\n\t\tview.AddOption( \u0022Zoom In\\tCtrl\u002B=\u0022, \u0022zoom_in\u0022, () =\u003E CodeView.FontSize \u002B= 1 );\n\t\tview.AddOption( \u0022Zoom Out\\tCtrl\u002B-\u0022, \u0022zoom_out\u0022, () =\u003E CodeView.FontSize -= 1 );\n\t\tview.AddOption( $\u0022Reset Zoom ({CodeView.FontSize:0}pt)\u0022, \u0022youtube_searched_for\u0022, () =\u003E CodeView.FontSize = CodeView.DefaultFontSize );\n\t\tview.AddSeparator();\n\t\tview.AddOption( \u0022Problems\u0022, \u0022error_outline\u0022, _output.ShowProblems );\n\t\tview.AddOption( \u0022Output\u0022, \u0022terminal\u0022, _output.ShowOutput );\n\t}\n\n\tprivate static void AddToggle( Menu menu, string text, string icon, bool on, Action\u003Cbool\u003E set )\n\t{\n\t\tvar option = menu.AddOption( text, icon );\n\t\toption.Checkable = true;\n\t\toption.Checked = on;\n\t\toption.Toggled \u002B= set;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// New files from the menu go in the project\u0027s Code folder (or the project root if there isn\u0027t one).\n\t/// \u003C/summary\u003E\n\tprivate void NewFileInCodeFolder( bool component )\n\t{\n\t\tvar code = Path.Combine( _root, \u0022Code\u0022 );\n\t\tvar folder = Directory.Exists( code ) ? code : _root;\n\n\t\tif ( component )\n\t\t\tAskCreate( folder, \u0022New Component\u0022, \u0022Component name:\u0022, \u0022MyComponent\u0022, name =\u003E ProjectFiles.CreateComponent( folder, name ) );\n\t\telse\n\t\t\tAskCreate( folder, \u0022New File\u0022, \u0022File name:\u0022, \u0022NewFile.cs\u0022, name =\u003E ProjectFiles.CreateFile( folder, name ) );\n\t}\n\n\tprivate void OpenFileDialog()\n\t{\n\t\tvar fd = new FileDialog( null );\n\t\tfd.Title = \u0022Open File\u0022;\n\t\tfd.Directory = _active is not null ? Path.GetDirectoryName( _active.Path ) : _root;\n\t\tfd.SetFindExistingFile();\n\t\tfd.SetModeOpen();\n\t\tif ( fd.Execute() ) OpenFile( fd.SelectedFile, preview: false );\n\t}\n\n\t[Shortcut( \u0022hotcode.settings\u0022, \u0022CTRL\u002B,\u0022 )]\n\tpublic void OpenSettings() =\u003E HotCodeSettingsWindow.Open();\n\n\t[Shortcut( \u0022hotcode.save-all\u0022, \u0022CTRL\u002BSHIFT\u002BS\u0022 )]\n\tpublic void SaveAll()\n\t{\n\t\tforeach ( var doc in _documents.Where( d =\u003E d.IsDirty ).ToList() )\n\t\t\tSaveDocument( doc );\n\t}\n\n\tprivate void CloseOtherTabs()\n\t{\n\t\tforeach ( var doc in _documents.Where( d =\u003E d != _active ).ToList() )\n\t\t\tRequestClose( doc );\n\t}\n\n\tprivate void CloseAllTabs()\n\t{\n\t\tforeach ( var doc in _documents.ToList() )\n\t\t\tRequestClose( doc );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Shows the welcome page without closing anything; clicking a tab goes back to it.\n\t/// \u003C/summary\u003E\n\tprivate void ShowWelcomePage()\n\t{\n\t\tif ( _active is not null ) _active.Editor.Visible = false;\n\t\t_active = null;\n\t\t_welcome.SetRecentFiles( RecentFiles() );\n\t\t_welcome.Visible = true;\n\t\t_tabs.SetActive( null );\n\t\t_tree.Update();\n\t}\n\n\t[Shortcut( \u0022hotcode.next-problem\u0022, \u0022F8\u0022 )]\n\tpublic void NextProblem() =\u003E StepProblem( 1 );\n\n\t[Shortcut( \u0022hotcode.previous-problem\u0022, \u0022SHIFT\u002BF8\u0022 )]\n\tpublic void PreviousProblem() =\u003E StepProblem( -1 );\n\n\tprivate int _problemIndex = -1;\n\n\tprivate void StepProblem( int direction )\n\t{\n\t\tvar problems = _output.Problems;\n\t\tif ( problems.Count == 0 ) return;\n\n\t\t_problemIndex = ((_problemIndex \u002B direction) % problems.Count \u002B problems.Count) % problems.Count;\n\t\tGoToProblem( problems[_problemIndex] );\n\t}\n\n\t// --- EXPLORER HEADER ---\n\n\tprivate Widget BuildExplorerHeader( Widget parent, string title )\n\t{\n\t\tvar header = new Widget( parent );\n\t\theader.FixedHeight = HotCodeStyle.TabHeight;\n\t\theader.OnPaintOverride = () =\u003E\n\t\t{\n\t\t\tPaint.ClearPen();\n\t\t\tPaint.SetBrush( Theme.TabBarBackground );\n\t\t\tPaint.DrawRect( header.LocalRect );\n\n\t\t\tPaint.SetDefaultFont( 7, 700 );\n\t\t\tPaint.SetPen( Theme.TextLight );\n\t\t\tPaint.DrawText( new Rect( 10, 0, header.Width - 120, header.Height ), Paint.GetElidedText( $\u0022EXPLORER  -  {title.ToUpperInvariant()}\u0022, header.Width - 130, ElideMode.Right ), TextFlag.LeftCenter );\n\t\t\treturn false;\n\t\t};\n\n\t\theader.Layout = Layout.Row();\n\t\theader.Layout.Margin = new Sandbox.UI.Margin( 0, 0, 4, 0 );\n\t\theader.Layout.AddStretchCell();\n\n\t\tvar rootFolder = Directory.Exists( Path.Combine( _root, \u0022Code\u0022 ) ) ? Path.Combine( _root, \u0022Code\u0022 ) : _root;\n\t\theader.Layout.Add( new IconButton( \u0022note_add\u0022, () =\u003E NewFileInCodeFolder( component: false ) ) { ToolTip = \u0022New File\u0022 } );\n\t\theader.Layout.Add( new IconButton( \u0022create_new_folder\u0022, () =\u003E AskCreate( rootFolder, \u0022New Folder\u0022, \u0022Folder name:\u0022, \u0022NewFolder\u0022, name =\u003E ProjectFiles.CreateFolder( rootFolder, name ) ) ) { ToolTip = \u0022New Folder\u0022 } );\n\t\theader.Layout.Add( new IconButton( \u0022refresh\u0022, () =\u003E _rootNode.Dirty() ) { ToolTip = \u0022Refresh\u0022 } );\n\t\theader.Layout.Add( new IconButton( \u0022unfold_less\u0022, () =\u003E\n\t\t{\n\t\t\t_tree.Close( _rootNode, recursive: true );\n\t\t\t_tree.Open( _rootNode );\n\t\t} ) { ToolTip = \u0022Collapse All\u0022 } );\n\n\t\treturn header;\n\t}\n\n\t// --- RECENT FILES ---\n\n\tprivate string RecentFilesCookie =\u003E \u0022hotcode.recentfiles.\u0022 \u002B _root.ToLowerInvariant().Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\n\tprivate List\u003Cstring\u003E RecentFiles() =\u003E EditorCookie.Get\u003CList\u003Cstring\u003E\u003E( RecentFilesCookie, null ) ?? new();\n\n\tprivate void AddRecentFile( string path )\n\t{\n\t\tvar files = RecentFiles();\n\t\tfiles.RemoveAll( f =\u003E string.Equals( f, path, StringComparison.OrdinalIgnoreCase ) );\n\t\tfiles.Insert( 0, path );\n\t\tif ( files.Count \u003E 20 ) files.RemoveRange( 20, files.Count - 20 );\n\t\tEditorCookie.Set( RecentFilesCookie, files );\n\t}\n\n\t// --- GO TO LINE ---\n\n\t/// \u003Csummary\u003E\n\t/// Ctrl\u002BG: \u002242\u0022 or \u002242:7\u0022 (line:column).\n\t/// \u003C/summary\u003E\n\t[Shortcut( \u0022hotcode.goto-line\u0022, \u0022CTRL\u002BG\u0022 )]\n\tpublic void GoToLine()\n\t{\n\t\tvar doc = _active;\n\t\tif ( doc is null ) return;\n\n\t\tvar current = doc.Editor.CaretPosition;\n\t\tDialog.AskString( text =\u003E\n\t\t{\n\t\t\tvar parts = text.Split( \u0027:\u0027, \u0027,\u0027 );\n\t\t\tif ( !int.TryParse( parts[0].Trim(), out var line ) ) return;\n\t\t\tvar column = parts.Length \u003E 1 \u0026\u0026 int.TryParse( parts[1].Trim(), out var c ) ? c : 1;\n\n\t\t\tPushNavPoint();\n\t\t\tdoc.Editor.GoTo( Math.Clamp( line, 1, doc.Editor.LineCount ) - 1, Math.Max( 1, column ) - 1 );\n\t\t\tdoc.Editor.FocusEditor();\n\t\t}, $\u0022Line number (1 - {doc.Editor.LineCount}), or line:column\u0022, okay: \u0022Go\u0022, initialName: (current.Line \u002B 1).ToString(), title: \u0022Go to Line\u0022 );\n\t}\n\n\t// --- AUTO SAVE ---\n\n\t/// \u003Csummary\u003E\n\t/// With Auto Save \u0022after a short pause\u0022, saves files once you\u0027ve stopped typing for a moment.\n\t/// Saving is what triggers s\u0026amp;box\u0027s hotload, so the game keeps up with your edits.\n\t/// \u003C/summary\u003E\n\tprivate void AutoSaveIfIdle()\n\t{\n\t\tif ( HotCodeSettings.AutoSave != AutoSaveMode.AfterDelay ) return;\n\n\t\tforeach ( var doc in _documents )\n\t\t{\n\t\t\tif ( doc.IsDirty \u0026\u0026 RealTime.Now - doc.LastEdit \u003E HotCodeSettings.AutoSaveDelay \u0026\u0026 File.Exists( doc.Path ) )\n\t\t\t\tSaveDocument( doc );\n\t\t}\n\t}\n\n\t// --- STATUS BAR ---\n\n\tprivate void UpdateStatusBar()\n\t{\n\t\t// Is s\u0026box compiling this project? If not, there\u0027s no live build to report on.\n\t\tif ( RealTime.Now \u003E _nextLoadedCheck )\n\t\t{\n\t\t\t_nextLoadedCheck = RealTime.Now \u002B 2;\n\t\t\t_projectLoaded = EditorUtility.Projects.GetAll().Any( p =\u003E\n\t\t\t{\n\t\t\t\tvar root = p.GetRootPath();\n\t\t\t\treturn !string.IsNullOrEmpty( root ) \u0026\u0026 (ProjectFiles.IsUnder( _root, root ) || ProjectFiles.IsUnder( root, _root ));\n\t\t\t} );\n\t\t}\n\n\t\tif ( !_projectLoaded )\n\t\t\t_buildStatus.Set( \u0022Not loaded in s\u0026box - no live compile\u0022, \u0022cloud_off\u0022, Theme.TextLight );\n\t\telse if ( _output.IsBuilding )\n\t\t\t_buildStatus.Set( \u0022Building...\u0022, \u0022sync\u0022, Theme.Primary );\n\t\telse if ( _output.ErrorCount \u003E 0 )\n\t\t\t_buildStatus.Set( $\u0022{_output.ErrorCount} error{(_output.ErrorCount == 1 ? \u0022\u0022 : \u0022s\u0022)}, {_output.WarningCount} warning{(_output.WarningCount == 1 ? \u0022\u0022 : \u0022s\u0022)}\u0022, \u0022error\u0022, Theme.Red );\n\t\telse if ( _output.WarningCount \u003E 0 )\n\t\t\t_buildStatus.Set( $\u0022Build OK, {_output.WarningCount} warning{(_output.WarningCount == 1 ? \u0022\u0022 : \u0022s\u0022)}\u0022, \u0022warning\u0022, Theme.Yellow );\n\t\telse if ( _output.LastBuildSucceeded == true )\n\t\t\t_buildStatus.Set( \u0022Build succeeded\u0022, \u0022check_circle\u0022, Theme.Green );\n\t\telse\n\t\t\t_buildStatus.Set( \u0022Ready\u0022, \u0022check\u0022, Theme.TextLight );\n\n\t\tvar doc = _active;\n\t\tif ( doc is null )\n\t\t{\n\t\t\tforeach ( var item in new[] { _position, _indent, _lineEnding, _encoding, _language } )\n\t\t\t\titem.Set( \u0022\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar caret = doc.Editor.CaretPosition;\n\t\tvar selected = doc.Editor.SelectionLength;\n\t\t_position.Set( $\u0022Ln {caret.Line \u002B 1}, Col {caret.Col \u002B 1}\u0022 \u002B (selected \u003E 0 ? $\u0022 ({selected} selected)\u0022 : \u0022\u0022) );\n\t\t_indent.Set( doc.Editor.IndentDescription );\n\t\t_lineEnding.Set( doc.Editor.LineEndingName );\n\t\t_encoding.Set( doc.HasBom ? \u0022UTF-8 with BOM\u0022 : \u0022UTF-8\u0022 );\n\t\t_language.Set( doc.Editor.LanguageName, HotCodeStyle.FileIcon( doc.Path ), HotCodeStyle.FileIconColor( doc.Path ) );\n\t}\n\n\tinternal void RegisterNode( CodeFileNode node )\n\t{\n\t\tif ( node.IsFolder ) _nodes[Normalize( node.FullPath )] = node;\n\t}\n\n\tprivate static string Normalize( string path ) =\u003E Path.GetFullPath( path ).TrimEnd( \u0027\\\\\u0027, \u0027/\u0027 );\n\n\t/// \u003Csummary\u003E\n\t/// Rebuilds a folder\u0027s children in the tree (and opens it, so new items are visible).\n\t/// \u003C/summary\u003E\n\tprivate void RefreshFolder( string folder, bool open = false )\n\t{\n\t\tif ( !_nodes.TryGetValue( Normalize( folder ), out var node ) ) return;\n\n\t\tnode.Dirty();\n\t\tif ( open ) _tree.Open( node );\n\t}\n\n\tinternal void ShowFileContextMenu( CodeFileNode node )\n\t{\n\t\tvar path = node.FullPath;\n\t\tvar folder = node.IsFolder ? path : Path.GetDirectoryName( path );\n\t\tvar isRoot = string.Equals( Normalize( path ), Normalize( _root ), StringComparison.OrdinalIgnoreCase );\n\n\t\tvar menu = new ContextMenu( this );\n\n\t\tif ( !node.IsFolder )\n\t\t{\n\t\t\tmenu.AddOption( \u0022Open\u0022, \u0022open_in_new\u0022, () =\u003E OpenFile( path, preview: false ) );\n\t\t\tmenu.AddOption( \u0022Open in External Editor\u0022, \u0022launch\u0022, () =\u003E CodeEditor.OpenFile( path ) );\n\t\t\tmenu.AddSeparator();\n\t\t}\n\n\t\tmenu.AddOption( \u0022New File...\u0022, \u0022note_add\u0022, () =\u003E AskCreate( folder, \u0022New File\u0022, \u0022File name:\u0022, \u0022NewFile.cs\u0022, name =\u003E ProjectFiles.CreateFile( folder, name ) ) );\n\t\tmenu.AddOption( \u0022New C# Component...\u0022, \u0022code\u0022, () =\u003E AskCreate( folder, \u0022New Component\u0022, \u0022Component name:\u0022, \u0022MyComponent\u0022, name =\u003E ProjectFiles.CreateComponent( folder, name ) ) );\n\t\tmenu.AddOption( \u0022New Folder...\u0022, \u0022create_new_folder\u0022, () =\u003E AskCreate( folder, \u0022New Folder\u0022, \u0022Folder name:\u0022, \u0022NewFolder\u0022, name =\u003E ProjectFiles.CreateFolder( folder, name ) ) );\n\t\tmenu.AddSeparator();\n\n\t\tif ( !isRoot )\n\t\t{\n\t\t\tmenu.AddOption( \u0022Rename...\u0022, \u0022drive_file_rename_outline\u0022, () =\u003E AskRename( node ) );\n\t\t\tmenu.AddOption( \u0022Delete\u0022, \u0022delete\u0022, () =\u003E AskDelete( node ) );\n\t\t\tmenu.AddSeparator();\n\t\t}\n\n\t\tmenu.AddOption( \u0022Copy Path\u0022, \u0022content_copy\u0022, () =\u003E EditorUtility.Clipboard.Copy( path ) );\n\t\tmenu.AddOption( \u0022Copy Relative Path\u0022, \u0022content_copy\u0022, () =\u003E EditorUtility.Clipboard.Copy( Path.GetRelativePath( _root, path ) ) );\n\t\tmenu.AddOption( \u0022Reveal in File Explorer\u0022, \u0022folder_open\u0022, () =\u003E EditorUtility.OpenFileFolder( path ) );\n\n\t\tif ( node.IsFolder )\n\t\t\tmenu.AddOption( \u0022Refresh\u0022, \u0022refresh\u0022, () =\u003E node.Dirty() );\n\n\t\tmenu.OpenAtCursor();\n\t}\n\n\tprivate void AskCreate( string folder, string title, string question, string suggestion, Func\u003Cstring, string\u003E create )\n\t{\n\t\tDialog.AskString( name =\u003E\n\t\t{\n\t\t\tif ( !TryFileOp( () =\u003E create( name ), out var created ) ) return;\n\n\t\t\tRefreshFolder( folder, open: true );\n\t\t\tif ( File.Exists( created ) ) OpenFile( created, preview: false );\n\t\t}, question, okay: \u0022Create\u0022, initialName: suggestion, title: title );\n\t}\n\n\tprivate void AskRename( CodeFileNode node )\n\t{\n\t\tvar oldPath = node.FullPath;\n\n\t\tDialog.AskString( name =\u003E\n\t\t{\n\t\t\tif ( !TryFileOp( () =\u003E ProjectFiles.Rename( oldPath, name ), out var newPath ) ) return;\n\n\t\t\t// Open tabs follow the file (or everything inside a renamed folder)\n\t\t\tforeach ( var doc in _documents )\n\t\t\t{\n\t\t\t\tif ( string.Equals( doc.Path, oldPath, StringComparison.OrdinalIgnoreCase ) )\n\t\t\t\t\tdoc.Path = newPath;\n\t\t\t\telse if ( ProjectFiles.IsUnder( doc.Path, oldPath ) )\n\t\t\t\t\tdoc.Path = newPath \u002B doc.Path[oldPath.Length..];\n\t\t\t}\n\n\t\t\t_tabs.Update();\n\t\t\tRefreshFolder( Path.GetDirectoryName( oldPath ) );\n\t\t}, \u0022New name:\u0022, okay: \u0022Rename\u0022, initialName: node.Name, title: \u0022Rename\u0022 );\n\t}\n\n\tprivate void AskDelete( CodeFileNode node )\n\t{\n\t\tvar path = node.FullPath;\n\t\tvar affected = _documents.Where( d =\u003E ProjectFiles.IsUnder( d.Path, path ) ).ToList();\n\t\tvar warning = affected.Any( d =\u003E d.IsDirty ) ? \u0022 Unsaved changes in open files will be lost.\u0022 : \u0022\u0022;\n\n\t\tDialog.AskConfirm( () =\u003E\n\t\t{\n\t\t\tif ( !TryFileOp( () =\u003E { ProjectFiles.Delete( path ); return path; }, out _ ) ) return;\n\n\t\t\tforeach ( var doc in affected ) CloseDocument( doc );\n\t\t\tRefreshFolder( Path.GetDirectoryName( path ) );\n\t\t}, $\u0022Move \u0027{node.Name}\u0027 to the Recycle Bin?{warning}\u0022, \u0022Delete\u0022, \u0022Delete\u0022, \u0022Cancel\u0022 );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Runs a file operation and shows any error instead of throwing.\n\t/// \u003C/summary\u003E\n\tprivate bool TryFileOp( Func\u003Cstring\u003E op, out string result )\n\t{\n\t\ttry\n\t\t{\n\t\t\tresult = op();\n\t\t\treturn true;\n\t\t}\n\t\tcatch ( Exception e ) when ( e is InvalidOperationException or IOException or UnauthorizedAccessException or OperationCanceledException )\n\t\t{\n\t\t\tresult = null;\n\t\t\tLog.Warning( $\u0022[HotCode] {e.Message}\u0022 );\n\t\t\tnew PopupWindow( \u0022HotCode\u0022, e.Message, \u0022OK\u0022, new Dictionary\u003Cstring, Action\u003E() ).Show();\n\t\t\treturn false;\n\t\t}\n\t}\n\n\t// --- WATCHING THE DISK ---\n\n\tprivate void QueueChange( string path, bool structural )\n\t{\n\t\t// Build output churns constantly and is never shown; ignore it\n\t\tif ( CodeFileNode.IsInHiddenFolder( Path.GetRelativePath( _root, path ) ) ) return;\n\t\t_changedPaths.Enqueue( (path, structural) );\n\t}\n\n\t[EditorEvent.Frame]\n\tpublic void Frame()\n\t{\n\t\twhile ( _definitionResults.TryDequeue( out var definition ) )\n\t\t\tShowDefinitionResult( definition.Doc, definition.Result );\n\n\t\t_completions.Pump();\n\t\t_signatures.Pump();\n\t\tUpdateStatusBar();\n\t\tAutoSaveIfIdle();\n\n\t\tif ( _changedPaths.IsEmpty ) return;\n\n\t\tvar folders = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\n\t\tvar files = new HashSet\u003Cstring\u003E( StringComparer.OrdinalIgnoreCase );\n\n\t\twhile ( _changedPaths.TryDequeue( out var change ) )\n\t\t{\n\t\t\tfiles.Add( change.Path );\n\t\t\tvar parent = Path.GetDirectoryName( change.Path );\n\t\t\tif ( change.Structural \u0026\u0026 parent is not null ) folders.Add( parent );\n\t\t}\n\n\t\tforeach ( var folder in folders )\n\t\t\tRefreshFolder( folder );\n\n\t\tforeach ( var file in files )\n\t\t\tReloadIfChangedOnDisk( file );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Picks up edits made by other programs to a file that\u0027s open but has no unsaved changes here.\n\t/// \u003C/summary\u003E\n\tprivate void ReloadIfChangedOnDisk( string path )\n\t{\n\t\tvar doc = FindDocument( path );\n\t\tif ( doc is null || doc.IsDirty || !File.Exists( path ) || !HotCodeSettings.ReloadChangedFiles ) return;\n\t\tif ( !TryReadFile( path, out var contents, out _ ) ) return;\n\n\t\t// Our own saves land here too; only reload when the text really differs\n\t\tif ( TextBuffer.Normalize( contents ) == TextBuffer.Normalize( doc.Editor.Text ) ) return;\n\n\t\tdoc.Editor.ReloadText( contents );\n\t\tApplyMarkers( doc );\n\t\tLog.Info( $\u0022[HotCode] Reloaded {doc.Title} (changed outside HotCode)\u0022 );\n\t}\n\n\t// --- CLOSING ---\n\n\t[Shortcut( \u0022hotcode.close-tab\u0022, \u0022CTRL\u002BW\u0022 )]\n\tpublic void CloseActiveTab()\n\t{\n\t\tif ( _active is not null )\n\t\t\tRequestClose( _active );\n\t}\n\n\tprivate void RequestClose( EditorDocument doc )\n\t{\n\t\tif ( !doc.IsDirty )\n\t\t{\n\t\t\tCloseDocument( doc );\n\t\t\treturn;\n\t\t}\n\n\t\tvar confirm = new PopupWindow(\n\t\t\t\u0022Unsaved Changes\u0022, $\u0022Save changes to {doc.Title}?\u0022, \u0022Cancel\u0022,\n\t\t\tnew Dictionary\u003Cstring, Action\u003E()\n\t\t\t{\n\t\t\t\t{ \u0022Don\u0027t Save\u0022, () =\u003E CloseDocument( doc ) },\n\t\t\t\t{ \u0022Save\u0022, () =\u003E { if ( SaveDocument( doc ) ) CloseDocument( doc ); } }\n\t\t\t}\n\t\t);\n\n\t\tconfirm.Show();\n\t}\n\n\tprivate void CloseDocument( EditorDocument doc )\n\t{\n\t\tvar index = _documents.IndexOf( doc );\n\t\tif ( index \u003C 0 ) return;\n\t\tvar wasActive = doc == _active;\n\n\t\tRemoveDocument( doc );\n\n\t\tif ( wasActive )\n\t\t{\n\t\t\t// Same as most IDEs: move to the tab that slid into this spot, or the one before it\n\t\t\tvar next = _documents.Count == 0 ? null : _documents[Math.Min( index, _documents.Count - 1 )];\n\t\t\t_active = null;\n\t\t\tActivate( next );\n\t\t}\n\n\t\tUpdateTitle();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Drops a document without any prompts or activating anything else.\n\t/// \u003C/summary\u003E\n\tprivate void RemoveDocument( EditorDocument doc )\n\t{\n\t\t_documents.Remove( doc );\n\t\t_tabs.Remove( doc );\n\t\tdoc.Editor.Destroy();\n\n\t\tif ( _active == doc )\n\t\t\t_active = null;\n\t}\n\n\t// --- SAVING ---\n\n\t[Shortcut( \u0022editor.save\u0022, \u0022CTRL\u002BS\u0022 )]\n\tpublic void Save()\n\t{\n\t\tif ( _active is not null )\n\t\t\tSaveDocument( _active );\n\t}\n\n\tprivate bool SaveDocument( EditorDocument doc )\n\t{\n\t\tif ( !doc.IsDirty )\n\t\t\treturn true;\n\n\t\tif ( HotCodeSettings.TrimTrailingWhitespace || HotCodeSettings.InsertFinalNewline )\n\t\t\tdoc.Editor.FormatForSave( HotCodeSettings.TrimTrailingWhitespace, HotCodeSettings.InsertFinalNewline );\n\n\t\ttry\n\t\t{\n\t\t\tFile.WriteAllText( doc.Path, doc.Editor.Text, new UTF8Encoding( doc.HasBom ) );\n\t\t\tLog.Info( $\u0022[HotCode] Saved {doc.Title}\u0022 );\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or UnauthorizedAccessException )\n\t\t{\n\t\t\tLog.Warning( $\u0022[HotCode] Couldn\u0027t save {doc.Path}: {e.Message}\u0022 );\n\t\t\treturn false;\n\t\t}\n\n\t\tdoc.Editor.MarkSaved();\n\t\tdoc.IsDirty = false;\n\t\t_tabs.Update();\n\t\t_tree.Update();\n\t\tUpdateTitle();\n\t\treturn true;\n\t}\n\n\tprivate void UpdateTitle()\n\t{\n\t\tvar unsaved = _documents.Count( d =\u003E d.IsDirty );\n\n\t\tWindowTitle = unsaved \u003E 0\n\t\t\t? $\u0022HotCode - {_entry.Title} ({unsaved} unsaved)\u0022\n\t\t\t: $\u0022HotCode - {_entry.Title}\u0022;\n\t}\n\n\tprotected override bool OnClose()\n\t{\n\t\tvar dirty = _documents.Where( d =\u003E d.IsDirty ).ToList();\n\t\tif ( dirty.Count == 0 )\n\t\t\treturn true;\n\n\t\tvar confirm = new PopupWindow(\n\t\t\t\u0022Unsaved Changes\u0022, $\u0022{dirty.Count} file(s) have unsaved changes. Save before closing?\u0022, \u0022Cancel\u0022,\n\t\t\tnew Dictionary\u003Cstring, Action\u003E()\n\t\t\t{\n\t\t\t\t{ \u0022Don\u0027t Save\u0022, () =\u003E { dirty.ForEach( d =\u003E d.IsDirty = false ); Close(); } },\n\t\t\t\t{ \u0022Save\u0022, () =\u003E { if ( dirty.All( SaveDocument ) ) Close(); } }\n\t\t\t}\n\t\t);\n\n\t\tconfirm.Show();\n\t\treturn false;\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/Launcher/RecentEntries.cs","FileName":"RecentEntries.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json;\n\npublic enum HotCodeEntryKind\n{\n\tProject,\n\tSolution\n}\n\n/// \u003Csummary\u003E\n/// Something the launcher can open: an s\u0026amp;box project (.sbproj) or a solution (.sln/.slnx).\n/// \u003C/summary\u003E\npublic sealed record HotCodeEntry( string Path, HotCodeEntryKind Kind, string Title, string Type )\n{\n\t/// \u003Csummary\u003E\n\t/// When it was last opened from HotCode, if it\u0027s in the recents list.\n\t/// \u003C/summary\u003E\n\tpublic DateTime? LastOpened { get; init; }\n\n\t/// \u003Csummary\u003E\n\t/// Pinned entries stay at the top and never drop off the recents list.\n\t/// \u003C/summary\u003E\n\tpublic bool Pinned { get; init; }\n\n\tpublic string Directory =\u003E System.IO.Path.GetDirectoryName( Path );\n\n\tpublic string Icon =\u003E Kind == HotCodeEntryKind.Solution ? \u0022account_tree\u0022 : Type switch\n\t{\n\t\t\u0022game\u0022 =\u003E \u0022sports_esports\u0022,\n\t\t\u0022library\u0022 =\u003E \u0022local_library\u0022,\n\t\t\u0022map\u0022 =\u003E \u0022public\u0022,\n\t\t\u0022content\u0022 =\u003E \u0022perm_media\u0022,\n\t\t\u0022tool\u0022 =\u003E \u0022hardware\u0022,\n\t\t\u0022addon\u0022 =\u003E \u0022extension\u0022,\n\t\t_ =\u003E \u0022folder_special\u0022\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// \u0022Game\u0022, \u0022Library\u0022, \u0022Solution\u0022...\n\t/// \u003C/summary\u003E\n\tpublic string TypeName =\u003E Kind == HotCodeEntryKind.Solution ? \u0022Solution\u0022\n\t\t: string.IsNullOrEmpty( Type ) ? \u0022Project\u0022 : char.ToUpperInvariant( Type[0] ) \u002B Type[1..];\n\n\tpublic Color TypeColor =\u003E Kind == HotCodeEntryKind.Solution ? Theme.Pink : Type switch\n\t{\n\t\t\u0022game\u0022 =\u003E Theme.Green,\n\t\t\u0022library\u0022 =\u003E Theme.Blue,\n\t\t\u0022addon\u0022 or \u0022content\u0022 or \u0022map\u0022 =\u003E Theme.Yellow,\n\t\t_ =\u003E Theme.TextLight\n\t};\n\n\tpublic static HotCodeEntry FromPath( string path )\n\t{\n\t\tvar ext = System.IO.Path.GetExtension( path ).ToLowerInvariant();\n\n\t\tif ( ext is \u0022.sln\u0022 or \u0022.slnx\u0022 )\n\t\t\treturn new HotCodeEntry( path, HotCodeEntryKind.Solution, System.IO.Path.GetFileNameWithoutExtension( path ), \u0022solution\u0022 );\n\n\t\tvar title = System.IO.Path.GetFileNameWithoutExtension( path );\n\t\tvar type = \u0022\u0022;\n\n\t\ttry\n\t\t{\n\t\t\tusing var doc = JsonDocument.Parse( File.ReadAllText( path ) );\n\t\t\tif ( doc.RootElement.TryGetProperty( \u0022Title\u0022, out var t ) \u0026\u0026 t.GetString() is { Length: \u003E 0 } s ) title = s;\n\t\t\tif ( doc.RootElement.TryGetProperty( \u0022Type\u0022, out var ty ) ) type = ty.GetString() ?? \u0022\u0022;\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\tLog.Warning( $\u0022[HotCode] Couldn\u0027t read {path}: {e.Message}\u0022 );\n\t\t}\n\n\t\treturn new HotCodeEntry( path, HotCodeEntryKind.Project, title, type );\n\t}\n}\n\n/// \u003Csummary\u003E\n/// One remembered project or solution. Stored in an editor cookie, so it\u0027s a plain class for JSON.\n/// \u003C/summary\u003E\npublic class RecentRecord\n{\n\tpublic string Path { get; set; }\n\tpublic DateTime LastOpened { get; set; }\n\tpublic bool Pinned { get; set; }\n}\n\n/// \u003Csummary\u003E\n/// Recently opened projects/solutions, persisted across editor sessions.\n/// \u003C/summary\u003E\npublic static class RecentEntries\n{\n\tconst string CookieKey = \u0022hotcode.recent.v2\u0022;\n\tconst string OldCookieKey = \u0022hotcode.recent\u0022;\n\n\t/// \u003Csummary\u003E\n\t/// How many unpinned entries to keep. Pinned ones don\u0027t count.\n\t/// \u003C/summary\u003E\n\tconst int MaxEntries = 8;\n\n\tprivate static List\u003CRecentRecord\u003E Load()\n\t{\n\t\tvar records = EditorCookie.Get\u003CList\u003CRecentRecord\u003E\u003E( CookieKey, null );\n\t\tif ( records is not null ) return records;\n\n\t\t// Carry over the old list of plain paths\n\t\tvar old = EditorCookie.Get\u003CList\u003Cstring\u003E\u003E( OldCookieKey, null ) ?? new();\n\t\trecords = old.Select( ( p, i ) =\u003E new RecentRecord { Path = p, LastOpened = DateTime.Now.AddMinutes( -i ) } ).ToList();\n\t\tSave( records );\n\t\treturn records;\n\t}\n\n\tprivate static void Save( List\u003CRecentRecord\u003E records ) =\u003E EditorCookie.Set( CookieKey, records );\n\n\t/// \u003Csummary\u003E\n\t/// Recent entries that still exist on disk: pinned first, then most recent.\n\t/// \u003C/summary\u003E\n\tpublic static IEnumerable\u003CHotCodeEntry\u003E GetAll() =\u003E Load()\n\t\t.Where( r =\u003E File.Exists( r.Path ) )\n\t\t.OrderByDescending( r =\u003E r.Pinned )\n\t\t.ThenByDescending( r =\u003E r.LastOpened )\n\t\t.Select( r =\u003E HotCodeEntry.FromPath( r.Path ) with { LastOpened = r.LastOpened, Pinned = r.Pinned } );\n\n\t/// \u003Csummary\u003E\n\t/// Marks a path as just opened (adding it if needed).\n\t/// \u003C/summary\u003E\n\tpublic static void Touch( string path )\n\t{\n\t\tvar records = Load();\n\t\tvar record = Find( records, path );\n\t\tif ( record is null )\n\t\t{\n\t\t\trecord = new RecentRecord { Path = Normalize( path ) };\n\t\t\trecords.Add( record );\n\t\t}\n\n\t\trecord.LastOpened = DateTime.Now;\n\n\t\t// Drop the oldest unpinned ones past the limit\n\t\tvar extra = records.Where( r =\u003E !r.Pinned ).OrderByDescending( r =\u003E r.LastOpened ).Skip( MaxEntries ).ToList();\n\t\trecords.RemoveAll( extra.Contains );\n\n\t\tSave( records );\n\t}\n\n\tpublic static void SetPinned( string path, bool pinned )\n\t{\n\t\tvar records = Load();\n\t\tvar record = Find( records, path );\n\t\tif ( record is null )\n\t\t{\n\t\t\trecord = new RecentRecord { Path = Normalize( path ), LastOpened = DateTime.Now };\n\t\t\trecords.Add( record );\n\t\t}\n\n\t\trecord.Pinned = pinned;\n\t\tSave( records );\n\t}\n\n\tpublic static void Remove( string path )\n\t{\n\t\tvar records = Load();\n\t\trecords.RemoveAll( r =\u003E Normalize( r.Path ) == Normalize( path ) );\n\t\tSave( records );\n\t}\n\n\tprivate static RecentRecord Find( List\u003CRecentRecord\u003E records, string path ) =\u003E records.FirstOrDefault( r =\u003E Normalize( r.Path ) == Normalize( path ) );\n\n\tpublic static string Normalize( string path ) =\u003E System.IO.Path.GetFullPath( path ).Replace( \u0027\\\\\u0027, \u0027/\u0027 );\n\n\t/// \u003Csummary\u003E\n\t/// \u0022just now\u0022, \u00225 minutes ago\u0022, \u0022yesterday\u0022, \u002212 Mar\u0022...\n\t/// \u003C/summary\u003E\n\tpublic static string Ago( DateTime time )\n\t{\n\t\tvar span = DateTime.Now - time;\n\t\tif ( span.TotalMinutes \u003C 1 ) return \u0022just now\u0022;\n\t\tif ( span.TotalMinutes \u003C 60 ) return $\u0022{(int)span.TotalMinutes} min ago\u0022;\n\t\tif ( span.TotalHours \u003C 24 ) return $\u0022{(int)span.TotalHours} hour{((int)span.TotalHours == 1 ? \u0022\u0022 : \u0022s\u0022)} ago\u0022;\n\t\tif ( span.TotalDays \u003C 2 ) return \u0022yesterday\u0022;\n\t\tif ( span.TotalDays \u003C 7 ) return $\u0022{(int)span.TotalDays} days ago\u0022;\n\t\treturn time.ToString( time.Year == DateTime.Now.Year ? \u0022d MMM\u0022 : \u0022d MMM yyyy\u0022 );\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/Navigation/DefinitionFinder.cs","FileName":"DefinitionFinder.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing Microsoft.CodeAnalysis;\nusing Microsoft.CodeAnalysis.CSharp;\nusing Microsoft.CodeAnalysis.CSharp.Syntax;\n\n/// \u003Csummary\u003E\n/// Where a symbol is declared. Line and Column are zero-based.\n/// \u003C/summary\u003E\npublic record DefinitionLocation( string Path, int Line, int Column, string Preview );\n\n/// \u003Csummary\u003E\n/// The answer to \u0022go to definition\u0022: source locations, or the assembly a symbol comes from\n/// when there\u0027s no source for it (like s\u0026amp;box\u0027s own types).\n/// \u003C/summary\u003E\npublic record DefinitionResult( string Symbol, IReadOnlyList\u003CDefinitionLocation\u003E Locations, string Assembly, string Error )\n{\n\tpublic static DefinitionResult Fail( string error ) =\u003E new( null, Array.Empty\u003CDefinitionLocation\u003E(), null, error );\n}\n\n/// \u003Csummary\u003E\n/// Go to definition, using the compilation from \u003Csee cref=\u0022RoslynProject\u0022/\u003E.\n/// \u003C/summary\u003E\npublic static class DefinitionFinder\n{\n\t/// \u003Csummary\u003E\n\t/// Finds the definition of whatever is at (line, column) in the snapshot\u0027s file. Safe to call off the main thread.\n\t/// \u003C/summary\u003E\n\tpublic static DefinitionResult Find( RoslynProject.Snapshot snapshot, int line, int column )\n\t{\n\t\ttry\n\t\t{\n\t\t\treturn FindInternal( snapshot, line, column );\n\t\t}\n\t\tcatch ( Exception e )\n\t\t{\n\t\t\treturn DefinitionResult.Fail( $\u0022Couldn\u0027t look that up: {e.Message}\u0022 );\n\t\t}\n\t}\n\n\tprivate static DefinitionResult FindInternal( RoslynProject.Snapshot snapshot, int line, int column )\n\t{\n\t\tvar (compilation, target) = RoslynProject.GetCompilation( snapshot );\n\t\tif ( target is null )\n\t\t\treturn DefinitionResult.Fail( \u0022Go to Definition works in C# files inside the project\u0027s code folders.\u0022 );\n\n\t\tvar model = compilation.GetSemanticModel( target );\n\t\tvar symbol = SymbolAt( model, target, line, column );\n\n\t\tif ( symbol is null )\n\t\t\treturn DefinitionResult.Fail( \u0022Nothing to go to here.\u0022 );\n\n\t\tsymbol = Unwrap( symbol );\n\t\tvar display = symbol.ToDisplayString( SymbolDisplayFormat.CSharpShortErrorMessageFormat );\n\n\t\tvar locations = symbol.Locations\n\t\t\t.Where( l =\u003E l.IsInSource \u0026\u0026 System.IO.File.Exists( l.SourceTree?.FilePath ) )\n\t\t\t.Select( l =\u003E\n\t\t\t{\n\t\t\t\tvar span = l.GetLineSpan();\n\t\t\t\tvar source = l.SourceTree.GetText().Lines[span.StartLinePosition.Line].ToString().Trim();\n\t\t\t\treturn new DefinitionLocation( l.SourceTree.FilePath, span.StartLinePosition.Line, span.StartLinePosition.Character, source );\n\t\t\t} )\n\t\t\t.ToList();\n\n\t\treturn new DefinitionResult( display, locations, symbol.ContainingAssembly?.Name, null );\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The symbol for the identifier at a position. Also works with the caret just after a word.\n\t/// \u003C/summary\u003E\n\tprivate static ISymbol SymbolAt( SemanticModel model, SyntaxTree tree, int line, int column )\n\t{\n\t\tvar text = tree.GetText();\n\t\tif ( line \u003C 0 || line \u003E= text.Lines.Count ) return null;\n\n\t\tvar lineInfo = text.Lines[line];\n\t\tvar position = Math.Min( lineInfo.Start \u002B column, lineInfo.End );\n\t\tvar root = tree.GetRoot();\n\n\t\tforeach ( var candidate in new[] { position, position - 1 } )\n\t\t{\n\t\t\tif ( candidate \u003C lineInfo.Start ) continue;\n\n\t\t\tvar token = root.FindToken( candidate );\n\t\t\tif ( !token.Span.Contains( candidate ) \u0026\u0026 token.Span.End != candidate ) continue;\n\t\t\tif ( !token.IsKind( SyntaxKind.IdentifierToken ) \u0026\u0026 !SyntaxFacts.IsPredefinedType( token.Kind() ) \u0026\u0026 !token.IsKind( SyntaxKind.BaseKeyword ) \u0026\u0026 !token.IsKind( SyntaxKind.ThisKeyword ) )\n\t\t\t\tcontinue;\n\n\t\t\tvar symbol = SymbolFor( model, token );\n\t\t\tif ( symbol is not null ) return symbol;\n\t\t}\n\n\t\treturn null;\n\t}\n\n\tprivate static ISymbol SymbolFor( SemanticModel model, SyntaxToken token )\n\t{\n\t\tvar node = token.Parent;\n\t\tif ( node is null ) return null;\n\n\t\t// On a declaration\u0027s own name: go to that declaration (useful for partial classes and overrides)\n\t\tvar declared = model.GetDeclaredSymbol( node );\n\t\tif ( declared is not null \u0026\u0026 token == DeclarationIdentifier( node ) )\n\t\t{\n\t\t\t// An override\u0027s name jumps to what it overrides, since you\u0027re already at its definition\n\t\t\treturn declared switch\n\t\t\t{\n\t\t\t\tIMethodSymbol { OverriddenMethod: { } m } =\u003E m,\n\t\t\t\tIPropertySymbol { OverriddenProperty: { } p } =\u003E p,\n\t\t\t\tIEventSymbol { OverriddenEvent: { } ev } =\u003E ev,\n\t\t\t\t_ =\u003E declared\n\t\t\t};\n\t\t}\n\n\t\tvar info = model.GetSymbolInfo( node );\n\t\tvar symbol = info.Symbol ?? info.CandidateSymbols.FirstOrDefault();\n\n\t\tif ( symbol is null \u0026\u0026 node.Parent is not null )\n\t\t{\n\t\t\tinfo = model.GetSymbolInfo( node.Parent );\n\t\t\tsymbol = info.Symbol ?? info.CandidateSymbols.FirstOrDefault();\n\t\t}\n\n\t\t// \u0060new Foo()\u0060 with no constructor written lands on Foo itself\n\t\tif ( symbol is IMethodSymbol { MethodKind: MethodKind.Constructor, IsImplicitlyDeclared: true } ctor )\n\t\t\tsymbol = ctor.ContainingType;\n\n\t\t// \u0060var\u0060 resolves to the type it stands for\n\t\tsymbol ??= model.GetTypeInfo( node ).Type;\n\n\t\treturn symbol;\n\t}\n\n\tprivate static SyntaxToken? DeclarationIdentifier( SyntaxNode node ) =\u003E node switch\n\t{\n\t\tBaseTypeDeclarationSyntax t =\u003E t.Identifier,\n\t\tDelegateDeclarationSyntax d =\u003E d.Identifier,\n\t\tMethodDeclarationSyntax m =\u003E m.Identifier,\n\t\tPropertyDeclarationSyntax p =\u003E p.Identifier,\n\t\tEventDeclarationSyntax e =\u003E e.Identifier,\n\t\tVariableDeclaratorSyntax v =\u003E v.Identifier,\n\t\tParameterSyntax p =\u003E p.Identifier,\n\t\tConstructorDeclarationSyntax c =\u003E c.Identifier,\n\t\tEnumMemberDeclarationSyntax e =\u003E e.Identifier,\n\t\t_ =\u003E null\n\t};\n\n\t/// \u003Csummary\u003E\n\t/// From a use of a generic or extension method back to the thing that was declared.\n\t/// \u003C/summary\u003E\n\tprivate static ISymbol Unwrap( ISymbol symbol )\n\t{\n\t\tif ( symbol is IMethodSymbol { ReducedFrom: { } reduced } ) symbol = reduced;\n\t\tif ( symbol is IAliasSymbol alias ) symbol = alias.Target;\n\t\treturn symbol.OriginalDefinition ?? symbol;\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/Settings/HotCodeSettingsWindow.cs","FileName":"HotCodeSettingsWindow.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\n\n/// \u003Csummary\u003E\n/// HotCode\u0027s settings, laid out like s\u0026amp;box\u0027s own Editor Settings: pages down the left, and a\n/// ControlSheet of properties on each page. Changes save and apply straight away.\n/// \u003C/summary\u003E\npublic class HotCodeSettingsWindow : Window\n{\n\tprivate static HotCodeSettingsWindow _open;\n\n\t/// \u003Csummary\u003E\n\t/// Opens the settings, or brings the already-open window to the front.\n\t/// \u003C/summary\u003E\n\tpublic static void Open()\n\t{\n\t\tif ( _open.IsValid() )\n\t\t{\n\t\t\t_open.Raise();\n\t\t\t_open.Focus( false );\n\t\t\treturn;\n\t\t}\n\n\t\t_open = new HotCodeSettingsWindow();\n\t}\n\n\t[Menu( \u0022Editor\u0022, \u0022HotCode/Settings\u0022, HotCodeLogo.IconName )]\n\tpublic static void OpenFromEditorMenu() =\u003E Open();\n\n\tprivate HotCodeSettingsWindow()\n\t{\n\t\tWindowTitle = \u0022HotCode Settings\u0022;\n\t\tSetWindowIcon( HotCodeLogo.WindowIcon );\n\t\tDeleteOnClose = true;\n\t\tSize = new Vector2( 860, 620 );\n\t\tMinimumSize = new Vector2( 640, 420 );\n\n\t\tCanvas = new Widget( this );\n\t\tCanvas.Layout = Layout.Column();\n\t\tCanvas.Layout.Margin = 4;\n\t\tCanvas.Layout.Spacing = 4;\n\n\t\tvar nav = Canvas.Layout.Add( new NavigationView( Canvas ), 1 );\n\t\tnav.AddSectionHeader( \u0022HotCode\u0022 );\n\t\tnav.AddPage( \u0022Editor\u0022, \u0022edit_note\u0022, Page( \u0022Editor\u0022, sheet =\u003E\n\t\t{\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.FontSize );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.FontFamily );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.LineSpacing );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.TabSize );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.IndentWithSpaces );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.WordWrap );\n\t\t} ) );\n\t\tnav.AddPage( \u0022Appearance\u0022, \u0022palette\u0022, Page( \u0022Appearance\u0022, sheet =\u003E\n\t\t{\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.CodeColors );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.ShowLineNumbers );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.HighlightCurrentLine );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.CursorBlink );\n\t\t} ) );\n\t\tnav.AddPage( \u0022Code Help\u0022, \u0022auto_awesome\u0022, Page( \u0022Code Help\u0022, sheet =\u003E\n\t\t{\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.SuggestWhileTyping );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.ParameterHints );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.AutoCloseBrackets );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.UnderlineErrors );\n\t\t} ) );\n\t\tnav.AddPage( \u0022Files\u0022, \u0022save\u0022, Page( \u0022Files\u0022, sheet =\u003E\n\t\t{\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.AutoSave );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.AutoSaveDelay );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.TrimTrailingWhitespace );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.InsertFinalNewline );\n\t\t\tsheet.AddProperty( () =\u003E HotCodeSettings.ReloadChangedFiles );\n\t\t} ) );\n\n\t\t// Footer\n\t\tCanvas.Layout.AddSeparator( 1, Theme.Border );\n\t\tvar footer = Canvas.Layout.AddRow();\n\t\tfooter.Margin = new Sandbox.UI.Margin( 8, 4, 8, 6 );\n\t\tfooter.Spacing = 8;\n\n\t\tvar note = footer.Add( new Label( \u0022Changes save and apply immediately.\u0022, Canvas ) );\n\t\tnote.SetStyles( $\u0022color: {Theme.TextLight.Hex};\u0022 );\n\t\tfooter.AddStretchCell();\n\n\t\tfooter.Add( new Button( \u0022Reset to Defaults\u0022, \u0022settings_backup_restore\u0022, Canvas )\n\t\t{\n\t\t\tClicked = () =\u003E Dialog.AskConfirm( () =\u003E\n\t\t\t{\n\t\t\t\tHotCodeSettings.ResetToDefaults();\n\t\t\t\t_open = null;\n\t\t\t\tClose();\n\t\t\t\tOpen();\n\t\t\t}, \u0022Reset every HotCode setting to its default?\u0022, \u0022Reset Settings\u0022, \u0022Reset\u0022, \u0022Cancel\u0022 )\n\t\t} );\n\t\tfooter.Add( new Button.Primary( \u0022Done\u0022, \u0022done\u0022, Canvas ) { Clicked = () =\u003E Close() } );\n\n\t\tShow();\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// A settings page: a heading and a property sheet, padded like s\u0026amp;box\u0027s preference pages.\n\t/// \u003C/summary\u003E\n\tprivate Widget Page( string title, Action\u003CControlSheet\u003E fill )\n\t{\n\t\tvar page = new Widget( this );\n\t\tpage.Layout = Layout.Column();\n\t\tpage.Layout.Margin = 32;\n\t\tpage.Layout.Spacing = 8;\n\n\t\tvar heading = page.Layout.AddRow();\n\t\theading.Spacing = 10;\n\t\theading.Add( new LogoWidget( page, 22, badge: false ) );\n\t\theading.Add( new Label.Subtitle( title, page ) );\n\t\theading.AddStretchCell();\n\n\t\tvar sheet = new ControlSheet();\n\t\tfill( sheet );\n\t\tpage.Layout.Add( sheet );\n\t\tpage.Layout.AddStretchCell();\n\n\t\treturn page;\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/CodeView/SyntaxLanguage.cs","FileName":"SyntaxLanguage.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\r\nusing System.Collections.Generic;\r\nusing System.IO;\r\n\r\npublic enum TokenKind\r\n{\r\n\tText,\r\n\tKeyword,\r\n\tControlKeyword,\r\n\tType,\r\n\tMethod,\r\n\tString,\r\n\tNumber,\r\n\tComment,\r\n\tPreprocessor,\r\n\tProperty\r\n}\r\n\r\npublic readonly record struct Token( int Start, int Length, TokenKind Kind );\r\n\r\n/// \u003Csummary\u003E\r\n/// What a line starts \u0022inside of\u0022, for constructs that span lines.\r\n/// \u003C/summary\u003E\r\npublic enum LexState\r\n{\r\n\tNormal,\r\n\tBlockComment,\r\n\tVerbatimString,\r\n\tRawString\r\n}\r\n\r\n/// \u003Csummary\u003E\r\n/// A small, line-based tokenizer configured per language. It isn\u0027t a real parser: types and\r\n/// methods are guessed (PascalCase word = type, word followed by \u0022(\u0022 = method), which is\r\n/// right most of the time for C# and cheap enough to run on every keystroke.\r\n/// \u003C/summary\u003E\r\npublic class SyntaxLanguage\r\n{\r\n\tpublic string Name { get; init; }\r\n\tpublic HashSet\u003Cstring\u003E Keywords { get; init; } = new();\r\n\tpublic HashSet\u003Cstring\u003E ControlKeywords { get; init; } = new();\r\n\tpublic string LineComment { get; init; }\r\n\tpublic bool BlockComments { get; init; }\r\n\tpublic bool CSharpStrings { get; init; }\r\n\tpublic bool SingleQuoteStrings { get; init; }\r\n\tpublic bool HashPreprocessor { get; init; }\r\n\tpublic bool GuessTypesAndMethods { get; init; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// CSS-style: identifiers may contain \u0027-\u0027, and \u0022name:\u0022 is a property.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool CssIdentifiers { get; init; }\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// No highlighting at all.\r\n\t/// \u003C/summary\u003E\r\n\tpublic bool IsPlain { get; init; }\r\n\r\n\tpublic static readonly SyntaxLanguage Plain = new() { Name = \u0022Plain Text\u0022, IsPlain = true };\r\n\r\n\tpublic static readonly SyntaxLanguage CSharp = new()\r\n\t{\r\n\t\tName = \u0022C#\u0022,\r\n\t\tLineComment = \u0022//\u0022,\r\n\t\tBlockComments = true,\r\n\t\tCSharpStrings = true,\r\n\t\tHashPreprocessor = true,\r\n\t\tGuessTypesAndMethods = true,\r\n\t\tKeywords = new()\r\n\t\t{\r\n\t\t\t\u0022abstract\u0022, \u0022as\u0022, \u0022base\u0022, \u0022bool\u0022, \u0022byte\u0022, \u0022char\u0022, \u0022checked\u0022, \u0022class\u0022, \u0022const\u0022, \u0022decimal\u0022, \u0022default\u0022, \u0022delegate\u0022,\r\n\t\t\t\u0022double\u0022, \u0022enum\u0022, \u0022event\u0022, \u0022explicit\u0022, \u0022extern\u0022, \u0022false\u0022, \u0022fixed\u0022, \u0022float\u0022, \u0022implicit\u0022, \u0022in\u0022, \u0022int\u0022, \u0022interface\u0022,\r\n\t\t\t\u0022internal\u0022, \u0022is\u0022, \u0022lock\u0022, \u0022long\u0022, \u0022namespace\u0022, \u0022new\u0022, \u0022null\u0022, \u0022object\u0022, \u0022operator\u0022, \u0022out\u0022, \u0022override\u0022, \u0022params\u0022,\r\n\t\t\t\u0022private\u0022, \u0022protected\u0022, \u0022public\u0022, \u0022readonly\u0022, \u0022ref\u0022, \u0022sbyte\u0022, \u0022sealed\u0022, \u0022short\u0022, \u0022sizeof\u0022, \u0022stackalloc\u0022, \u0022static\u0022,\r\n\t\t\t\u0022string\u0022, \u0022struct\u0022, \u0022this\u0022, \u0022true\u0022, \u0022typeof\u0022, \u0022uint\u0022, \u0022ulong\u0022, \u0022unchecked\u0022, \u0022unsafe\u0022, \u0022ushort\u0022, \u0022using\u0022, \u0022virtual\u0022,\r\n\t\t\t\u0022void\u0022, \u0022volatile\u0022, \u0022var\u0022, \u0022dynamic\u0022, \u0022async\u0022, \u0022get\u0022, \u0022set\u0022, \u0022init\u0022, \u0022value\u0022, \u0022partial\u0022, \u0022where\u0022, \u0022record\u0022, \u0022nameof\u0022,\r\n\t\t\t\u0022required\u0022, \u0022global\u0022, \u0022with\u0022, \u0022not\u0022, \u0022and\u0022, \u0022or\u0022, \u0022nint\u0022, \u0022nuint\u0022, \u0022file\u0022, \u0022scoped\u0022, \u0022field\u0022, \u0022add\u0022, \u0022remove\u0022, \u0022let\u0022,\r\n\t\t\t\u0022from\u0022, \u0022select\u0022, \u0022orderby\u0022, \u0022group\u0022, \u0022into\u0022, \u0022on\u0022, \u0022equals\u0022, \u0022by\u0022, \u0022ascending\u0022, \u0022descending\u0022, \u0022join\u0022, \u0022alias\u0022\r\n\t\t},\r\n\t\tControlKeywords = new()\r\n\t\t{\r\n\t\t\t\u0022if\u0022, \u0022else\u0022, \u0022switch\u0022, \u0022case\u0022, \u0022for\u0022, \u0022foreach\u0022, \u0022while\u0022, \u0022do\u0022, \u0022break\u0022, \u0022continue\u0022, \u0022return\u0022, \u0022goto\u0022, \u0022try\u0022,\r\n\t\t\t\u0022catch\u0022, \u0022finally\u0022, \u0022throw\u0022, \u0022yield\u0022, \u0022await\u0022, \u0022when\u0022\r\n\t\t}\r\n\t};\r\n\r\n\tpublic static readonly SyntaxLanguage Json = new()\r\n\t{\r\n\t\tName = \u0022JSON\u0022,\r\n\t\tLineComment = \u0022//\u0022,\r\n\t\tBlockComments = true,\r\n\t\tKeywords = new() { \u0022true\u0022, \u0022false\u0022, \u0022null\u0022 }\r\n\t};\r\n\r\n\tpublic static readonly SyntaxLanguage Css = new()\r\n\t{\r\n\t\tName = \u0022SCSS\u0022,\r\n\t\tLineComment = \u0022//\u0022,\r\n\t\tBlockComments = true,\r\n\t\tSingleQuoteStrings = true,\r\n\t\tCssIdentifiers = true,\r\n\t\tKeywords = new() { \u0022!important\u0022, \u0022@import\u0022, \u0022@use\u0022, \u0022@media\u0022, \u0022@keyframes\u0022, \u0022@mixin\u0022, \u0022@include\u0022, \u0022@extend\u0022, \u0022@if\u0022, \u0022@else\u0022, \u0022@each\u0022, \u0022@for\u0022, \u0022@function\u0022, \u0022@return\u0022 }\r\n\t};\r\n\r\n\tpublic static SyntaxLanguage FromPath( string path ) =\u003E Path.GetExtension( path ).ToLowerInvariant() switch\r\n\t{\r\n\t\t\u0022.cs\u0022 =\u003E CSharp,\r\n\t\t\u0022.json\u0022 or \u0022.sbproj\u0022 =\u003E Json,\r\n\t\t\u0022.scss\u0022 or \u0022.css\u0022 =\u003E Css,\r\n\t\t_ =\u003E Plain\r\n\t};\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Appends the colored tokens of \u003Cparamref name=\u0022line\u0022/\u003E to \u003Cparamref name=\u0022tokens\u0022/\u003E (plain text gets no token)\r\n\t/// and returns the state the next line starts in.\r\n\t/// \u003C/summary\u003E\r\n\tpublic LexState Tokenize( string line, LexState state, List\u003CToken\u003E tokens )\r\n\t{\r\n\t\tif ( IsPlain ) return LexState.Normal;\r\n\r\n\t\tint i = 0;\r\n\t\tint n = line.Length;\r\n\r\n\t\t// Continue whatever the previous line left open\r\n\t\tswitch ( state )\r\n\t\t{\r\n\t\t\tcase LexState.BlockComment:\r\n\t\t\t{\r\n\t\t\t\tvar end = line.IndexOf( \u0022*/\u0022, StringComparison.Ordinal );\r\n\t\t\t\tif ( end \u003C 0 ) { Add( tokens, 0, n, TokenKind.Comment ); return state; }\r\n\t\t\t\tAdd( tokens, 0, end \u002B 2, TokenKind.Comment );\r\n\t\t\t\ti = end \u002B 2;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t\tcase LexState.VerbatimString:\r\n\t\t\t{\r\n\t\t\t\tvar end = FindVerbatimEnd( line, 0 );\r\n\t\t\t\tif ( end \u003C 0 ) { Add( tokens, 0, n, TokenKind.String ); return state; }\r\n\t\t\t\tAdd( tokens, 0, end, TokenKind.String );\r\n\t\t\t\ti = end;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t\tcase LexState.RawString:\r\n\t\t\t{\r\n\t\t\t\tvar end = line.IndexOf( \u0022\\\u0022\\\u0022\\\u0022\u0022, StringComparison.Ordinal );\r\n\t\t\t\tif ( end \u003C 0 ) { Add( tokens, 0, n, TokenKind.String ); return state; }\r\n\t\t\t\tAdd( tokens, 0, end \u002B 3, TokenKind.String );\r\n\t\t\t\ti = end \u002B 3;\r\n\t\t\t\tbreak;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif ( HashPreprocessor \u0026\u0026 i == 0 )\r\n\t\t{\r\n\t\t\tvar first = 0;\r\n\t\t\twhile ( first \u003C n \u0026\u0026 char.IsWhiteSpace( line[first] ) ) first\u002B\u002B;\r\n\t\t\tif ( first \u003C n \u0026\u0026 line[first] == \u0027#\u0027 )\r\n\t\t\t{\r\n\t\t\t\tAdd( tokens, first, n - first, TokenKind.Preprocessor );\r\n\t\t\t\treturn LexState.Normal;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\twhile ( i \u003C n )\r\n\t\t{\r\n\t\t\tvar c = line[i];\r\n\r\n\t\t\tif ( char.IsWhiteSpace( c ) ) { i\u002B\u002B; continue; }\r\n\r\n\t\t\t// Comments\r\n\t\t\tif ( LineComment is not null \u0026\u0026 string.CompareOrdinal( line, i, LineComment, 0, LineComment.Length ) == 0 )\r\n\t\t\t{\r\n\t\t\t\tAdd( tokens, i, n - i, TokenKind.Comment );\r\n\t\t\t\treturn LexState.Normal;\r\n\t\t\t}\r\n\r\n\t\t\tif ( BlockComments \u0026\u0026 c == \u0027/\u0027 \u0026\u0026 i \u002B 1 \u003C n \u0026\u0026 line[i \u002B 1] == \u0027*\u0027 )\r\n\t\t\t{\r\n\t\t\t\tvar end = line.IndexOf( \u0022*/\u0022, i \u002B 2, StringComparison.Ordinal );\r\n\t\t\t\tif ( end \u003C 0 ) { Add( tokens, i, n - i, TokenKind.Comment ); return LexState.BlockComment; }\r\n\t\t\t\tAdd( tokens, i, end \u002B 2 - i, TokenKind.Comment );\r\n\t\t\t\ti = end \u002B 2;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// Strings\r\n\t\t\tif ( CSharpStrings \u0026\u0026 TryStartCSharpString( line, i, out var prefix, out var verbatim, out var raw ) )\r\n\t\t\t{\r\n\t\t\t\tvar bodyStart = i \u002B prefix;\r\n\t\t\t\tif ( raw )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar end = line.IndexOf( \u0022\\\u0022\\\u0022\\\u0022\u0022, bodyStart, StringComparison.Ordinal );\r\n\t\t\t\t\tif ( end \u003C 0 ) { Add( tokens, i, n - i, TokenKind.String ); return LexState.RawString; }\r\n\t\t\t\t\tAdd( tokens, i, end \u002B 3 - i, TokenKind.String );\r\n\t\t\t\t\ti = end \u002B 3;\r\n\t\t\t\t}\r\n\t\t\t\telse if ( verbatim )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar end = FindVerbatimEnd( line, bodyStart );\r\n\t\t\t\t\tif ( end \u003C 0 ) { Add( tokens, i, n - i, TokenKind.String ); return LexState.VerbatimString; }\r\n\t\t\t\t\tAdd( tokens, i, end - i, TokenKind.String );\r\n\t\t\t\t\ti = end;\r\n\t\t\t\t}\r\n\t\t\t\telse\r\n\t\t\t\t{\r\n\t\t\t\t\tvar end = FindQuotedEnd( line, bodyStart, \u0027\u0022\u0027 );\r\n\t\t\t\t\tAdd( tokens, i, end - i, TokenKind.String );\r\n\t\t\t\t\ti = end;\r\n\t\t\t\t}\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\tif ( c == \u0027\u0022\u0027 || (c == \u0027\\\u0027\u0027 \u0026\u0026 (CSharpStrings || SingleQuoteStrings)) )\r\n\t\t\t{\r\n\t\t\t\tvar end = FindQuotedEnd( line, i \u002B 1, c );\r\n\t\t\t\tAdd( tokens, i, end - i, TokenKind.String );\r\n\t\t\t\ti = end;\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// Numbers\r\n\t\t\tif ( char.IsDigit( c ) || (c == \u0027.\u0027 \u0026\u0026 i \u002B 1 \u003C n \u0026\u0026 char.IsDigit( line[i \u002B 1] )) )\r\n\t\t\t{\r\n\t\t\t\tvar start = i;\r\n\t\t\t\twhile ( i \u003C n \u0026\u0026 (char.IsLetterOrDigit( line[i] ) || line[i] == \u0027.\u0027 || line[i] == \u0027_\u0027) ) i\u002B\u002B;\r\n\t\t\t\tif ( CssIdentifiers \u0026\u0026 i \u003C n \u0026\u0026 line[i] == \u0027%\u0027 ) i\u002B\u002B;\r\n\t\t\t\tAdd( tokens, start, i - start, TokenKind.Number );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// CSS hex colours: #fff, #a0b1c2\r\n\t\t\tif ( CssIdentifiers \u0026\u0026 c == \u0027#\u0027 \u0026\u0026 i \u002B 1 \u003C n \u0026\u0026 Uri.IsHexDigit( line[i \u002B 1] ) )\r\n\t\t\t{\r\n\t\t\t\tvar start = i\u002B\u002B;\r\n\t\t\t\twhile ( i \u003C n \u0026\u0026 char.IsLetterOrDigit( line[i] ) ) i\u002B\u002B;\r\n\t\t\t\tAdd( tokens, start, i - start, TokenKind.Number );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\t// Words\r\n\t\t\tif ( IsIdentStart( c ) || (c == \u0027@\u0027 \u0026\u0026 i \u002B 1 \u003C n \u0026\u0026 IsIdentStart( line[i \u002B 1] )) || (CssIdentifiers \u0026\u0026 (c == \u0027$\u0027 || c == \u0027!\u0027)) )\r\n\t\t\t{\r\n\t\t\t\tvar start = i\u002B\u002B;\r\n\t\t\t\twhile ( i \u003C n \u0026\u0026 IsIdentPart( line[i] ) ) i\u002B\u002B;\r\n\t\t\t\tvar word = line.Substring( start, i - start );\r\n\t\t\t\tvar kind = ClassifyWord( line, word, i );\r\n\t\t\t\tif ( kind != TokenKind.Text ) Add( tokens, start, i - start, kind );\r\n\t\t\t\tcontinue;\r\n\t\t\t}\r\n\r\n\t\t\ti\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn LexState.Normal;\r\n\t}\r\n\r\n\tprivate TokenKind ClassifyWord( string line, string word, int after )\r\n\t{\r\n\t\tif ( ControlKeywords.Contains( word ) ) return TokenKind.ControlKeyword;\r\n\t\tif ( Keywords.Contains( word ) ) return TokenKind.Keyword;\r\n\r\n\t\tif ( CssIdentifiers )\r\n\t\t{\r\n\t\t\tif ( word[0] == \u0027$\u0027 ) return TokenKind.Type;\r\n\r\n\t\t\t// \u0022color: red\u0022 is a property, but \u0022a:hover {\u0022 is a selector\r\n\t\t\tvar isFirstOnLine = string.IsNullOrWhiteSpace( line[..(after - word.Length)] );\r\n\t\t\treturn isFirstOnLine \u0026\u0026 NextNonSpace( line, after ) == \u0027:\u0027 \u0026\u0026 !line.Contains( \u0027{\u0027 ) ? TokenKind.Property : TokenKind.Text;\r\n\t\t}\r\n\r\n\t\tif ( !GuessTypesAndMethods ) return TokenKind.Text;\r\n\r\n\t\tvar nextChar = NextNonSpace( line, after );\r\n\t\tif ( nextChar == \u0027(\u0027 ) return TokenKind.Method;\r\n\r\n\t\tvar bare = word[0] == \u0027@\u0027 ? word[1..] : word;\r\n\t\tif ( bare.Length \u003E 0 \u0026\u0026 char.IsUpper( bare[0] ) ) return TokenKind.Type;\r\n\r\n\t\treturn TokenKind.Text;\r\n\t}\r\n\r\n\tprivate bool IsIdentStart( char c ) =\u003E char.IsLetter( c ) || c == \u0027_\u0027;\r\n\tprivate bool IsIdentPart( char c ) =\u003E char.IsLetterOrDigit( c ) || c == \u0027_\u0027 || (CssIdentifiers \u0026\u0026 c == \u0027-\u0027);\r\n\r\n\tprivate static char SafeChar( string s, int i ) =\u003E i \u003C s.Length ? s[i] : \u0027\\0\u0027;\r\n\r\n\tprivate static char NextNonSpace( string line, int i )\r\n\t{\r\n\t\twhile ( i \u003C line.Length \u0026\u0026 line[i] == \u0027 \u0027 ) i\u002B\u002B;\r\n\t\treturn SafeChar( line, i );\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Recognises the C# string openers: \u0022  $\u0022  @\u0022  $@\u0022  @$\u0022  \u0022\u0022\u0022  $\u0022\u0022\u0022  (and $$\u0022\u0022\u0022 etc).\r\n\t/// \u003C/summary\u003E\r\n\tprivate static bool TryStartCSharpString( string line, int i, out int prefixLength, out bool verbatim, out bool raw )\r\n\t{\r\n\t\tprefixLength = 0;\r\n\t\tverbatim = false;\r\n\t\traw = false;\r\n\r\n\t\tvar j = i;\r\n\t\twhile ( j \u003C line.Length \u0026\u0026 (line[j] == \u0027$\u0027 || line[j] == \u0027@\u0027) )\r\n\t\t{\r\n\t\t\tif ( line[j] == \u0027@\u0027 ) verbatim = true;\r\n\t\t\tj\u002B\u002B;\r\n\t\t}\r\n\r\n\t\tif ( j \u003E= line.Length || line[j] != \u0027\u0022\u0027 )\r\n\t\t\treturn false;\r\n\r\n\t\t// Plain \u0022...\u0022 is handled by the generic quote branch\r\n\t\tif ( j == i \u0026\u0026 !(j \u002B 2 \u003C line.Length \u0026\u0026 line[j \u002B 1] == \u0027\u0022\u0027 \u0026\u0026 line[j \u002B 2] == \u0027\u0022\u0027) )\r\n\t\t\treturn false;\r\n\r\n\t\tif ( !verbatim \u0026\u0026 j \u002B 2 \u003C line.Length \u0026\u0026 line[j \u002B 1] == \u0027\u0022\u0027 \u0026\u0026 line[j \u002B 2] == \u0027\u0022\u0027 )\r\n\t\t{\r\n\t\t\traw = true;\r\n\t\t\tvar k = j;\r\n\t\t\twhile ( k \u003C line.Length \u0026\u0026 line[k] == \u0027\u0022\u0027 ) k\u002B\u002B;\r\n\t\t\tprefixLength = k - i;\r\n\t\t\treturn true;\r\n\t\t}\r\n\r\n\t\tprefixLength = j \u002B 1 - i;\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Index just past the closing quote of a verbatim string (\u0022\u0022 is an escaped quote), or -1 if it runs off the line.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static int FindVerbatimEnd( string line, int i )\r\n\t{\r\n\t\twhile ( i \u003C line.Length )\r\n\t\t{\r\n\t\t\tif ( line[i] == \u0027\u0022\u0027 )\r\n\t\t\t{\r\n\t\t\t\tif ( i \u002B 1 \u003C line.Length \u0026\u0026 line[i \u002B 1] == \u0027\u0022\u0027 ) { i \u002B= 2; continue; }\r\n\t\t\t\treturn i \u002B 1;\r\n\t\t\t}\r\n\t\t\ti\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn -1;\r\n\t}\r\n\r\n\t/// \u003Csummary\u003E\r\n\t/// Index just past the closing quote, honouring backslash escapes. Unterminated strings end at the line end.\r\n\t/// \u003C/summary\u003E\r\n\tprivate static int FindQuotedEnd( string line, int i, char quote )\r\n\t{\r\n\t\twhile ( i \u003C line.Length )\r\n\t\t{\r\n\t\t\tif ( line[i] == \u0027\\\\\u0027 ) { i \u002B= 2; continue; }\r\n\t\t\tif ( line[i] == quote ) return i \u002B 1;\r\n\t\t\ti\u002B\u002B;\r\n\t\t}\r\n\r\n\t\treturn line.Length;\r\n\t}\r\n\r\n\tprivate static void Add( List\u003CToken\u003E tokens, int start, int length, TokenKind kind )\r\n\t{\r\n\t\tif ( length \u003E 0 ) tokens.Add( new Token( start, length, kind ) );\r\n\t}\r\n}\r\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/Launcher/HotCodeInstallUi.cs","FileName":"HotCodeInstallUi.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using Sandbox;\nusing Editor;\nusing System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\n/// \u003Csummary\u003E\n/// The confirmations and messages around \u003Csee cref=\u0022HotCodeInstaller\u0022/\u003E, shared by the launcher and the editor.\n/// \u003C/summary\u003E\npublic static class HotCodeInstallUi\n{\n\t/// \u003Csummary\u003E\n\t/// Where this copy of HotCode lives: the loaded project whose .sbproj is HotCode\u0027s.\n\t/// \u003C/summary\u003E\n\tpublic static string SourceRoot\n\t{\n\t\tget\n\t\t{\n\t\t\tvar loaded = EditorUtility.Projects.GetAll()\n\t\t\t\t.Where( p =\u003E p.ConfigFilePath is not null \u0026\u0026 HotCodeInstaller.IsHotCode( p.ConfigFilePath ) )\n\t\t\t\t.Select( p =\u003E Path.GetDirectoryName( p.ConfigFilePath ) )\n\t\t\t\t.FirstOrDefault();\n\t\t\tif ( loaded is not null ) return loaded;\n\n\t\t\t// Libraries might not be in the project list: look in the open project\u0027s Libraries folder\n\t\t\tvar current = Project.Current?.GetRootPath();\n\t\t\treturn string.IsNullOrEmpty( current ) ? null : HotCodeInstaller.FindInstalled( current );\n\t\t}\n\t}\n\n\tpublic enum State\n\t{\n\t\t/// \u003Csummary\u003ENot a project HotCode can go in (a solution, or HotCode\u0027s own source).\u003C/summary\u003E\n\t\tNotApplicable,\n\t\tNotInstalled,\n\t\tInstalled\n\t}\n\n\tpublic static State StateOf( HotCodeEntry entry )\n\t{\n\t\tif ( entry.Kind != HotCodeEntryKind.Project ) return State.NotApplicable;\n\n\t\tvar source = SourceRoot;\n\t\tif ( source is not null \u0026\u0026 (ProjectFiles.IsUnder( entry.Directory, source ) || ProjectFiles.IsUnder( source, entry.Directory )) )\n\t\t\treturn State.NotApplicable;\n\n\t\ttry\n\t\t{\n\t\t\treturn HotCodeInstaller.FindInstalled( entry.Directory ) is null ? State.NotInstalled : State.Installed;\n\t\t}\n\t\tcatch ( Exception e ) when ( e is IOException or UnauthorizedAccessException )\n\t\t{\n\t\t\treturn State.NotApplicable;\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Adds the install/update/remove options for a project to a menu.\n\t/// \u003C/summary\u003E\n\tpublic static void AddMenuOptions( Menu menu, HotCodeEntry entry, Action changed )\n\t{\n\t\tswitch ( StateOf( entry ) )\n\t\t{\n\t\t\tcase State.NotInstalled:\n\t\t\t\tmenu.AddOption( \u0022Add HotCode to This Project\u0022, HotCodeLogo.IconName, () =\u003E AskInstall( entry, changed ) );\n\t\t\t\tbreak;\n\n\t\t\tcase State.Installed:\n\t\t\t\tmenu.AddOption( \u0022Update HotCode in This Project\u0022, \u0022system_update_alt\u0022, () =\u003E AskInstall( entry, changed ) );\n\t\t\t\tmenu.AddOption( \u0022Remove HotCode from This Project\u0022, \u0022remove_circle_outline\u0022, () =\u003E AskUninstall( entry, changed ) );\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tpublic static void AskInstall( HotCodeEntry entry, Action changed = null )\n\t{\n\t\tvar source = SourceRoot;\n\t\tif ( source is null )\n\t\t{\n\t\t\tShow( \u0022Add HotCode\u0022, \u0022Couldn\u0027t find HotCode\u0027s own files. Open HotCode\u0027s project (or one that has it) in s\u0026box first.\u0022 );\n\t\t\treturn;\n\t\t}\n\n\t\tvar updating = StateOf( entry ) == State.Installed;\n\t\tvar question = updating\n\t\t\t? $\u0022Update the copy of HotCode in \u0027{entry.Title}\u0027 to this version?\\n\\nThe old copy goes to the Recycle Bin.\u0022\n\t\t\t: $\u0022Add HotCode to \u0027{entry.Title}\u0027?\\n\\nThis copies HotCode into its Libraries folder, the same place the Library Manager puts libraries. Next time you open that project in s\u0026box, HotCode will be there.\u0022;\n\n\t\tDialog.AskConfirm( () =\u003E\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tvar folder = HotCodeInstaller.Install( source, entry.Directory, ProjectFiles.Delete );\n\t\t\t\tLog.Info( $\u0022[HotCode] {(updating ? \u0022Updated\u0022 : \u0022Added\u0022)} HotCode in {folder}\u0022 );\n\t\t\t\tShow( updating ? \u0022HotCode Updated\u0022 : \u0022HotCode Added\u0022,\n\t\t\t\t\t$\u0022HotCode is now in \u0027{entry.Title}\u0027.\\n\\n{folder}\\n\\nOpen (or reopen) that project in s\u0026box to use it.\u0022 );\n\t\t\t\tchanged?.Invoke();\n\t\t\t}\n\t\t\tcatch ( Exception e ) when ( e is InvalidOperationException or IOException or UnauthorizedAccessException )\n\t\t\t{\n\t\t\t\tShow( \u0022Couldn\u0027t Add HotCode\u0022, e.Message );\n\t\t\t}\n\t\t}, question, updating ? \u0022Update HotCode\u0022 : \u0022Add HotCode\u0022, updating ? \u0022Update\u0022 : \u0022Add\u0022, \u0022Cancel\u0022 );\n\t}\n\n\tpublic static void AskUninstall( HotCodeEntry entry, Action changed = null )\n\t{\n\t\tvar source = SourceRoot ?? \u0022\u0022;\n\n\t\tDialog.AskConfirm( () =\u003E\n\t\t{\n\t\t\ttry\n\t\t\t{\n\t\t\t\tif ( HotCodeInstaller.Uninstall( source, entry.Directory, ProjectFiles.Delete ) )\n\t\t\t\t\tLog.Info( $\u0022[HotCode] Removed HotCode from {entry.Directory}\u0022 );\n\t\t\t\tchanged?.Invoke();\n\t\t\t}\n\t\t\tcatch ( Exception e ) when ( e is InvalidOperationException or IOException or UnauthorizedAccessException )\n\t\t\t{\n\t\t\t\tShow( \u0022Couldn\u0027t Remove HotCode\u0022, e.Message );\n\t\t\t}\n\t\t}, $\u0022Remove HotCode from \u0027{entry.Title}\u0027?\\n\\nIts copy goes to the Recycle Bin.\u0022, \u0022Remove HotCode\u0022, \u0022Remove\u0022, \u0022Cancel\u0022 );\n\t}\n\n\tprivate static void Show( string title, string message ) =\u003E\n\t\tnew PopupWindow( title, message, \u0022OK\u0022, new Dictionary\u003Cstring, Action\u003E() ).Show();\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/CodeView/TextBuffer.cs","FileName":"TextBuffer.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Text;\n\n/// \u003Csummary\u003E\n/// A position in a \u003Csee cref=\u0022TextBuffer\u0022/\u003E: zero-based line and character index within that line.\n/// \u003C/summary\u003E\npublic readonly record struct TextPos( int Line, int Col ) : IComparable\u003CTextPos\u003E\n{\n\tpublic int CompareTo( TextPos other ) =\u003E Line != other.Line ? Line.CompareTo( other.Line ) : Col.CompareTo( other.Col );\n\n\tpublic static bool operator \u003C( TextPos a, TextPos b ) =\u003E a.CompareTo( b ) \u003C 0;\n\tpublic static bool operator \u003E( TextPos a, TextPos b ) =\u003E a.CompareTo( b ) \u003E 0;\n\tpublic static bool operator \u003C=( TextPos a, TextPos b ) =\u003E a.CompareTo( b ) \u003C= 0;\n\tpublic static bool operator \u003E=( TextPos a, TextPos b ) =\u003E a.CompareTo( b ) \u003E= 0;\n\n\tpublic static TextPos Min( TextPos a, TextPos b ) =\u003E a \u003C= b ? a : b;\n\tpublic static TextPos Max( TextPos a, TextPos b ) =\u003E a \u003E= b ? a : b;\n}\n\n/// \u003Csummary\u003E\n/// Plain text stored as a list of lines. Knows nothing about carets, undo or drawing.\n/// \u003C/summary\u003E\npublic class TextBuffer\n{\n\tpublic List\u003Cstring\u003E Lines { get; } = new() { \u0022\u0022 };\n\n\t/// \u003Csummary\u003E\n\t/// Line ending used when joining lines back into text. Detected from the loaded text.\n\t/// \u003C/summary\u003E\n\tpublic string LineEnding { get; set; } = \u0022\\n\u0022;\n\n\tpublic int LineCount =\u003E Lines.Count;\n\n\tpublic string Text\n\t{\n\t\tget =\u003E string.Join( LineEnding, Lines );\n\t\tset\n\t\t{\n\t\t\tvalue ??= \u0022\u0022;\n\t\t\tLineEnding = value.Contains( \u0022\\r\\n\u0022 ) ? \u0022\\r\\n\u0022 : \u0022\\n\u0022;\n\t\t\tLines.Clear();\n\t\t\tLines.AddRange( Normalize( value ).Split( \u0027\\n\u0027 ) );\n\t\t}\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Converts any line endings to \\n.\n\t/// \u003C/summary\u003E\n\tpublic static string Normalize( string text ) =\u003E text.Replace( \u0022\\r\\n\u0022, \u0022\\n\u0022 ).Replace( \u0027\\r\u0027, \u0027\\n\u0027 );\n\n\tpublic TextPos Clamp( TextPos p )\n\t{\n\t\tvar line = Math.Clamp( p.Line, 0, Lines.Count - 1 );\n\t\treturn new TextPos( line, Math.Clamp( p.Col, 0, Lines[line].Length ) );\n\t}\n\n\tpublic TextPos End =\u003E new( Lines.Count - 1, Lines[^1].Length );\n\n\t/// \u003Csummary\u003E\n\t/// Where text inserted at \u003Cparamref name=\u0022start\u0022/\u003E would end.\n\t/// \u003C/summary\u003E\n\tpublic static TextPos EndOf( TextPos start, string text )\n\t{\n\t\tvar lastNewline = text.LastIndexOf( \u0027\\n\u0027 );\n\t\tif ( lastNewline \u003C 0 )\n\t\t\treturn new TextPos( start.Line, start.Col \u002B text.Length );\n\n\t\tvar newlines = 0;\n\t\tforeach ( var c in text ) if ( c == \u0027\\n\u0027 ) newlines\u002B\u002B;\n\t\treturn new TextPos( start.Line \u002B newlines, text.Length - lastNewline - 1 );\n\t}\n\n\tpublic string GetText( TextPos a, TextPos b )\n\t{\n\t\tvar start = Clamp( TextPos.Min( a, b ) );\n\t\tvar end = Clamp( TextPos.Max( a, b ) );\n\n\t\tif ( start.Line == end.Line )\n\t\t\treturn Lines[start.Line].Substring( start.Col, end.Col - start.Col );\n\n\t\tvar sb = new StringBuilder();\n\t\tsb.Append( Lines[start.Line], start.Col, Lines[start.Line].Length - start.Col );\n\t\tfor ( int i = start.Line \u002B 1; i \u003C end.Line; i\u002B\u002B )\n\t\t\tsb.Append( \u0027\\n\u0027 ).Append( Lines[i] );\n\t\tsb.Append( \u0027\\n\u0027 ).Append( Lines[end.Line], 0, end.Col );\n\t\treturn sb.ToString();\n\t}\n\n\tpublic void Delete( TextPos a, TextPos b )\n\t{\n\t\tvar start = Clamp( TextPos.Min( a, b ) );\n\t\tvar end = Clamp( TextPos.Max( a, b ) );\n\t\tif ( start == end ) return;\n\n\t\tvar head = Lines[start.Line][..start.Col];\n\t\tvar tail = Lines[end.Line][end.Col..];\n\t\tLines.RemoveRange( start.Line \u002B 1, end.Line - start.Line );\n\t\tLines[start.Line] = head \u002B tail;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Inserts text (which may contain newlines) and returns the position just after it.\n\t/// \u003C/summary\u003E\n\tpublic TextPos Insert( TextPos at, string text )\n\t{\n\t\tat = Clamp( at );\n\t\ttext = Normalize( text );\n\t\tif ( text.Length == 0 ) return at;\n\n\t\tvar line = Lines[at.Line];\n\t\tvar head = line[..at.Col];\n\t\tvar tail = line[at.Col..];\n\t\tvar parts = text.Split( \u0027\\n\u0027 );\n\n\t\tif ( parts.Length == 1 )\n\t\t{\n\t\t\tLines[at.Line] = head \u002B text \u002B tail;\n\t\t\treturn new TextPos( at.Line, at.Col \u002B text.Length );\n\t\t}\n\n\t\tLines[at.Line] = head \u002B parts[0];\n\t\tfor ( int i = 1; i \u003C parts.Length - 1; i\u002B\u002B )\n\t\t\tLines.Insert( at.Line \u002B i, parts[i] );\n\t\tLines.Insert( at.Line \u002B parts.Length - 1, parts[^1] \u002B tail );\n\n\t\treturn new TextPos( at.Line \u002B parts.Length - 1, parts[^1].Length );\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/CodeView/UndoHistory.cs","FileName":"UndoHistory.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\n\n/// \u003Csummary\u003E\n/// One change to a \u003Csee cref=\u0022TextBuffer\u0022/\u003E: at \u003Csee cref=\u0022Start\u0022/\u003E, \u003Csee cref=\u0022Removed\u0022/\u003E was replaced by \u003Csee cref=\u0022Inserted\u0022/\u003E.\n/// \u003C/summary\u003E\npublic readonly record struct BufferEdit( TextPos Start, string Removed, string Inserted );\n\n/// \u003Csummary\u003E\n/// What kind of edit this was, which decides whether it can merge with the one before it.\n/// \u003C/summary\u003E\npublic enum EditKind\n{\n\t/// \u003Csummary\u003EPaste, cut, indent, new line... always its own undo step.\u003C/summary\u003E\n\tOther,\n\tTyping,\n\tDeleting\n}\n\n/// \u003Csummary\u003E\n/// One undo step. Usually a single edit, but typing a word, backspacing, or a \u0022replace all\u0022\n/// becomes one group.\n/// \u003C/summary\u003E\npublic class UndoGroup\n{\n\tpublic int Id { get; init; }\n\tpublic EditKind Kind { get; init; }\n\tpublic List\u003CBufferEdit\u003E Edits { get; } = new();\n\tpublic TextPos CaretBefore { get; init; }\n\tpublic TextPos? AnchorBefore { get; init; }\n\tpublic TextPos CaretAfter { get; set; }\n\tpublic TextPos? AnchorAfter { get; set; }\n\tpublic double Time { get; set; }\n}\n\n/// \u003Csummary\u003E\n/// Undo/redo stacks with editor-style merging:\n/// \u003Clist type=\u0022bullet\u0022\u003E\n/// \u003Citem\u003Etyping merges per word (\u0022hello world\u0022 undoes as \u0022world\u0022, then \u0022hello \u0022)\u003C/item\u003E\n/// \u003Citem\u003Ebackspace / delete runs merge\u003C/item\u003E\n/// \u003Citem\u003Emoving the caret, saving, pausing, or doing anything else starts a new step\u003C/item\u003E\n/// \u003C/list\u003E\n/// Also tracks the save point, so undoing back to the saved text counts as unmodified.\n/// \u003C/summary\u003E\npublic class UndoHistory\n{\n\t/// \u003Csummary\u003E\n\t/// A pause longer than this starts a new undo step.\n\t/// \u003C/summary\u003E\n\tpublic const double MergeTimeout = 3.0;\n\n\tconst int MaxSteps = 1000;\n\n\tprivate readonly List\u003CUndoGroup\u003E _undo = new();\n\tprivate readonly List\u003CUndoGroup\u003E _redo = new();\n\tprivate int _nextId = 1;\n\tprivate int _savedId;\n\tprivate bool _breakNext = true;\n\n\tprivate UndoGroup _transaction;\n\tprivate int _transactionDepth;\n\n\t/// \u003Csummary\u003E\n\t/// Id of the text as it is now; 0 is the text as loaded.\n\t/// \u003C/summary\u003E\n\tpublic int CurrentId =\u003E _undo.Count \u003E 0 ? _undo[^1].Id : 0;\n\n\t/// \u003Csummary\u003E\n\t/// Differs from what was last loaded or saved.\n\t/// \u003C/summary\u003E\n\tpublic bool IsModified =\u003E CurrentId != _savedId;\n\n\tpublic bool CanUndo =\u003E _undo.Count \u003E 0;\n\tpublic bool CanRedo =\u003E _redo.Count \u003E 0;\n\n\tpublic void Clear()\n\t{\n\t\t_undo.Clear();\n\t\t_redo.Clear();\n\t\t_savedId = 0;\n\t\t_breakNext = true;\n\t}\n\n\tpublic void MarkSaved()\n\t{\n\t\t_savedId = CurrentId;\n\t\t_breakNext = true;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// The next edit starts a new undo step (call when the caret moves, etc).\n\t/// \u003C/summary\u003E\n\tpublic void Break() =\u003E _breakNext = true;\n\n\t/// \u003Csummary\u003E\n\t/// Everything recorded until the matching \u003Csee cref=\u0022EndTransaction\u0022/\u003E becomes one undo step.\n\t/// \u003C/summary\u003E\n\tpublic void BeginTransaction( TextPos caret, TextPos? anchor, double now )\n\t{\n\t\tif ( _transactionDepth\u002B\u002B \u003E 0 ) return;\n\t\t_transaction = new UndoGroup { Id = _nextId\u002B\u002B, Kind = EditKind.Other, CaretBefore = caret, AnchorBefore = anchor, CaretAfter = caret, Time = now };\n\t}\n\n\tpublic void EndTransaction( TextPos caret, TextPos? anchor )\n\t{\n\t\tif ( _transactionDepth == 0 || --_transactionDepth \u003E 0 ) return;\n\n\t\tvar group = _transaction;\n\t\t_transaction = null;\n\t\tif ( group.Edits.Count == 0 ) return;\n\n\t\tgroup.CaretAfter = caret;\n\t\tgroup.AnchorAfter = anchor;\n\t\tPush( group );\n\t\t_breakNext = true;\n\t}\n\n\tpublic void Record( BufferEdit edit, EditKind kind, TextPos caretBefore, TextPos? anchorBefore, TextPos caretAfter, double now )\n\t{\n\t\tif ( _transaction is not null )\n\t\t{\n\t\t\t_transaction.Edits.Add( edit );\n\t\t\t_transaction.CaretAfter = caretAfter;\n\t\t\treturn;\n\t\t}\n\n\t\tvar top = _undo.Count \u003E 0 ? _undo[^1] : null;\n\t\tif ( top is not null \u0026\u0026 CanMerge( top, edit, kind, now ) )\n\t\t{\n\t\t\ttop.Edits.Add( edit );\n\t\t\ttop.CaretAfter = caretAfter;\n\t\t\ttop.AnchorAfter = null;\n\t\t\ttop.Time = now;\n\t\t\t_redo.Clear();\n\t\t\treturn;\n\t\t}\n\n\t\tvar group = new UndoGroup { Id = _nextId\u002B\u002B, Kind = kind, CaretBefore = caretBefore, AnchorBefore = anchorBefore, CaretAfter = caretAfter, Time = now };\n\t\tgroup.Edits.Add( edit );\n\t\tPush( group );\n\n\t\t// Typing and deleting stay open for merging; anything else stands alone\n\t\t_breakNext = kind == EditKind.Other;\n\t}\n\n\tprivate void Push( UndoGroup group )\n\t{\n\t\t_undo.Add( group );\n\t\t_redo.Clear();\n\t\tif ( _undo.Count \u003E MaxSteps ) _undo.RemoveAt( 0 );\n\t}\n\n\tprivate bool CanMerge( UndoGroup top, BufferEdit edit, EditKind kind, double now )\n\t{\n\t\tif ( _breakNext || kind == EditKind.Other || top.Kind != kind ) return false;\n\t\tif ( now - top.Time \u003E MergeTimeout ) return false;\n\n\t\t// Never merge into the saved state, or undo would step past it\n\t\tif ( top.Id == _savedId ) return false;\n\n\t\tvar last = top.Edits[^1];\n\n\t\tif ( kind == EditKind.Typing )\n\t\t{\n\t\t\tif ( edit.Removed.Length != 0 || edit.Inserted.Contains( \u0027\\n\u0027 ) ) return false;\n\t\t\tif ( edit.Start != TextBuffer.EndOf( last.Start, last.Inserted ) ) return false;\n\n\t\t\t// Start a new step at each new word: \u0022hello |world\u0022\n\t\t\tvar lastChar = last.Inserted.Length \u003E 0 ? last.Inserted[^1] : \u0027 \u0027;\n\t\t\treturn !(char.IsWhiteSpace( lastChar ) \u0026\u0026 !char.IsWhiteSpace( edit.Inserted[0] ));\n\t\t}\n\n\t\tif ( kind == EditKind.Deleting )\n\t\t{\n\t\t\tif ( edit.Inserted.Length != 0 || edit.Removed.Contains( \u0027\\n\u0027 ) ) return false;\n\n\t\t\tvar isBackspace = TextBuffer.EndOf( edit.Start, edit.Removed ) == last.Start;\n\t\t\tvar isForwardDelete = edit.Start == last.Start;\n\t\t\treturn isBackspace || isForwardDelete;\n\t\t}\n\n\t\treturn false;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Reverts the last step. Returns it so the caller can restore the caret, or null if there\u0027s nothing to undo.\n\t/// \u003C/summary\u003E\n\tpublic UndoGroup Undo( TextBuffer buffer )\n\t{\n\t\tif ( _undo.Count == 0 ) return null;\n\n\t\tvar group = _undo[^1];\n\t\t_undo.RemoveAt( _undo.Count - 1 );\n\n\t\tfor ( int i = group.Edits.Count - 1; i \u003E= 0; i-- )\n\t\t{\n\t\t\tvar e = group.Edits[i];\n\t\t\tbuffer.Delete( e.Start, TextBuffer.EndOf( e.Start, e.Inserted ) );\n\t\t\tbuffer.Insert( e.Start, e.Removed );\n\t\t}\n\n\t\t_redo.Add( group );\n\t\t_breakNext = true;\n\t\treturn group;\n\t}\n\n\tpublic UndoGroup Redo( TextBuffer buffer )\n\t{\n\t\tif ( _redo.Count == 0 ) return null;\n\n\t\tvar group = _redo[^1];\n\t\t_redo.RemoveAt( _redo.Count - 1 );\n\n\t\tforeach ( var e in group.Edits )\n\t\t{\n\t\t\tbuffer.Delete( e.Start, TextBuffer.EndOf( e.Start, e.Removed ) );\n\t\t\tbuffer.Insert( e.Start, e.Inserted );\n\t\t}\n\n\t\t_undo.Add( group );\n\t\t_breakNext = true;\n\t\treturn group;\n\t}\n}\n"},{"Ident":"chrinteractive.hotcode","Path":"Editor/HotCodeEditor/Navigation/XmlDocs.cs","FileName":"XmlDocs.cs","PackageType":"library","CodeKind":"Editor","AssetVersionId":399294,"IsPrivate":false,"Code":"using System;\nusing System.Collections.Generic;\nusing System.Globalization;\nusing System.IO;\nusing System.Threading;\nusing System.Xml;\nusing System.Xml.Linq;\nusing Microsoft.CodeAnalysis;\n\n/// \u003Csummary\u003E\n/// Hands Roslyn the doc comments from a compiler-generated XML doc file (Foo.xml next to Foo.dll).\n/// Roslyn\u0027s own XmlDocumentationProvider is in an assembly HotCode doesn\u0027t reference, and this is all it does.\n/// The file is read on first use, not when the reference is created.\n/// \u003C/summary\u003E\npublic sealed class XmlDocs : DocumentationProvider\n{\n\tprivate readonly string _path;\n\tprivate readonly string _text;\n\tprivate Dictionary\u003Cstring, string\u003E _members;\n\n\tprivate XmlDocs( string path, string text )\n\t{\n\t\t_path = path;\n\t\t_text = text;\n\t}\n\n\tpublic static XmlDocs FromFile( string path ) =\u003E new( path, null );\n\tpublic static XmlDocs FromText( string xml ) =\u003E new( null, xml );\n\n\tprotected override string GetDocumentationForSymbol( string documentationMemberID, CultureInfo preferredCulture, CancellationToken cancellationToken = default )\n\t{\n\t\tvar members = LazyInitializer.EnsureInitialized( ref _members, Load );\n\t\treturn members.TryGetValue( documentationMemberID, out var xml ) ? xml : \u0022\u0022;\n\t}\n\n\t/// \u003Csummary\u003E\n\t/// Member id (\u0022M:Sandbox.GameObject.Destroy\u0022) -\u003E its \u0026lt;member\u0026gt; element as XML.\n\t/// \u003C/summary\u003E\n\tprivate Dictionary\u003Cstring, string\u003E Load()\n\t{\n\t\tvar members = new Dictionary\u003Cstring, string\u003E( StringComparer.Ordinal );\n\n\t\ttry\n\t\t{\n\t\t\tvar doc = _text is not null ? XDocument.Parse( _text ) : XDocument.Load( _path );\n\t\t\tforeach ( var member in doc.Descendants( \u0022member\u0022 ) )\n\t\t\t{\n\t\t\t\tvar name = (string)member.Attribute( \u0022name\u0022 );\n\t\t\t\tif ( !string.IsNullOrEmpty( name ) ) members[name] = member.ToString( SaveOptions.DisableFormatting );\n\t\t\t}\n\t\t}\n\t\tcatch ( Exception e ) when ( e is XmlException or IOException or UnauthorizedAccessException )\n\t\t{\n\t\t\t// No docs is fine; hints just show without them\n\t\t}\n\n\t\treturn members;\n\t}\n\n\tpublic override bool Equals( object obj ) =\u003E obj is XmlDocs other \u0026\u0026 other._path == _path \u0026\u0026 ReferenceEquals( other._text, _text );\n\tpublic override int GetHashCode() =\u003E HashCode.Combine( _path, _text );\n}\n"}]}