From 7035652ac5dd0b6a470016f871ef02ab30737df5 Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:10:50 -0600 Subject: [PATCH 01/10] gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a8f04c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.vs +bin +obj +*.csproj.user +testfile.txt \ No newline at end of file From ce6644c85ad53d8b4424693a0481155eeec4fa7c Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:11:18 -0600 Subject: [PATCH 02/10] add slnx --- Yotepad.slnx | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Yotepad.slnx diff --git a/Yotepad.slnx b/Yotepad.slnx new file mode 100644 index 0000000..0565b31 --- /dev/null +++ b/Yotepad.slnx @@ -0,0 +1,3 @@ + + + From 03c14d7dcbe7b03e58cf42b4da703262412ca473 Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:12:04 -0600 Subject: [PATCH 03/10] add hotkeys for file menu --- MainWindow.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/MainWindow.cs b/MainWindow.cs index 1a4af9d..af1930d 100644 --- a/MainWindow.cs +++ b/MainWindow.cs @@ -263,7 +263,7 @@ private void SetupMenu() } // --- FILE MENU --- - var fileMenu = new ToolStripMenuItem("File"); + var fileMenu = new ToolStripMenuItem("&File"); fileMenu.DropDownItems.Add(new ToolStripMenuItem("New", null, (s, e) => { @@ -343,7 +343,7 @@ private void SetupMenu() }); // --- EDIT MENU --- - var editMenu = new ToolStripMenuItem("Edit"); + var editMenu = new ToolStripMenuItem("&Edit"); editMenu.DropDownItems.Add(new ToolStripMenuItem("Undo", null, (s, e) => _mainTextBox.Undo()) { ShortcutKeys = Keys.Control | Keys.Z }); editMenu.DropDownItems.Add(new ToolStripSeparator()); editMenu.DropDownItems.Add(new ToolStripMenuItem("Cut", null, (s, e) => _mainTextBox.Cut()) { ShortcutKeys = Keys.Control | Keys.X }); @@ -378,7 +378,7 @@ private void SetupMenu() editMenu.DropDownItems.Add(new ToolStripMenuItem("Time/Date", null, (s, e) => _mainTextBox.SelectedText = DateTime.Now.ToString()) { ShortcutKeys = Keys.F5 }); // --- FORMAT MENU --- - var formatMenu = new ToolStripMenuItem("Format"); + var formatMenu = new ToolStripMenuItem("F&ormat"); _wordWrapMenuItem.CheckOnClick = true; _wordWrapMenuItem.CheckedChanged += (s, e) => ToggleWordWrap(); _wordWrapMenuItem.Checked = true; @@ -397,7 +397,7 @@ private void SetupMenu() })); // --- VIEW MENU --- - var viewMenu = new ToolStripMenuItem("View"); + var viewMenu = new ToolStripMenuItem("&View"); _statusBarMenuItem.CheckOnClick = true; _statusBarMenuItem.Checked = true; _statusBarMenuItem.CheckedChanged += (s, e) => _statusBar.Visible = _statusBarMenuItem.Checked; @@ -410,8 +410,8 @@ private void SetupMenu() viewMenu.DropDownItems.Add(new ToolStripMenuItem("Toggle Theme", null, (s, e) => { _themeManager.ToggleTheme(); RefreshTheme(); })); // --- HELP MENU --- - var helpMenu = new ToolStripMenuItem("Help"); - + var helpMenu = new ToolStripMenuItem("&Help"); + helpMenu.DropDownItems.Add(new ToolStripMenuItem("View Help", null, (s, e) => { // Grab the colors directly from the text editor to ensure perfect contrast From 6ebfeaf5776919c2ba76238911f97a73d875b42e Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:12:20 -0600 Subject: [PATCH 04/10] ctrl+w closes application --- MainWindow.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MainWindow.cs b/MainWindow.cs index af1930d..9d10e2b 100644 --- a/MainWindow.cs +++ b/MainWindow.cs @@ -697,6 +697,9 @@ protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { switch (keyData) { + case Keys.Control | Keys.W: + this.Close(); + return true; case Keys.Control | Keys.Oemplus: case Keys.Control | Keys.Add: case Keys.Control | Keys.Shift | Keys.Oemplus: From 1984e5a8a64f4460ebc9fe1035af15c9b2d271c7 Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:12:41 -0600 Subject: [PATCH 05/10] wordwrap off by default --- MainWindow.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/MainWindow.cs b/MainWindow.cs index 9d10e2b..3b93d71 100644 --- a/MainWindow.cs +++ b/MainWindow.cs @@ -164,6 +164,7 @@ private void SetupEditor() _mainTextBox.Dock = DockStyle.Fill; _mainTextBox.Font = new Font("Consolas", 11F); _mainTextBox.ScrollBars = ScrollBars.Both; + _mainTextBox.WordWrap = false; _mainTextBox.AcceptsTab = true; _mainTextBox.BorderStyle = BorderStyle.None; _mainTextBox.HideSelection = false; @@ -381,7 +382,7 @@ private void SetupMenu() var formatMenu = new ToolStripMenuItem("F&ormat"); _wordWrapMenuItem.CheckOnClick = true; _wordWrapMenuItem.CheckedChanged += (s, e) => ToggleWordWrap(); - _wordWrapMenuItem.Checked = true; + _wordWrapMenuItem.Checked = false; formatMenu.DropDownItems.Add(_wordWrapMenuItem); formatMenu.DropDownItems.Add(new ToolStripMenuItem("Font...", null, (s, e) => { From 50a9abcc3402f3d0d57adbd0268489903d722c81 Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:13:43 -0600 Subject: [PATCH 06/10] enable menu margins so checkboxes appear --- ThemeManager.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/ThemeManager.cs b/ThemeManager.cs index 7bcb96e..bd84a85 100644 --- a/ThemeManager.cs +++ b/ThemeManager.cs @@ -63,6 +63,7 @@ private void ApplyMenuTheme(ToolStripMenuItem item) if (item.DropDown is ToolStripDropDownMenu dropDown) { dropDown.ShowImageMargin = false; + dropDown.ShowCheckMargin = true; dropDown.BackColor = MenuBackgroundColor; dropDown.ForeColor = TextColor; } @@ -126,6 +127,20 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } } + protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) + { + if (!_theme.IsDarkMode) + { + base.OnRenderImageMargin(e); + return; + } + + using (SolidBrush brush = new SolidBrush(_theme.MenuBackgroundColor)) + { + e.Graphics.FillRectangle(brush, e.AffectedBounds); + } + } + protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) { if (!_theme.IsDarkMode) @@ -135,9 +150,10 @@ protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) } int y = e.Item.Height / 2; + int startX = 24; using (Pen pen = new Pen(Color.FromArgb(70, 70, 70))) { - e.Graphics.DrawLine(pen, 4, y, e.Item.Width - 4, y); + e.Graphics.DrawLine(pen, startX, y, e.Item.Width - 4, y); } } From 88e2db5c82e039e9bd36a852ebc1e4ca5ae6358e Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:28:53 -0600 Subject: [PATCH 07/10] line numbers --- MainWindow.cs | 134 ++++++++++++++++++++++++++++++++++++++++++++++- NativeMethods.cs | 4 +- YoteTextBox.cs | 46 +++++++++++++--- 3 files changed, 174 insertions(+), 10 deletions(-) diff --git a/MainWindow.cs b/MainWindow.cs index 3b93d71..669d61c 100644 --- a/MainWindow.cs +++ b/MainWindow.cs @@ -10,12 +10,15 @@ public partial class MainWindow : Form private readonly SearchService _searchService = new SearchService(); private FindReplaceDialog? _searchDialog; private readonly YoteTextBox _mainTextBox = new YoteTextBox(); + private readonly Panel _editorHost = new Panel(); + private readonly LineNumberGutterPanel _lineNumberGutter = new LineNumberGutterPanel(); private readonly MenuStrip _topMenu = new MenuStrip(); private readonly StatusStrip _statusBar = new StatusStrip(); private readonly ToolStripStatusLabel _lblLocation = new ToolStripStatusLabel(); private readonly ToolStripMenuItem _wordWrapMenuItem = new ToolStripMenuItem("Word Wrap"); private readonly ToolStripMenuItem _statusBarMenuItem = new ToolStripMenuItem("Status Bar"); + private readonly ToolStripMenuItem _lineNumbersMenuItem = new ToolStripMenuItem("Line Numbers"); private ToolStripMenuItem _goToLineMenuItem = new ToolStripMenuItem("Go To Line..."); private readonly FileService _fileService = new FileService(); @@ -26,6 +29,18 @@ public partial class MainWindow : Form private bool _isModified = false; private IntPtr _iconHandle = IntPtr.Zero; + private sealed class LineNumberGutterPanel : Panel + { + public LineNumberGutterPanel() + { + this.SetStyle(ControlStyles.AllPaintingInWmPaint | + ControlStyles.UserPaint | + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw, true); + this.UpdateStyles(); + } + } + private void SystemEvents_UserPreferenceChanged(object sender, Microsoft.Win32.UserPreferenceChangedEventArgs e) { // 'General' covers OS theme changes in the registry @@ -160,6 +175,16 @@ private void ShowRecoveryDialog(RecoveryFile[] files) private void SetupEditor() { + _editorHost.Dock = DockStyle.Fill; + _editorHost.Margin = Padding.Empty; + _editorHost.Padding = Padding.Empty; + + _lineNumberGutter.Dock = DockStyle.Left; + _lineNumberGutter.Width = 48; + _lineNumberGutter.Margin = Padding.Empty; + _lineNumberGutter.Padding = Padding.Empty; + _lineNumberGutter.Paint += (s, e) => DrawLineNumbers(e.Graphics); + _mainTextBox.Multiline = true; _mainTextBox.Dock = DockStyle.Fill; _mainTextBox.Font = new Font("Consolas", 11F); @@ -190,8 +215,20 @@ private void SetupEditor() } }; - this.Controls.Add(_mainTextBox); - _mainTextBox.BringToFront(); + _mainTextBox.ViewportChanged += (s, e) => InvalidateLineNumbers(); + _mainTextBox.FontChanged += (s, e) => + { + UpdateLineNumberGutterWidth(); + InvalidateLineNumbers(); + }; + + _editorHost.Controls.Add(_mainTextBox); + _editorHost.Controls.Add(_lineNumberGutter); + this.Controls.Add(_editorHost); + _editorHost.BringToFront(); + + UpdateLineNumberGutterWidth(); + InvalidateLineNumbers(); } private void SetupStatusBar() @@ -403,6 +440,17 @@ private void SetupMenu() _statusBarMenuItem.Checked = true; _statusBarMenuItem.CheckedChanged += (s, e) => _statusBar.Visible = _statusBarMenuItem.Checked; viewMenu.DropDownItems.Add(_statusBarMenuItem); + + _lineNumbersMenuItem.CheckOnClick = true; + _lineNumbersMenuItem.CheckedChanged += (s, e) => + { + _lineNumberGutter.Visible = _lineNumbersMenuItem.Checked; + UpdateLineNumberGutterWidth(); + InvalidateLineNumbers(); + }; + _lineNumbersMenuItem.Checked = true; + viewMenu.DropDownItems.Add(_lineNumbersMenuItem); + viewMenu.DropDownItems.Add(new ToolStripSeparator()); viewMenu.DropDownItems.Add(new ToolStripMenuItem("Zoom In", null, (s, e) => ZoomIn()) { ShortcutKeyDisplayString = "Ctrl++" }); viewMenu.DropDownItems.Add(new ToolStripMenuItem("Zoom Out", null, (s, e) => ZoomOut()) { ShortcutKeyDisplayString = "Ctrl+-" }); @@ -436,6 +484,7 @@ private void ToggleWordWrap() _mainTextBox.WordWrap = _wordWrapMenuItem.Checked; _goToLineMenuItem.Enabled = !_wordWrapMenuItem.Checked; RefreshTheme(); + InvalidateLineNumbers(); if (_wordWrapMenuItem.Checked) { @@ -454,6 +503,10 @@ private void RefreshTheme() _themeManager.ApplyTheme(this, _mainTextBox, _topMenu, _statusBar); _searchDialog?.ApplyTheme(_themeManager); + _lineNumberGutter.BackColor = _themeManager.MenuBackgroundColor; + _lineNumberGutter.ForeColor = _themeManager.TextColor; + InvalidateLineNumbers(); + // Force the status bar popups to inherit the themed colors if (_btnLineEnding.DropDown is ToolStripDropDownMenu leMenu) { @@ -617,6 +670,9 @@ private void UpdateUIState() int column = index - _mainTextBox.GetFirstCharIndexFromLine(line); _lblLocation.Text = $"Ln {line + 1}, Col {column + 1}"; + if (UpdateLineNumberGutterWidth()) + InvalidateLineNumbers(); + // Update Line Ending UI _btnLineEnding.Text = _fileService.CurrentLineEnding switch { @@ -764,6 +820,80 @@ protected override void OnFormClosing(FormClosingEventArgs e) // it will skip the save prompt entirely and close instantly. base.OnFormClosing(e); } + + private bool UpdateLineNumberGutterWidth() + { + if (!_lineNumbersMenuItem.Checked) + return false; + + int lineCount = Math.Max(1, _mainTextBox.Lines.Length); + int digits = lineCount.ToString().Length; + int textWidth = TextRenderer.MeasureText(new string('9', digits), _mainTextBox.Font).Width; + int desiredWidth = Math.Max(36, textWidth + 10); + + if (_lineNumberGutter.Width != desiredWidth) + { + _lineNumberGutter.Width = desiredWidth; + return true; + } + + return false; + } + + private void InvalidateLineNumbers() + { + if (_lineNumbersMenuItem.Checked && _lineNumberGutter.IsHandleCreated) + _lineNumberGutter.Invalidate(); + } + + private void DrawLineNumbers(Graphics graphics) + { + graphics.Clear(_lineNumberGutter.BackColor); + + if (!_lineNumbersMenuItem.Checked || !_lineNumberGutter.Visible) return; + if (!_mainTextBox.IsHandleCreated) return; + + int firstVisibleDisplayLine = _mainTextBox.GetFirstVisibleDisplayLine(); + if (firstVisibleDisplayLine < 0) return; + + int lineHeight = Math.Max(1, _mainTextBox.Font.Height); + int visibleRows = (_lineNumberGutter.Height / lineHeight) + 3; + + using (var brush = new SolidBrush(_lineNumberGutter.ForeColor)) + using (var format = new StringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Near }) + { + int previousCharIndex = -1; + + for (int i = 0; i < visibleRows; i++) + { + int displayLine = firstVisibleDisplayLine + i; + int charIndex = _mainTextBox.GetCharIndexFromDisplayLine(displayLine); + + if (charIndex < 0 || charIndex > _mainTextBox.TextLength) + break; + + if (i > 0 && charIndex == previousCharIndex) + break; + + int y = i * lineHeight; + if (y > _lineNumberGutter.Height) + break; + + bool startsLogicalLine = charIndex == 0 || + (_mainTextBox.TextLength > 0 && charIndex <= _mainTextBox.TextLength && _mainTextBox.Text[charIndex - 1] == '\n'); + + if (startsLogicalLine) + { + int logicalLine = _mainTextBox.GetLineFromCharIndex(charIndex); + string lineText = (logicalLine + 1).ToString(); + var rect = new RectangleF(0, y, _lineNumberGutter.Width - 6, lineHeight + 2); + graphics.DrawString(lineText, _mainTextBox.Font, brush, rect, format); + } + + previousCharIndex = charIndex; + } + } + } } } diff --git a/NativeMethods.cs b/NativeMethods.cs index 9baf8d5..91184b5 100644 --- a/NativeMethods.cs +++ b/NativeMethods.cs @@ -28,6 +28,8 @@ internal static class NativeMethods [DllImport("user32.dll", SetLastError = true)] public static extern bool DestroyCaret(); - + + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam); } } \ No newline at end of file diff --git a/YoteTextBox.cs b/YoteTextBox.cs index 8e243b2..a6b68c5 100644 --- a/YoteTextBox.cs +++ b/YoteTextBox.cs @@ -7,9 +7,28 @@ namespace Yotepad public class YoteTextBox : TextBox { private const int WM_PASTE = 0x0302; - + private const int WM_VSCROLL = 0x0115; + private const int WM_MOUSEWHEEL = 0x020A; + private const int WM_HSCROLL = 0x0114; + private const int WM_SIZE = 0x0005; + + private const int EM_GETFIRSTVISIBLELINE = 0x00CE; + private const int EM_LINEINDEX = 0x00BB; + + public event EventHandler? ViewportChanged; + public bool IsOverwriteMode { get; private set; } = false; + public int GetFirstVisibleDisplayLine() + { + return (int)NativeMethods.SendMessage(this.Handle, EM_GETFIRSTVISIBLELINE, IntPtr.Zero, IntPtr.Zero); + } + + public int GetCharIndexFromDisplayLine(int displayLine) + { + return (int)NativeMethods.SendMessage(this.Handle, EM_LINEINDEX, new IntPtr(displayLine), IntPtr.Zero); + } + protected override void WndProc(ref Message m) { if (m.Msg == WM_PASTE && Clipboard.ContainsText()) @@ -19,7 +38,19 @@ protected override void WndProc(ref Message m) this.SelectedText = text; return; } + base.WndProc(ref m); + + if (m.Msg == WM_VSCROLL || m.Msg == WM_MOUSEWHEEL || m.Msg == WM_HSCROLL || m.Msg == WM_SIZE) + { + ViewportChanged?.Invoke(this, EventArgs.Empty); + } + } + + protected override void OnTextChanged(EventArgs e) + { + base.OnTextChanged(e); + ViewportChanged?.Invoke(this, EventArgs.Empty); } protected override void OnKeyDown(KeyEventArgs e) @@ -35,13 +66,13 @@ protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyPress(KeyPressEventArgs e) { - if (IsOverwriteMode && - this.SelectionLength == 0 && - this.SelectionStart < this.TextLength && + if (IsOverwriteMode && + this.SelectionLength == 0 && + this.SelectionStart < this.TextLength && !char.IsControl(e.KeyChar)) { char nextChar = this.Text[this.SelectionStart]; - + if (nextChar != '\r' && nextChar != '\n') { this.SelectionLength = 1; @@ -50,7 +81,7 @@ protected override void OnKeyPress(KeyPressEventArgs e) base.OnKeyPress(e); } - // The OS constantly tries to reset the caret to a line. + // The OS constantly tries to reset the caret to a line. // We must reassert our block caret after these events. protected override void OnKeyUp(KeyEventArgs e) { @@ -62,6 +93,7 @@ protected override void OnMouseUp(MouseEventArgs mevent) { base.OnMouseUp(mevent); UpdateCaretAppearance(); + ViewportChanged?.Invoke(this, EventArgs.Empty); } protected override void OnGotFocus(EventArgs e) @@ -77,7 +109,7 @@ private void UpdateCaretAppearance() // Measure roughly how wide a character is in the current font int width = TextRenderer.MeasureText("W", this.Font).Width / 2; int height = this.Font.Height; - + // Passing IntPtr.Zero creates a solid black/white inverted block NativeMethods.CreateCaret(this.Handle, IntPtr.Zero, width, height); NativeMethods.ShowCaret(this.Handle); From 45787ddb7f30bba6c2ddaec1e9003c796fb057be Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:39:35 -0600 Subject: [PATCH 08/10] improvements --- MainWindow.cs | 99 ++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 91 insertions(+), 8 deletions(-) diff --git a/MainWindow.cs b/MainWindow.cs index 669d61c..a7564d6 100644 --- a/MainWindow.cs +++ b/MainWindow.cs @@ -28,6 +28,7 @@ public partial class MainWindow : Form private bool _isModified = false; private IntPtr _iconHandle = IntPtr.Zero; + private int _lastFirstVisibleDisplayLine = -1; private sealed class LineNumberGutterPanel : Panel { @@ -409,7 +410,7 @@ private void SetupMenu() editMenu.DropDownItems.Add(new ToolStripSeparator()); _goToLineMenuItem.ShortcutKeys = Keys.Control | Keys.G; _goToLineMenuItem.Click += (s, e) => ShowGoToLine(); - _goToLineMenuItem.Enabled = !_wordWrapMenuItem.Checked; + _goToLineMenuItem.Enabled = true; editMenu.DropDownItems.Add(_goToLineMenuItem); editMenu.DropDownItems.Add(new ToolStripSeparator()); editMenu.DropDownItems.Add(new ToolStripMenuItem("Select All", null, (s, e) => _mainTextBox.SelectAll()) { ShortcutKeys = Keys.Control | Keys.A }); @@ -482,7 +483,6 @@ private void SetupMenu() private void ToggleWordWrap() { _mainTextBox.WordWrap = _wordWrapMenuItem.Checked; - _goToLineMenuItem.Enabled = !_wordWrapMenuItem.Checked; RefreshTheme(); InvalidateLineNumbers(); @@ -529,6 +529,16 @@ private bool ConfirmSaveIfModified() return result == DialogResult.No; } + private void InitializeMissingFilePath(string path) + { + _fileService.SetFilePath(path); + _mainTextBox.Text = string.Empty; + _mainTextBox.SelectionStart = 0; + _isModified = false; + _lastRecoveryContent = string.Empty; + UpdateUIState(); + } + private void LoadInitialFile(string path) { try @@ -632,14 +642,14 @@ private void FindPreviousShortcut() private void ShowGoToLine() { int currentIndex = _mainTextBox.SelectionStart + _mainTextBox.SelectionLength; - int currentLine = _mainTextBox.GetLineFromCharIndex(currentIndex) + 1; - int maxLine = _mainTextBox.GetLineFromCharIndex(_mainTextBox.Text.Length) + 1; + int currentLine = GetLogicalLineFromCharIndex(currentIndex) + 1; + int maxLine = GetLogicalLineCount(); using (var dialog = new GoToLineDialog(_themeManager, currentLine, maxLine)) { if (dialog.ShowDialog(this) == DialogResult.OK) { - int charIndex = _mainTextBox.GetFirstCharIndexFromLine(dialog.LineNumber - 1); + int charIndex = GetCharIndexFromLogicalLine(dialog.LineNumber - 1); if (charIndex >= 0) { _mainTextBox.SelectionStart = charIndex; @@ -670,7 +680,22 @@ private void UpdateUIState() int column = index - _mainTextBox.GetFirstCharIndexFromLine(line); _lblLocation.Text = $"Ln {line + 1}, Col {column + 1}"; + bool shouldInvalidateLineNumbers = false; + if (UpdateLineNumberGutterWidth()) + shouldInvalidateLineNumbers = true; + + if (_lineNumbersMenuItem.Checked && _mainTextBox.IsHandleCreated) + { + int firstVisibleDisplayLine = _mainTextBox.GetFirstVisibleDisplayLine(); + if (firstVisibleDisplayLine != _lastFirstVisibleDisplayLine) + { + _lastFirstVisibleDisplayLine = firstVisibleDisplayLine; + shouldInvalidateLineNumbers = true; + } + } + + if (shouldInvalidateLineNumbers) InvalidateLineNumbers(); // Update Line Ending UI @@ -826,7 +851,7 @@ private bool UpdateLineNumberGutterWidth() if (!_lineNumbersMenuItem.Checked) return false; - int lineCount = Math.Max(1, _mainTextBox.Lines.Length); + int lineCount = Math.Max(1, GetLogicalLineCount()); int digits = lineCount.ToString().Length; int textWidth = TextRenderer.MeasureText(new string('9', digits), _mainTextBox.Font).Width; int desiredWidth = Math.Max(36, textWidth + 10); @@ -840,6 +865,56 @@ private bool UpdateLineNumberGutterWidth() return false; } + private int GetLogicalLineCount() + { + string text = _mainTextBox.Text; + if (string.IsNullOrEmpty(text)) return 1; + + int count = 1; + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '\n') count++; + } + return count; + } + + private int GetLogicalLineFromCharIndex(int charIndex) + { + string text = _mainTextBox.Text; + if (string.IsNullOrEmpty(text) || charIndex <= 0) return 0; + + if (charIndex > text.Length) charIndex = text.Length; + + int line = 0; + for (int i = 0; i < charIndex; i++) + { + if (text[i] == '\n') line++; + } + return line; + } + + private int GetCharIndexFromLogicalLine(int targetLine) + { + if (targetLine <= 0) return 0; + + string text = _mainTextBox.Text; + int currentLine = 0; + + for (int i = 0; i < text.Length; i++) + { + if (text[i] == '\n') + { + currentLine++; + if (currentLine == targetLine) + { + return i + 1; + } + } + } + + return -1; + } + private void InvalidateLineNumbers() { if (_lineNumbersMenuItem.Checked && _lineNumberGutter.IsHandleCreated) @@ -863,6 +938,10 @@ private void DrawLineNumbers(Graphics graphics) using (var format = new StringFormat { Alignment = StringAlignment.Far, LineAlignment = StringAlignment.Near }) { int previousCharIndex = -1; + int firstCharIndex = _mainTextBox.GetCharIndexFromDisplayLine(firstVisibleDisplayLine); + if (firstCharIndex < 0 || firstCharIndex > _mainTextBox.TextLength) return; + + int currentLogicalLine = GetLogicalLineFromCharIndex(firstCharIndex); for (int i = 0; i < visibleRows; i++) { @@ -882,10 +961,14 @@ private void DrawLineNumbers(Graphics graphics) bool startsLogicalLine = charIndex == 0 || (_mainTextBox.TextLength > 0 && charIndex <= _mainTextBox.TextLength && _mainTextBox.Text[charIndex - 1] == '\n'); + if (i > 0 && startsLogicalLine) + { + currentLogicalLine++; + } + if (startsLogicalLine) { - int logicalLine = _mainTextBox.GetLineFromCharIndex(charIndex); - string lineText = (logicalLine + 1).ToString(); + string lineText = (currentLogicalLine + 1).ToString(); var rect = new RectangleF(0, y, _lineNumberGutter.Width - 6, lineHeight + 2); graphics.DrawString(lineText, _mainTextBox.Font, brush, rect, format); } From 8a8b401b0144ca72fdfb1d79cd376d65d0ab0dd7 Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:43:01 -0600 Subject: [PATCH 09/10] line numbers looking good --- YoteTextBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/YoteTextBox.cs b/YoteTextBox.cs index a6b68c5..83b1c1a 100644 --- a/YoteTextBox.cs +++ b/YoteTextBox.cs @@ -14,6 +14,7 @@ public class YoteTextBox : TextBox private const int EM_GETFIRSTVISIBLELINE = 0x00CE; private const int EM_LINEINDEX = 0x00BB; + private const int EM_SCROLLCARET = 0x00B7; public event EventHandler? ViewportChanged; @@ -41,7 +42,7 @@ protected override void WndProc(ref Message m) base.WndProc(ref m); - if (m.Msg == WM_VSCROLL || m.Msg == WM_MOUSEWHEEL || m.Msg == WM_HSCROLL || m.Msg == WM_SIZE) + if (m.Msg == WM_VSCROLL || m.Msg == WM_MOUSEWHEEL || m.Msg == WM_HSCROLL || m.Msg == WM_SIZE || m.Msg == EM_SCROLLCARET) { ViewportChanged?.Invoke(this, EventArgs.Empty); } From b4f8426d69d87c650e8755b5b213830eb3d4729d Mon Sep 17 00:00:00 2001 From: Rebecca Keller Date: Mon, 20 Apr 2026 12:51:55 -0600 Subject: [PATCH 10/10] respect filename from path even if file doesn't exist (ie create new file) --- MainWindow.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/MainWindow.cs b/MainWindow.cs index a7564d6..e817f88 100644 --- a/MainWindow.cs +++ b/MainWindow.cs @@ -84,10 +84,21 @@ public MainWindow(string filePath = "", Point? startPosition = null, bool skipRe _themeManager.InitializeTheme(); RefreshTheme(); - if (!string.IsNullOrEmpty(filePath) && File.Exists(filePath)) - LoadInitialFile(filePath); + if (!string.IsNullOrEmpty(filePath)) + { + if (File.Exists(filePath)) + { + LoadInitialFile(filePath); + } + else + { + InitializeMissingFilePath(filePath); + } + } else + { UpdateUIState(); + } // Skip recovery scan if this instance was launched from a recovery action if (!skipRecovery) @@ -534,7 +545,7 @@ private void InitializeMissingFilePath(string path) _fileService.SetFilePath(path); _mainTextBox.Text = string.Empty; _mainTextBox.SelectionStart = 0; - _isModified = false; + _isModified = true; _lastRecoveryContent = string.Empty; UpdateUIState(); }