using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Threading; namespace codelanguage; public partial class EnhancedCodeEditor : UserControl, IFindReplaceTarget { // ── Text buffer (list of lines) ─────────────────────────────────────────── private readonly List _lines = new() { "" }; public string SourceCode { get => string.Join("\n", _lines); set { // Programmatic load (file open, tab switch): reset caret and history. SetLines(value ?? ""); _caretLine = 0; _caretColumn = 0; ClearSelection(); _history.Clear(); _searchMatchRects.Clear(); InvalidateCanvas(); RaiseUndoStateChanged(); } } // ── Visual metrics ─────────────────────────────────────────────────────── private static readonly FontFamily EditorFont = new("Consolas"); private static readonly Typeface EditorTypeface = new(EditorFont, FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); private const double EditorFontSizeVal = 14.0; private const int GutterWidth = 48; private const int TabWidth = 4; private const double TextPad = 8.0; private double _charW; private double _lineH; private double _baseline; // ── Caret ───────────────────────────────────────────────────────────────── private int _caretLine; private int _caretColumn; private int _caretDesiredX = -1; private bool _caretVisible = true; private DispatcherTimer? _caretTimer; // ── Selection ───────────────────────────────────────────────────────────── private bool _hasSelection; private int _selStartLine, _selStartCol; private int _selEndLine, _selEndCol; private bool _isSelecting; // ── Syntax highlighting ────────────────────────────────────────────────── private readonly SyntaxHighlighter _highlighter = new(); private List? _cachedTokens; private int _cachedLineCount; // ── Breakpoints / debug ────────────────────────────────────────────────── private readonly Dictionary _breakpoints = new(); private int _currentLineHighlight = -1; public event EventHandler? BreakpointToggled; public event EventHandler? CodeChanged; // ── Completion ──────────────────────────────────────────────────────────── private readonly CodeCompletionService _completionService = new(); private bool _completionActive; // ── Constructor ─────────────────────────────────────────────────────────── public EnhancedCodeEditor() { InitializeComponent(); RenderSurface.RenderAction = RenderEditorContent; Loaded += OnLoaded; SizeChanged += (_, _) => InvalidateCanvas(); PreviewKeyDown += OnPreviewKeyDown; TextInput += OnTextInput; MouseLeftButtonDown += OnMouseDown; MouseMove += OnMouseMove; MouseLeftButtonUp += OnMouseUp; CompletionDropdown.ItemSelected += OnCompletionItemSelected; CompletionDropdown.CompletionCancelled += OnCompletionCancelled; TextArea = new EditorTextArea(this); Document = new EditorDocument(this); ComputeMetrics(); } private void OnLoaded(object sender, RoutedEventArgs e) { ComputeMetrics(); InvalidateCanvas(); Dispatcher.BeginInvoke(new Action(() => Keyboard.Focus(this)), DispatcherPriority.Input); StartCaretBlink(); } // ── Metrics ─────────────────────────────────────────────────────────────── private void ComputeMetrics() { double dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip; var ft = new FormattedText("W", System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, EditorTypeface, EditorFontSizeVal, Brushes.White, dpi); _charW = ft.Width; _lineH = ft.Height * 1.25; _baseline = ft.Baseline; } private double ColumnToX(int lineIdx, int col) { string line = _lines[lineIdx]; double x = 0; for (int i = 0; i < col && i < line.Length; i++) x += line[i] == '\t' ? _charW * TabWidth : _charW; return x; } private (int col, double x) XToColumn(int lineIdx, double targetX) { string line = _lines[lineIdx]; double x = 0; for (int i = 0; i < line.Length; i++) { double w = line[i] == '\t' ? _charW * TabWidth : _charW; if (x + w / 2 > targetX) return (i, x); x += w; } return (line.Length, x); } private double LineRenderWidth(string line) { double w = 0; foreach (char c in line) w += c == '\t' ? _charW * TabWidth : _charW; return w; } // ── Rendering ───────────────────────────────────────────────────────────── private void InvalidateCanvas() => RenderSurface?.InvalidateVisual(); private void RenderEditorContent(DrawingContext dc) { if (ScrollViewer == null) return; double dpi = VisualTreeHelper.GetDpi(this).PixelsPerDip; double viewW = ScrollViewer.ViewportWidth; double viewH = ScrollViewer.ViewportHeight; double scrollY = ScrollViewer.VerticalOffset; int totalLines = _lines.Count; double contentW = Math.Max(viewW, GutterWidth + TextPad + (totalLines > 0 ? _lines.Max(LineRenderWidth) : 0) + 20); double contentH = totalLines * _lineH + 4; RenderSurface.Width = contentW; RenderSurface.Height = contentH; // Background dc.DrawRectangle(GetBrush("Background"), null, new Rect(0, 0, contentW, contentH)); // Gutter dc.DrawRectangle(GetBrush("LineNumberBackground"), null, new Rect(0, 0, GutterWidth, contentH)); int firstVis = Math.Max(0, (int)(scrollY / _lineH)); int lastVis = Math.Min(totalLines - 1, (int)((scrollY + viewH) / _lineH) + 1); // Current-line highlight if (_currentLineHighlight >= firstVis && _currentLineHighlight <= lastVis) { double y = (_currentLineHighlight - 1) * _lineH; dc.DrawRectangle(GetBrush("CurrentLine"), null, new Rect(GutterWidth, y, contentW - GutterWidth, _lineH)); } // Search-match highlights (drawn under the selection) if (_searchMatchRects.Count > 0) { var matchBrush = GetBrush("FindHighlight"); foreach (var (mLine, mFrom, mTo) in _searchMatchRects) { if (mLine < firstVis || mLine > lastVis) continue; double sx = GutterWidth + TextPad + ColumnToX(mLine, mFrom); double ex = GutterWidth + TextPad + ColumnToX(mLine, mTo); dc.DrawRectangle(matchBrush, null, new Rect(sx, mLine * _lineH, ex - sx, _lineH)); } } // Syntax tokens var tokens = GetHighlightTokens(); for (int i = firstVis; i <= lastVis; i++) { double y = i * _lineH; string line = _lines[i]; // Gutter text bool bp = _breakpoints.ContainsKey(i + 1); bool isCurLine = (i + 1) == _currentLineHighlight; string gutterText = bp ? "●" : (isCurLine ? "▶" : (i + 1).ToString()); var gft = new FormattedText(gutterText, System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, EditorTypeface, EditorFontSizeVal - 2, bp ? GetBrush("Breakpoint") : (isCurLine ? GetBrush("Text") : GetBrush("LineNumberText")), dpi); dc.DrawText(gft, new Point(GutterWidth - 10 - gft.Width, y)); // Selection background (int selStart, int selEnd) = GetLineSelection(i); if (selStart >= 0) { double sx = GutterWidth + TextPad + ColumnToX(i, selStart); double ex = GutterWidth + TextPad + ColumnToX(i, selEnd); dc.DrawRectangle(GetBrush("Selection"), null, new Rect(sx, i * _lineH, ex - sx, _lineH)); } if (line.Length == 0) continue; // Draw text with syntax highlighting var lineTokens = tokens?.Where(t => t.StartLine == i).OrderBy(t => t.StartColumn).ToList(); double tx = GutterWidth + TextPad; int lastCol = 0; if (lineTokens != null && lineTokens.Count > 0) { foreach (var tok in lineTokens) { if (tok.StartColumn > lastCol) RenderTextSegment(dc, line, lastCol, tok.StartColumn - lastCol, ref tx, y, dpi, null); int tokEnd = tok.StartColumn + tok.Length; RenderTextSegment(dc, line, tok.StartColumn, tok.Length - Math.Max(0, tok.StartColumn + tok.Length - line.Length), ref tx, y, dpi, tok.Type); lastCol = tokEnd; } } if (lastCol < line.Length) RenderTextSegment(dc, line, lastCol, line.Length - lastCol, ref tx, y, dpi, null); } // Caret if (_caretVisible && IsFocused) { double cx = GutterWidth + TextPad + ColumnToX(_caretLine, _caretColumn); double cy = _caretLine * _lineH; dc.DrawRectangle(Brushes.White, null, new Rect(cx, cy, 2, _lineH - 4)); } } private void RenderTextSegment(DrawingContext dc, string line, int start, int len, ref double x, double y, double dpi, HighlightTokenType? tokenType) { int end = Math.Min(start + len, line.Length); for (int i = start; i < end; i++) { if (line[i] == '\t') { x += _charW * TabWidth; } else { int segStart = i; while (i < end && line[i] != '\t') i++; int segLen = Math.Min(i, end) - segStart; if (segLen > 0) { Brush brush = tokenType.HasValue ? new SolidColorBrush(VisualThemeManager.GetSyntaxColor(tokenType.Value)) : GetBrush("Text"); var ft = new FormattedText(line.Substring(segStart, segLen), System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, EditorTypeface, EditorFontSizeVal, brush, dpi); dc.DrawText(ft, new Point(x, y)); x += segLen * _charW; } i--; } } } private (int start, int end) GetLineSelection(int lineIdx) { if (!_hasSelection) return (-1, -1); int aL = _selStartLine, aC = _selStartCol; int bL = _selEndLine, bC = _selEndCol; if (aL > bL || (aL == bL && aC > bC)) { (aL, bL) = (bL, aL); (aC, bC) = (bC, aC); } if (lineIdx < aL || lineIdx > bL) return (-1, -1); if (lineIdx == aL && lineIdx == bL) return (aC, bC); if (lineIdx == aL) return (aC, _lines[aL].Length); if (lineIdx == bL) return (0, bC); return (0, _lines[lineIdx].Length); } private List? GetHighlightTokens() { if (_cachedTokens == null || _cachedLineCount != _lines.Count) { _cachedTokens = _highlighter.HighlightCode(SourceCode); _cachedLineCount = _lines.Count; } return _cachedTokens; } // ── Caret ───────────────────────────────────────────────────────────────── private void StartCaretBlink() { _caretTimer?.Stop(); _caretTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(530), DispatcherPriority.Normal, (_, _) => { _caretVisible = !_caretVisible; InvalidateCanvas(); }, Dispatcher); _caretTimer.Start(); } private void MoveCaret(int line, int col) { line = Math.Clamp(line, 0, _lines.Count - 1); col = Math.Clamp(col, 0, _lines[line].Length); _caretLine = line; _caretColumn = col; _caretDesiredX = -1; _caretVisible = true; _caretTimer?.Stop(); _caretTimer?.Start(); ScrollIntoView(); InvalidateCanvas(); } private void ScrollIntoView() { if (ScrollViewer == null) return; double x = GutterWidth + TextPad + ColumnToX(_caretLine, _caretColumn); double y = _caretLine * _lineH; if (x < ScrollViewer.HorizontalOffset + 10) ScrollViewer.ScrollToHorizontalOffset(Math.Max(0, x - 20)); else if (x > ScrollViewer.HorizontalOffset + ScrollViewer.ViewportWidth - 30) ScrollViewer.ScrollToHorizontalOffset(x - ScrollViewer.ViewportWidth + 30); if (y < ScrollViewer.VerticalOffset) ScrollViewer.ScrollToVerticalOffset(y); else if (y + _lineH > ScrollViewer.VerticalOffset + ScrollViewer.ViewportHeight - 10) ScrollViewer.ScrollToVerticalOffset(y + _lineH - ScrollViewer.ViewportHeight + 10); } // ── Selection ───────────────────────────────────────────────────────────── private void BeginSelection(int line, int col) { _hasSelection = true; _selStartLine = _selEndLine = line; _selStartCol = _selEndCol = col; } private void ExtendSelection(int line, int col) { _selEndLine = line; _selEndCol = col; } private void ClearSelection() { _hasSelection = false; } public string GetSelectedText() { if (!_hasSelection) return ""; int aL = _selStartLine, aC = _selStartCol; int bL = _selEndLine, bC = _selEndCol; if (aL > bL || (aL == bL && aC > bC)) { (aL, bL) = (bL, aL); (aC, bC) = (bC, aC); } if (aL == bL) return _lines[aL][aC..bC]; var parts = new List { _lines[aL][aC..] }; for (int i = aL + 1; i < bL; i++) parts.Add(_lines[i]); parts.Add(_lines[bL][..bC]); return string.Join("\n", parts); } private void DeleteSelection() { if (!_hasSelection) return; int aL = _selStartLine, aC = _selStartCol; int bL = _selEndLine, bC = _selEndCol; if (aL > bL || (aL == bL && aC > bC)) { (aL, bL) = (bL, aL); (aC, bC) = (bC, aC); } if (aL == bL) { _lines[aL] = _lines[aL].Remove(aC, bC - aC); } else { string first = _lines[aL][..aC]; string last = _lines[bL][bC..]; _lines.RemoveRange(aL + 1, bL - aL); _lines[aL] = first + last; } _caretLine = aL; _caretColumn = aC; ClearSelection(); _cachedTokens = null; } // ── Edit operations ─────────────────────────────────────────────────────── private void InsertText(string text) { if (_hasSelection) DeleteSelection(); string line = _lines[_caretLine]; _lines[_caretLine] = line.Insert(_caretColumn, text); _caretColumn += text.Length; _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); } private void InsertNewLine() { RunEdit(EditKind.Other, () => { if (_hasSelection) DeleteSelection(); string line = _lines[_caretLine]; string before = line[.._caretColumn]; string after = line[_caretColumn..]; string indent = ""; _lines[_caretLine] = before; _lines.Insert(_caretLine + 1, indent + after); _caretLine++; _caretColumn = indent.Length; _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); }); } private void DoBackspace() { if (_hasSelection) { RunEdit(EditKind.Other, () => { DeleteSelection(); _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); }); return; } if (_caretColumn == 0 && _caretLine == 0) return; RunEdit(_caretColumn > 0 ? EditKind.DeleteBackward : EditKind.Other, () => { if (_caretColumn == 0) { int prevLen = _lines[_caretLine - 1].Length; _lines[_caretLine - 1] += _lines[_caretLine]; _lines.RemoveAt(_caretLine); _caretLine--; _caretColumn = prevLen; } else { _lines[_caretLine] = _lines[_caretLine].Remove(_caretColumn - 1, 1); _caretColumn--; } _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); }); } private void DoDelete() { if (_hasSelection) { RunEdit(EditKind.Other, () => { DeleteSelection(); _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); }); return; } string line = _lines[_caretLine]; RunEdit(_caretColumn < line.Length ? EditKind.DeleteForward : EditKind.Other, () => { if (_caretColumn < line.Length) _lines[_caretLine] = line.Remove(_caretColumn, 1); else if (_caretLine < _lines.Count - 1) { _lines[_caretLine] += _lines[_caretLine + 1]; _lines.RemoveAt(_caretLine + 1); } _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); }); } private void DoTab() { RunEdit(EditKind.Other, () => { if (_hasSelection) { int aL = Math.Min(_selStartLine, _selEndLine); int bL = Math.Max(_selStartLine, _selEndLine); for (int i = aL; i <= bL; i++) _lines[i] = "\t" + _lines[i]; MoveCaret(_caretLine, _caretColumn + 1); _selStartCol++; _selEndCol++; } else { InsertText("\t"); } }); } private void DoUntab() { RunEdit(EditKind.Other, () => { if (_hasSelection) { int aL = Math.Min(_selStartLine, _selEndLine); int bL = Math.Max(_selStartLine, _selEndLine); for (int i = aL; i <= bL; i++) if (_lines[i].StartsWith('\t')) _lines[i] = _lines[i][1..]; if (_caretColumn > 0) MoveCaret(_caretLine, _caretColumn - 1); if (_selStartCol > 0) _selStartCol--; if (_selEndCol > 0) _selEndCol--; } else { if (_lines[_caretLine].StartsWith('\t')) { _lines[_caretLine] = _lines[_caretLine][1..]; if (_caretColumn > 0) _caretColumn--; _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); } } }); } // ── Completion dropdown handlers ───────────────────────────────────────── private void ShowCompletions(bool forceAll) { string src = SourceCode; _completionService.AnalyzeCode(src); string prefix = GetCurrentWord(); var context = CompletionContext.All; var items = _completionService.GetCompletions(prefix, context); if (items.Count == 0 && !forceAll) return; double cx = GutterWidth + TextPad + ColumnToX(_caretLine, _caretColumn); double cy = _caretLine * _lineH; CompletionDropdown.ShowCompletions(items, (int)cx, (int)cy); _completionActive = true; } private void ShowMemberCompletions(string objectName) { _completionService.AnalyzeCode(SourceCode); var items = _completionService.GetMemberAccessCompletions(objectName); if (items.Count == 0) return; double cx = GutterWidth + TextPad + ColumnToX(_caretLine, _caretColumn); double cy = _caretLine * _lineH; CompletionDropdown.ShowCompletions(items, (int)cx, (int)cy); _completionActive = true; } private string GetCurrentWord() { if (_caretLine >= _lines.Count || _caretLine < 0) return ""; string line = _lines[_caretLine]; int col = Math.Min(_caretColumn, line.Length); int start = col; while (start > 0 && (char.IsLetterOrDigit(line[start - 1]) || line[start - 1] == '_')) start--; return line[start..col]; } private void OnCompletionItemSelected(object? sender, CompletionItemUI item) { if (item == null) return; // Remove the current word prefix and insert the completion (one undo step) RunEdit(EditKind.Other, () => { string line = _lines[_caretLine]; int col = Math.Min(_caretColumn, line.Length); int start = col; while (start > 0 && (char.IsLetterOrDigit(line[start - 1]) || line[start - 1] == '_')) start--; if (start < col) { _lines[_caretLine] = line.Remove(start, col - start); _caretColumn = start; } InsertText(item.InsertText); }); _completionActive = false; } private void OnCompletionCancelled(object? sender, EventArgs e) { _completionActive = false; } // ── Keyboard ────────────────────────────────────────────────────────────── private void OnPreviewKeyDown(object sender, KeyEventArgs e) { bool ctrl = Keyboard.Modifiers == ModifierKeys.Control; bool shift = Keyboard.Modifiers == ModifierKeys.Shift; // If completion dropdown is visible, let it handle navigation/accept keys if (_completionActive && CompletionDropdown.Visibility == Visibility.Visible) { if (e.Key == Key.Up || e.Key == Key.Down || e.Key == Key.Enter || e.Key == Key.Tab || e.Key == Key.Escape) { return; // let event tunnel down to dropdown's PreviewKeyDown } CompletionDropdown.Hide(); _completionActive = false; } // Ctrl+Space = completion if (e.Key == Key.Space && ctrl) { ShowCompletions(forceAll: true); e.Handled = true; return; } // Ctrl+Z / Ctrl+Y (Ctrl+Shift+Z) = undo / redo if (e.Key == Key.Z && Keyboard.Modifiers == ModifierKeys.Control) { Undo(); e.Handled = true; return; } if ((e.Key == Key.Y && Keyboard.Modifiers == ModifierKeys.Control) || (e.Key == Key.Z && Keyboard.Modifiers == (ModifierKeys.Control | ModifierKeys.Shift))) { Redo(); e.Handled = true; return; } // Ctrl+A = select all if (e.Key == Key.A && ctrl) { _hasSelection = true; _selStartLine = 0; _selStartCol = 0; _selEndLine = _lines.Count - 1; _selEndCol = _lines[^1].Length; InvalidateCanvas(); e.Handled = true; return; } // Ctrl+C = copy if (e.Key == Key.C && ctrl) { CopySel(); e.Handled = true; return; } // Ctrl+X = cut if (e.Key == Key.X && ctrl) { Cut(); e.Handled = true; return; } // Ctrl+V = paste if (e.Key == Key.V && ctrl) { PasteText(); e.Handled = true; return; } // Tab / Shift+Tab if (e.Key == Key.Tab && shift) { DoUntab(); e.Handled = true; return; } if (e.Key == Key.Tab && !shift) { DoTab(); e.Handled = true; return; } // Enter if (e.Key == Key.Enter) { InsertNewLine(); e.Handled = true; return; } // Backspace / Delete if (e.Key == Key.Back) { DoBackspace(); e.Handled = true; return; } if (e.Key == Key.Delete) { DoDelete(); e.Handled = true; return; } // Arrow keys if (e.Key == Key.Left) { MoveCaretHorizontal(-1, shift); e.Handled = true; return; } if (e.Key == Key.Right) { MoveCaretHorizontal(1, shift); e.Handled = true; return; } if (e.Key == Key.Up) { MoveCaretVertical(-1, shift); e.Handled = true; return; } if (e.Key == Key.Down) { MoveCaretVertical(1, shift); e.Handled = true; return; } // Home / End if (e.Key == Key.Home) { MoveCaret(ctrl ? 0 : _caretLine, 0); if (shift) ExtendOrBeginSelection(); else ClearSelection(); InvalidateCanvas(); e.Handled = true; return; } if (e.Key == Key.End) { int l = ctrl ? _lines.Count - 1 : _caretLine; MoveCaret(l, _lines[l].Length); if (shift) ExtendOrBeginSelection(); else ClearSelection(); InvalidateCanvas(); e.Handled = true; return; } // Escape = clear selection if (e.Key == Key.Escape) { ClearSelection(); InvalidateCanvas(); e.Handled = true; return; } } private void OnTextInput(object sender, TextCompositionEventArgs e) { string text = e.Text; if (text.Length == 0) return; if (text.Length == 1 && text[0] < 0x20) return; // skip control chars RunEdit(text.Length == 1 ? EditKind.InsertChar : EditKind.Other, () => { if (_hasSelection) DeleteSelection(); InsertText(text); }); e.Handled = true; // After inserting '.', trigger member completion if (text.Length == 1 && text[0] == '.') { string objName = GetWordBeforeCaret(); if (!string.IsNullOrEmpty(objName)) ShowMemberCompletions(objName); } } private string GetWordBeforeCaret() { if (_caretLine >= _lines.Count || _caretLine < 0) return ""; string line = _lines[_caretLine]; int col = Math.Min(_caretColumn, line.Length); int start = col - 2; // skip the '.' while (start >= 0 && (char.IsLetterOrDigit(line[start]) || line[start] == '_')) start--; start++; if (start < col - 1) return line[start..(col - 1)]; return ""; } private void CopySel() { string sel = GetSelectedText(); if (sel.Length > 0) Clipboard.SetText(sel); } private void PasteText() { if (!Clipboard.ContainsText()) return; string text = Clipboard.GetText(); RunEdit(EditKind.Other, () => { if (_hasSelection) DeleteSelection(); string[] pasteLines = text.Split('\n'); if (pasteLines.Length == 1) { InsertText(text); } else { string first = pasteLines[0]; string last = pasteLines[^1]; string curLine = _lines[_caretLine]; string before = curLine[.._caretColumn]; string after = curLine[_caretColumn..]; _lines[_caretLine] = before + first; for (int i = 1; i < pasteLines.Length - 1; i++) _lines.Insert(_caretLine + i, pasteLines[i]); _lines.Insert(_caretLine + pasteLines.Length - 1, last + after); _caretLine += pasteLines.Length - 1; _caretColumn = last.Length; _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); } }); } private void MoveCaretHorizontal(int delta, bool extendSelection) { int newCol = _caretColumn + delta; int newLine = _caretLine; if (newCol < 0 && _caretLine > 0) { newLine--; newCol = _lines[newLine].Length; } else if (newCol > _lines[_caretLine].Length && _caretLine < _lines.Count - 1) { newLine++; newCol = 0; } else newCol = Math.Clamp(newCol, 0, _lines[newLine].Length); _caretDesiredX = -1; MoveCaret(newLine, newCol); if (extendSelection) ExtendOrBeginSelection(); else ClearSelection(); InvalidateCanvas(); } private void MoveCaretVertical(int delta, bool extendSelection) { int newLine = Math.Clamp(_caretLine + delta, 0, _lines.Count - 1); if (_caretDesiredX < 0) _caretDesiredX = (int)ColumnToX(_caretLine, _caretColumn); var (col, _) = XToColumn(newLine, _caretDesiredX); _caretLine = newLine; _caretColumn = Math.Clamp(col, 0, _lines[newLine].Length); _caretVisible = true; ScrollIntoView(); if (extendSelection) ExtendOrBeginSelection(); else ClearSelection(); InvalidateCanvas(); } private void ExtendOrBeginSelection() { if (!_hasSelection) { _hasSelection = true; _selStartLine = _selEndLine = _caretLine; _selStartCol = _selEndCol = _caretColumn; } _selEndLine = _caretLine; _selEndCol = _caretColumn; } // ── Mouse ───────────────────────────────────────────────────────────────── private void OnMouseDown(object sender, MouseButtonEventArgs e) { Focus(); var pos = e.GetPosition(RenderSurface); int line = Math.Clamp((int)(pos.Y / _lineH), 0, _lines.Count - 1); // Gutter click = toggle breakpoint if (pos.X < GutterWidth) { ToggleBreakpoint(line + 1); e.Handled = true; return; } var (col, _) = XToColumn(line, Math.Max(0, pos.X - GutterWidth - TextPad)); _isSelecting = true; MoveCaret(line, col); BeginSelection(line, col); InvalidateCanvas(); Mouse.Capture(this); e.Handled = true; } private void OnMouseMove(object sender, MouseEventArgs e) { if (!_isSelecting) return; var pos = e.GetPosition(RenderSurface); int line = Math.Clamp((int)(pos.Y / _lineH), 0, _lines.Count - 1); var (col, _) = XToColumn(line, Math.Max(0, pos.X - GutterWidth - TextPad)); _caretLine = line; _caretColumn = Math.Clamp(col, 0, _lines[line].Length); ExtendSelection(line, _caretColumn); ScrollIntoView(); InvalidateCanvas(); } private void OnMouseUp(object sender, MouseButtonEventArgs e) { _isSelecting = false; Mouse.Capture(null); } // ── Public breakpoint / debug API ─────────────────────────────────────── public void SetBreakpoint(int line) { if (line < 1) return; _breakpoints[line] = true; InvalidateCanvas(); BreakpointToggled?.Invoke(this, line); } public void ClearBreakpoint(int line) { if (line < 1) return; _breakpoints.Remove(line); InvalidateCanvas(); BreakpointToggled?.Invoke(this, line); } public void ToggleBreakpoint(int line) { if (line < 1) return; if (_breakpoints.ContainsKey(line)) _breakpoints.Remove(line); else _breakpoints[line] = true; InvalidateCanvas(); BreakpointToggled?.Invoke(this, line); } public void ClearAllBreakpoints() { _breakpoints.Clear(); InvalidateCanvas(); } public void SetCurrentLine(int line) { _currentLineHighlight = line; InvalidateCanvas(); } public IEnumerable GetBreakpoints() => _breakpoints.Keys; // ── AvalonEdit-compat API (for MainWindow swap) ────────────────────────── /// Alias for SourceCode (AvalonEdit compat). public string Text { get => SourceCode; set => SourceCode = value; } /// Current caret line (1-indexed, AvalonEdit compat). public int CaretLine { get => _caretLine + 1; set => _caretLine = Math.Clamp(value - 1, 0, _lines.Count - 1); } /// Current caret column (1-indexed, AvalonEdit compat). public int CaretColumn { get => _caretColumn + 1; set => _caretColumn = Math.Clamp(value - 1, 0, _lines[_caretLine].Length); } /// Character offset from start of text (AvalonEdit compat). public int CaretOffset { get { int offset = 0; for (int i = 0; i < _caretLine && i < _lines.Count; i++) offset += _lines[i].Length + 1; return offset + Math.Min(_caretColumn, _lines[_caretLine].Length); } set { int offset = Math.Clamp(value, 0, SourceCode.Length); _caretLine = 0; _caretColumn = 0; for (int i = 0; i < _lines.Count; i++) { int lineLen = _lines[i].Length + 1; if (offset < lineLen) { _caretColumn = offset; break; } offset -= lineLen; _caretLine++; } _caretColumn = Math.Clamp(_caretColumn, 0, _lines[_caretLine].Length); InvalidateCanvas(); } } public int LineCount => _lines.Count; public string GetLineText(int lineNumber) { int idx = lineNumber - 1; return idx >= 0 && idx < _lines.Count ? _lines[idx] : ""; } public void ScrollToLine(int line) { _caretLine = Math.Clamp(line - 1, 0, _lines.Count - 1); _caretColumn = 0; ScrollIntoView(); InvalidateCanvas(); } // ── Undo / redo ──────────────────────────────────────────────────────── private readonly UndoStack _history = new(); public event EventHandler? UndoStateChanged; public bool CanUndo => _history.CanUndo; public bool CanRedo => _history.CanRedo; private void RaiseUndoStateChanged() => UndoStateChanged?.Invoke(this, EventArgs.Empty); /// Runs one user-visible edit as a single undo step. private void RunEdit(EditKind kind, Action edit) { var before = CaptureSnapshot(); edit(); _history.Push(before, kind, _caretLine, _caretColumn); RaiseUndoStateChanged(); } private EditorSnapshot CaptureSnapshot() => new( SourceCode, _caretLine, _caretColumn, _hasSelection, _selStartLine, _selStartCol, _selEndLine, _selEndCol); /// Replaces the line buffer without touching caret or history. private void SetLines(string text) { _lines.Clear(); _lines.AddRange(text.Split('\n')); if (_lines.Count == 0) _lines.Add(""); _cachedTokens = null; InvalidateCanvas(); } private void ApplySnapshot(EditorSnapshot snapshot) { SetLines(snapshot.Text); _caretLine = Math.Clamp(snapshot.CaretLine, 0, _lines.Count - 1); _caretColumn = Math.Clamp(snapshot.CaretColumn, 0, _lines[_caretLine].Length); _hasSelection = snapshot.HasSelection; _selStartLine = Math.Clamp(snapshot.SelStartLine, 0, _lines.Count - 1); _selEndLine = Math.Clamp(snapshot.SelEndLine, 0, _lines.Count - 1); _selStartCol = Math.Clamp(snapshot.SelStartCol, 0, _lines[_selStartLine].Length); _selEndCol = Math.Clamp(snapshot.SelEndCol, 0, _lines[_selEndLine].Length); _caretDesiredX = -1; _caretVisible = true; InvalidateCanvas(); } public void Undo() { var snapshot = _history.Undo(CaptureSnapshot()); if (snapshot == null) return; ApplySnapshot(snapshot); FireTextChanged(); RaiseUndoStateChanged(); } public void Redo() { var snapshot = _history.Redo(CaptureSnapshot()); if (snapshot == null) return; ApplySnapshot(snapshot); FireTextChanged(); RaiseUndoStateChanged(); } /// Full text (IFindReplaceTarget). public string GetText() => SourceCode; // ── Find / replace (IFindReplaceTarget) ──────────────────────────────── private readonly List<(int Line, int StartCol, int EndCol)> _searchMatchRects = new(); private int TextLength { get { int length = 0; for (int i = 0; i < _lines.Count; i++) length += _lines[i].Length + (i < _lines.Count - 1 ? 1 : 0); return length; } } private (int Line, int Col) OffsetToPosition(int offset) { offset = Math.Clamp(offset, 0, TextLength); int line = 0; while (line < _lines.Count - 1 && offset > _lines[line].Length) { offset -= _lines[line].Length + 1; line++; } return (line, Math.Clamp(offset, 0, _lines[line].Length)); } private int PositionToOffset(int line, int col) { line = Math.Clamp(line, 0, _lines.Count - 1); int offset = 0; for (int i = 0; i < line; i++) offset += _lines[i].Length + 1; return offset + Math.Clamp(col, 0, _lines[line].Length); } private ((int Line, int Col) Start, (int Line, int Col) End) NormalizedSelection() { var a = (_selStartLine, _selStartCol); var b = (_selEndLine, _selEndCol); if (a.Item1 > b.Item1 || (a.Item1 == b.Item1 && a.Item2 > b.Item2)) (a, b) = (b, a); return (a, b); } public int SelectionStart { get { if (!_hasSelection) return CaretOffset; var (start, _) = NormalizedSelection(); return PositionToOffset(start.Line, start.Col); } } public int SelectionLength { get { if (!_hasSelection) return 0; var (start, end) = NormalizedSelection(); return PositionToOffset(end.Line, end.Col) - PositionToOffset(start.Line, start.Col); } } public void Select(int start, int length) { int from = Math.Clamp(start, 0, TextLength); int to = Math.Clamp(start + length, 0, TextLength); var (startLine, startCol) = OffsetToPosition(from); var (endLine, endCol) = OffsetToPosition(to); _selStartLine = startLine; _selStartCol = startCol; _selEndLine = endLine; _selEndCol = endCol; _hasSelection = length > 0; _caretLine = endLine; _caretColumn = endCol; _caretDesiredX = -1; _caretVisible = true; ScrollIntoView(); InvalidateCanvas(); } public void ScrollToOffset(int offset) { var (line, col) = OffsetToPosition(offset); MoveCaret(line, col); } public void FocusEditor() => Focus(); public void SetSearchMatches(IReadOnlyList matches) { _searchMatchRects.Clear(); foreach (var match in matches) { var (startLine, startCol) = OffsetToPosition(match.Start); var (endLine, endCol) = OffsetToPosition(match.End); for (int line = startLine; line <= endLine && line < _lines.Count; line++) { int from = line == startLine ? startCol : 0; int to = line == endLine ? endCol : _lines[line].Length; if (to > from) _searchMatchRects.Add((line, from, to)); } } InvalidateCanvas(); } public void ReplaceSelection(string replacement) { RunEdit(EditKind.Other, () => { if (_hasSelection) DeleteSelection(); InsertText(replacement); }); } public int ReplaceAll(string query, string replacement, bool matchCase, bool wholeWord) { var (newText, count) = EditorSearch.ReplaceAll(SourceCode, query, replacement, matchCase, wholeWord); if (count == 0) return 0; RunEdit(EditKind.Other, () => { SetLines(newText); _caretLine = Math.Clamp(_caretLine, 0, _lines.Count - 1); _caretColumn = Math.Clamp(_caretColumn, 0, _lines[_caretLine].Length); ClearSelection(); _searchMatchRects.Clear(); FireTextChanged(); }); return count; } public new void Cut() { string sel = GetSelectedText(); if (sel.Length == 0) return; RunEdit(EditKind.Other, () => { Clipboard.SetText(sel); DeleteSelection(); _cachedTokens = null; InvalidateCanvas(); FireTextChanged(); }); } public new void Copy() { string sel = GetSelectedText(); if (sel.Length > 0) Clipboard.SetText(sel); } public new void Paste() => PasteText(); public new void SelectAll() { _hasSelection = true; _selStartLine = 0; _selStartCol = 0; _selEndLine = _lines.Count - 1; _selEndCol = _lines[^1].Length; InvalidateCanvas(); } public void PerformTextInput(string text) { if (string.IsNullOrEmpty(text)) return; RunEdit(text.Length == 1 ? EditKind.InsertChar : EditKind.Other, () => { if (_hasSelection) DeleteSelection(); string normalized = text.Replace("\r\n", "\n", StringComparison.Ordinal).Replace('\r', '\n'); string[] insertedLines = normalized.Split('\n'); if (insertedLines.Length == 1) { InsertText(normalized); return; } string current = _lines[_caretLine]; string before = current[.._caretColumn]; string after = current[_caretColumn..]; _lines[_caretLine] = before + insertedLines[0]; for (int i = 1; i < insertedLines.Length - 1; i++) _lines.Insert(_caretLine + i, insertedLines[i]); _lines.Insert(_caretLine + insertedLines.Length - 1, insertedLines[^1] + after); _caretLine += insertedLines.Length - 1; _caretColumn = insertedLines[^1].Length; _cachedTokens = null; ClearSelection(); InvalidateCanvas(); FireTextChanged(); }); } public void Redraw() => InvalidateCanvas(); // ── AvalonEdit-compat sub-types ──────────────────────────────────────── public sealed class EditorCaret { private readonly EnhancedCodeEditor _editor; public EditorCaret(EnhancedCodeEditor editor) => _editor = editor; public int Line { get => _editor.CaretLine; set => _editor.CaretLine = value; } public int Column { get => _editor.CaretColumn; set => _editor.CaretColumn = value; } public event EventHandler? PositionChanged; internal void RaisePositionChanged() => PositionChanged?.Invoke(this, EventArgs.Empty); public void BringCaretToView() => _editor.ScrollIntoView(); } public sealed class EditorTextView { private readonly EnhancedCodeEditor _editor; public EditorTextView(EnhancedCodeEditor editor) => _editor = editor; public System.Collections.IList LineTransformers { get; } = new System.Collections.ArrayList(); public void Redraw() => _editor.InvalidateCanvas(); } public sealed class EditorTextArea { private readonly EnhancedCodeEditor _editor; public EditorTextArea(EnhancedCodeEditor editor) { _editor = editor; Caret = new EditorCaret(editor); TextView = new EditorTextView(editor); } public EditorCaret Caret { get; } public EditorTextView TextView { get; } public object? IndentationStrategy { get; set; } public void PerformTextInput(string text) => _editor.PerformTextInput(text); } public sealed class EditorDocumentLine { public int LineNumber { get; set; } public int Offset { get; set; } public int Length { get; set; } public int TotalLength => Length; } public sealed class EditorDocument { private readonly EnhancedCodeEditor _editor; public EditorDocument(EnhancedCodeEditor editor) => _editor = editor; public int LineCount => _editor.LineCount; public EditorDocumentLine GetLineByNumber(int lineNumber) { int idx = lineNumber - 1; string text = idx >= 0 && idx < _editor._lines.Count ? _editor._lines[idx] : ""; int offset = 0; for (int i = 0; i < idx && i < _editor._lines.Count; i++) offset += _editor._lines[i].Length + 1; return new EditorDocumentLine { LineNumber = lineNumber, Offset = offset, Length = text.Length }; } public string GetText(EditorDocumentLine line) { int idx = line.LineNumber - 1; return idx >= 0 && idx < _editor._lines.Count ? _editor._lines[idx] : ""; } } public sealed class EditorOptions { public bool EnableHyperlinks { get; set; } public bool EnableEmailHyperlinks { get; set; } public bool ShowSpaces { get; set; } public bool ShowTabs { get; set; } public int IndentationSize { get; set; } = 4; public bool ConvertTabsToSpaces { get; set; } } // ── AvalonEdit-compat properties ────────────────────────────────────── public event EventHandler? TextChanged; private void FireTextChanged() { string src = SourceCode; CodeChanged?.Invoke(this, src); TextChanged?.Invoke(this, EventArgs.Empty); } public object? SyntaxHighlighting { get; set; } public EditorTextArea TextArea { get; } = null!; // set in constructor public EditorDocument Document { get; } = null!; // set in constructor public EditorOptions Options { get; } = new EditorOptions(); private static Brush GetBrush(string name) { var brush = new SolidColorBrush(VisualThemeManager.GetColor(name)); brush.Freeze(); return brush; } } public class DrawingCanvas : Canvas { public Action? RenderAction; protected override void OnRender(DrawingContext dc) { base.OnRender(dc); RenderAction?.Invoke(dc); } }