objective-ccocoanstextview

How to draw line number with NSRulerView?


How to draw line number with NSRulerView?

Displaying line numbers with NSTextView, written with Objective-C, clean and simple. Sorry, I know this is an old question frequently asked, but I can't find an answer here or by google searching, there's several project, e.g, https://github.com/scottharwell/NoodleKit, can't work, and https://github.com/krzyzanowskim/STTextView, etc, when I converted one of the sample working Swift code to OC, the same code can't work as expected, drawHashMarksAndLabels() never called in the subclass of NSRulerView, the from code: https://github.com/yichizhang/NSTextView-LineNumberView, the skeleton of the converted code:



    ===1. subclass NSRulerView
    @interface PERulerView: NSRulerView
    - (void)drawHashMarksAndLabels:(NSRect)rect{
       //never called!!!
    }
    
    - (id)initWithTextView:(NSTextView *)textView {
        NSScrollView *aScrollView = [textView enclosingScrollView];
        self = [super initWithScrollView:aScrollView orientation: NSVerticalRuler];
        
        if ( self != nil) {
            self.clientView = textView;
            self.ruleThickness = 40;
        }
    
        return self;
    }
    
    
    ===2. setup RulerView
    @implementation ViewController
    @property (nonatomic, strong) IBOutlet PETextView *textView;
    - (void)viewDidLoad {
        [self.textView initRulerView];
    }
    
    ===3. create RulerView
    @interface PETextView: NSTextView
    - (void)initRulerView{
        rulerView = [[PERulerView alloc] initWithTextView: self];
        NSScrollView *scrollView = [self enclosingScrollView];
        [scrollView setVerticalRulerView: rulerView];
        [scrollView setHasHorizontalRuler: NO];
        [scrollView setHasVerticalRuler: YES];
        [scrollView setRulersVisible: YES];
    
        self.postsFrameChangedNotifications = YES;
        [[NSNotificationCenter defaultCenter] addObserver: self
                                                 selector: @selector(textViewFrameDidChange:)
                                                     name: NSViewFrameDidChangeNotification
                                                   object: self];
        
        [[NSNotificationCenter defaultCenter] addObserver: self
                                                 selector: @selector(textViewTextDidChange:)
                                                     name: NSTextDidChangeNotification
                                                   object: self];
    }
    
    
    - (void)textViewFrameDidChange: (NSNotification *)notif{
        [rulerView setNeedsDisplay: YES];
    }
    
    - (void)textViewTextDidChange: (NSNotification *)notif{
        [rulerView setNeedsDisplay: YES];
    }
    ===3. end
    
    
    ===4. draw code
    - (void)doDrawHashMarksAndLabels:(NSRect)rect{
            if(isDrawing) return;
            NSTextView *textView = (NSTextView *)[self clientView];
            NSLayoutManager *layoutManager = [textView layoutManager];
            if(layoutManager == nil) return;
        
            isDrawing = YES;
            NSLog(@"doDrawHashMarksAndLabels");
            CGPoint relativePoint = [self convertPoint: NSZeroPoint fromView: textView];
            NSMutableDictionary *attr = [NSMutableDictionary new];
            attr[NSFontAttributeName] = textView.font;
            attr[NSForegroundColorAttributeName] = [NSColor redColor];
        
            static void(^drawLineNumber)(NSDictionary *a, NSString *lineNumberString, CGFloat y) = nil;
            if (!drawLineNumber) {
                drawLineNumber = ^(NSDictionary *a, NSString *lineNumberString, CGFloat y) {
                    NSAttributedString *attString = [[NSAttributedString alloc] initWithString: lineNumberString attributes: a];
                    NSSize stringSize = [lineNumberString sizeWithAttributes: a];
                    // Calculate the x position, within the gutter.
                    CGFloat x = 40 - stringSize.width;
                    // Draw the attributed string to the calculated point.
                    [attString drawAtPoint: NSMakePoint(x, y)];
                    NSLog(@"drawLineNumber %@.", lineNumberString);
                };
            }
        
            
            NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect: textView.visibleRect inTextContainer: textView.textContainer];
            NSUInteger firstVisibleGlyphCharacterIndex = [layoutManager characterIndexForGlyphAtIndex: visibleGlyphRange.location];
            NSRegularExpression *newLineRegex = [[NSRegularExpression alloc] initWithPattern: @"\n" options:NSRegularExpressionCaseInsensitive error: nil];
            NSRange matchRage = NSMakeRange(0, firstVisibleGlyphCharacterIndex);
            NSArray *matches = [newLineRegex matchesInString:textView.string options:NSMatchingReportProgress range: matchRage];
            NSUInteger lineNumber = [matches count] + 1;
            NSLog(@"lineNumber: %ld", lineNumber);
        
            NSUInteger glyphIndexForStringLine = visibleGlyphRange.location;
    
            // Go through each line in the string.
            while( glyphIndexForStringLine < NSMaxRange(visibleGlyphRange) ){
                
                // Range of current line in the string.
                NSUInteger characterIndex = [layoutManager characterIndexForGlyphAtIndex: glyphIndexForStringLine];
                NSRange characterRangeForStringLine = [textView.string lineRangeForRange:NSMakeRange(characterIndex, 0)];
                
                NSRange glyphRangeForStringLine = [layoutManager glyphRangeForCharacterRange:characterRangeForStringLine
                                                 actualCharacterRange: nil];
                
                NSUInteger glyphIndexForGlyphLine = glyphIndexForStringLine;
                NSUInteger glyphLineCount = 0;
                
                while ( glyphIndexForGlyphLine < NSMaxRange(glyphRangeForStringLine) ) {
                    
                    // See if the current line in the string spread across
                    // several lines of glyphs
                    NSRange effectiveRange = NSMakeRange(0, 0);
                    
                    // Range of current "line of glyphs". If a line is wrapped,
                    // then it will have more than one "line of glyphs"
                    
                    NSRect lineRect  = [layoutManager lineFragmentRectForGlyphAtIndex:glyphIndexForGlyphLine
                                                                       effectiveRange: &effectiveRange withoutAdditionalLayout: YES];
                    
                    if (glyphLineCount > 0 ){
                        drawLineNumber(attr, @"-", NSMinY(lineRect)+relativePoint.y);
                    } else {
                        drawLineNumber(attr, [NSString stringWithFormat: @"%ld", lineNumber], NSMinY(lineRect)+relativePoint.y);
                    }
                    
                    // Move to next glyph line
                    glyphLineCount += 1;
                    glyphIndexForGlyphLine = NSMaxRange(effectiveRange);
                }
                
                glyphIndexForStringLine = NSMaxRange(glyphRangeForStringLine);
                lineNumber += 1;
            }
        
            // Draw line number for the extra line at the end of the text
            if([layoutManager extraLineFragmentTextContainer] != nil){
                drawLineNumber(attr, [NSString  stringWithFormat: @"%ld", lineNumber], NSMinY(layoutManager.extraLineFragmentRect)+relativePoint.y);
            }
            
            isDrawing = NO;
    }
    
    - (void)drawHashMarksAndLabels:(NSRect)rect{
        NSLog(@"drawHashMarksAndLabels");
        
        id obj = [self clientView];
        if ([obj isKindOfClass:[NSTextView class]]){
            [self doDrawHashMarksAndLabels: rect];
        }
    }
    ===4. end


The env: Xcode 16.2 on macOS Sonoma 14.6.1, and the screenshot of the result: enter image description here


Solution

  • drawHashMarksAndLabels does not exist in AppKit. The method is called drawHashMarksAndLabels(in:) in Swift and drawHashMarksAndLabelsInRect: in Objective-C. Note the InRect at the end.

    Replace

    - (void)drawHashMarksAndLabels:(NSRect)rect{
    

    by

    - (void)drawHashMarksAndLabelsInRect:(NSRect)rect {
    

    Documentation drawHashMarksAndLabelsInRect: