line numbers in text widget

Richard Suchenwirth 2008-03-18 - The following codelet allows to display line numbers in a text widget. Note that they are just literally inserted at beginnings of lines, so if you do copy & paste of multiple lines, best toggle line number display off.

proc linenums {w} {
    if [llength [$w tag ranges linenum]] {
        foreach {from to} [$w tag ranges linenum] {
            $w delete $from $to
        }
    } else {
        set lastline [expr int([$w index "end - 1 c"])]
        for {set i 1} {$i <= $lastline} {incr i} {
            $w insert $i.0 [format "%5d " $i] linenum
        }
    }    
}

Test and demo - the source file itself is displayed, line numbers on/off are toggled with F1:

proc readfile filename {
    set f [open $filename]
    return [read $f][close $f]
}

pack [text .t]
.t tag configure linenum -background yellow
.t insert 1.0 [readfile [info script]]
bind .t <F1> {linenums %W}

Bryan Oakley March 18, 2008 - here's my take on the subject. The above is great for quick-n-dirty but has several shortcomings. For example, if the text uses tabs and a fixed width font, things won't line up properly unless the width of the line numbers is a multiple of the tabs (and even then there can be problems if using the traditional tk tabstops).

Another problem is that if the user wants to select a range of text to copy they'll end up with the line numbers. Maybe that's good but since the line numbers are metadata, usually you want just the actual data when copying.

Here's a solution that draws line numbers on demand. It seems to work quite well in practice. This solution requires Tk 8.5.

package require Tk 8.5
proc main {} {
    text .text \
        -wrap word \
        -borderwidth 0 \
        -yscrollcommand [list .vsb set]
    canvas .canvas \
        -width 20 \
        -highlightthickness 0 \
        -background white
    scrollbar .vsb \
        -borderwidth 1 \
        -command [list .text yview]

    pack .vsb -side right -fill y
    pack .canvas -side left -fill y
    pack .text -side left -fill both -expand true

    # Arrange for line numbers to be redrawn when just about anything
    # happens to the text widget. This runs much faster than you might
    # think.
    trace add execution .text leave [list traceCallback .text .canvas]
    bind .text <Configure> [list traceCallback .text .canvas]

    set f [open [info script] r]
    set data [read $f]
    close $f
    .text insert end $data

}

proc traceCallback {text canvas args} {

    # only redraw if args are null (meaning we were called by a binding)
    # or called by the trace and the command could potentially change
    # the size of a line.
    set benign {
        mark bbox cget compare count debug dlineinfo
        dump get index mark peer search
    }
    if {[llength $args] == 0 ||
        [lindex $args 0 1] ni $benign} {

        $canvas delete all
        set i [$text index @0,0]
        while true {
            set dline [$text dlineinfo $i]
            if {[llength $dline] == 0} break
            set height [lindex $dline 3]
            set y [lindex $dline 1]
            set cy [expr {$y + int($height/2.0)}]
            set linenum [lindex [split $i .] 0]
            $canvas create text 0 $y -anchor nw -text $linenum
            set i [$text index "$i + 1 line"]
        }

    }
}
main

KD: To view the line/column where the insertion cursor is, add the following to the traceCallback procedure:

    if {[lindex $args 0 1] in {insert delete mark}} {
        scan [$text index insert] %d.%d ::line ::column
    }

And use the line/column global variables as textvariable in two labels, e.g. by adding the following to the main procedure:

    toplevel .lc
    pack [label .lc.line -textvariable line -width 5 -relief sunken -anchor e]
    pack [label .lc.col -textvariable column -width 5 -relief sunken -anchor e]

dzach 2011-9-1 This is really good! By adding the image:

            image create bitmap wrap_bit -foreground #aac -data {
              #define wrap_width 8
              #define wrap_height 8
              static unsigned char wrap_bits[] = {
                0x02, 0x02, 0x02, 0x22, 0x62, 0xfc, 0x60, 0x20
              };
            }

and the following code before changing i at the end of the loop, you can get small line wrap indicators:

            if {[$text count -ypixels $i "$i lineend"]} {
              $canvas create image 0 [expr {$y + [lindex $dline 4] + 8}] -image wrap_bit -anchor w
            }