diff --git a/Cauldron.Macos/Main.storyboard b/Cauldron.Macos/Main.storyboard
index 0634e53..5eb2ba2 100644
--- a/Cauldron.Macos/Main.storyboard
+++ b/Cauldron.Macos/Main.storyboard
@@ -684,7 +684,7 @@
-
+
diff --git a/Cauldron.Macos/SourceWriter/LineNumberRuler.cs b/Cauldron.Macos/SourceWriter/LineNumberRuler.cs
new file mode 100644
index 0000000..5ed9366
--- /dev/null
+++ b/Cauldron.Macos/SourceWriter/LineNumberRuler.cs
@@ -0,0 +1,126 @@
+using System;
+using AppKit;
+using CoreGraphics;
+using Foundation;
+
+namespace Cauldron.Macos.SourceWriter;
+
+public class LineNumberRuler : NSRulerView
+{
+ private NSColor _foregroundColor = NSColor.DisabledControlText;
+ private NSColor _backgroundColor = NSColor.TextBackground;
+
+ public float GutterWidth { get; private set; } = 40f;
+ public NSColor ForegroundColor
+ {
+ get => this._foregroundColor;
+ set
+ {
+ this._foregroundColor = value;
+ this.NeedsDisplay = true;
+ }
+ }
+ public NSColor BackgroundColor
+ {
+ get => this._backgroundColor;
+ set
+ {
+ this._backgroundColor = value;
+ this.NeedsDisplay = true;
+ }
+ }
+
+ public LineNumberRuler(NSTextView textView)
+ : base(textView.EnclosingScrollView, NSRulerOrientation.Vertical)
+ {
+ this.ClientView = textView;
+ this.RuleThickness = this.GutterWidth;
+ }
+
+ public LineNumberRuler(NSTextView textView, NSColor foregroundColor, NSColor backgroundColor)
+ : base(textView.EnclosingScrollView, NSRulerOrientation.Vertical)
+ {
+ this.ClientView = textView;
+ this.ForegroundColor = foregroundColor;
+ this.BackgroundColor = backgroundColor;
+ this.RuleThickness = this.GutterWidth;
+ }
+
+ public override void DrawHashMarksAndLabels(CGRect rect)
+ {
+ this.BackgroundColor.Set();
+ NSGraphicsContext.CurrentContext.CGContext.FillRect(rect);
+
+ if (this.ClientView is not NSTextView textView)
+ return;
+
+ NSLayoutManager layoutManager = textView.LayoutManager;
+ NSTextContainer textContainer = textView.TextContainer;
+
+ if (layoutManager is null || textContainer is null)
+ return;
+
+ NSString content = new NSString(textView.Value);
+ NSRange visibleGlyphsRange = layoutManager
+ .GetGlyphRangeForBoundingRect(textView.VisibleRect(), textContainer);
+
+ int lineNumber = 1;
+
+ NSRegularExpression newlineRegex = new NSRegularExpression(new NSString("\n"),
+ new NSRegularExpressionOptions(), out NSError error);
+ if (error is not null)
+ return;
+
+ lineNumber += (int)newlineRegex.GetNumberOfMatches(content, new NSMatchingOptions(),
+ new NSRange(0, visibleGlyphsRange.Location));
+
+ nint firstGlyphOfLineIndex = visibleGlyphsRange.Location;
+
+ while (firstGlyphOfLineIndex < visibleGlyphsRange.Location + visibleGlyphsRange.Length)
+ {
+ NSRange charRangeOfLine = content.LineRangeForRange(
+ new NSRange((nint)layoutManager.GetCharacterIndex((nuint)firstGlyphOfLineIndex), 0));
+ NSRange glyphRangeOfLine = layoutManager.GetGlyphRange(charRangeOfLine);
+
+ nint firstGlyphOfRowIndex = firstGlyphOfLineIndex;
+ int lineWrapCount = 0;
+
+ while (firstGlyphOfRowIndex < glyphRangeOfLine.Location + glyphRangeOfLine.Length)
+ {
+ CGRect lineRect = layoutManager.GetLineFragmentRect((nuint)firstGlyphOfRowIndex,
+ out NSRange effectiveRange, true);
+
+ if (lineWrapCount == 0)
+ this.DrawLineNumber(lineNumber, (float)lineRect.GetMinY()
+ + (float)textView.TextContainerInset.Height);
+ else
+ break;
+
+ // Move to next row
+ firstGlyphOfRowIndex = effectiveRange.Location + effectiveRange.Length;
+ }
+
+ firstGlyphOfLineIndex = glyphRangeOfLine.Location + glyphRangeOfLine.Length;
+ lineNumber += 1;
+ }
+
+ if (layoutManager.ExtraLineFragmentTextContainer != null)
+ this.DrawLineNumber(lineNumber, (float)layoutManager.ExtraLineFragmentRect.GetMinY()
+ + (float)textView.TextContainerInset.Height);
+ }
+
+ private void DrawLineNumber(int lineNumber, float yPosition)
+ {
+ if (this.ClientView is not NSTextView textView)
+ return;
+
+ NSDictionary attributes = new(NSStringAttributeKey.Font, textView.Font,
+ NSStringAttributeKey.ForegroundColor, this.ForegroundColor);
+ NSAttributedString attributedLineNumber = new(lineNumber.ToString(), attributes);
+ CGPoint relativePoint = this.ConvertPointFromView(CGPoint.Empty, textView);
+ nfloat xPosition = this.GutterWidth - (attributedLineNumber.Size.Width + 8);
+
+ attributedLineNumber.DrawAtPoint(new CGPoint(xPosition, relativePoint.Y + yPosition));
+ }
+}
+
diff --git a/Cauldron.Macos/SourceWriter/SourceTextView.cs b/Cauldron.Macos/SourceWriter/SourceTextView.cs
index acfd397..3bbcb47 100644
--- a/Cauldron.Macos/SourceWriter/SourceTextView.cs
+++ b/Cauldron.Macos/SourceWriter/SourceTextView.cs
@@ -52,6 +52,8 @@ public class SourceTextView : NSTextView
/// Should the editor only use default words if the keyword list is empty.
private bool _defaultWordsOnlyIfKeywordsEmpty = true;
+ private LineNumberRuler LineNumberRuler;
+
#endregion
#region Computed Properties
@@ -245,6 +247,28 @@ public class SourceTextView : NSTextView
this.UsesAdaptiveColorMappingForDarkAppearance = true;
}
+ public override void AwakeFromNib()
+ {
+ base.AwakeFromNib();
+
+ this.LineNumberRuler = new LineNumberRuler(this);
+
+ this.EnclosingScrollView.VerticalRulerView = this.LineNumberRuler;
+ this.EnclosingScrollView.HasVerticalRuler = true;
+ this.EnclosingScrollView.RulersVisible = true;
+
+ this.PostsFrameChangedNotifications = true;
+ NSView.Notifications.ObserveBoundsChanged((_, _) => this.DrawGutter());
+ this.OnTextChanged += (_, _) => this.DrawGutter();
+ }
+
+ [Export("drawGutter")]
+ public void DrawGutter()
+ {
+ if (this.LineNumberRuler is not null)
+ this.LineNumberRuler.NeedsDisplay = true;
+ }
+
#endregion
#region Private Methods
@@ -678,6 +702,7 @@ public class SourceTextView : NSTextView
// Console.WriteLine ("Read selection from pasteboard");
bool result = base.ReadSelectionFromPasteboard(pboard);
Formatter?.Reformat();
+ this.OnTextChanged?.Invoke(this, null);
return result;
}
@@ -695,6 +720,7 @@ public class SourceTextView : NSTextView
// Console.WriteLine ("Read selection from pasteboard also");
var result = base.ReadSelectionFromPasteboard(pboard, type);
Formatter?.Reformat();
+ this.OnTextChanged?.Invoke(this, null);
return result;
}