lib / blame.tclon commit git-gui: Display tooltips in blame viewer (41bf23d)
   1# git-gui blame viewer
   2# Copyright (C) 2006, 2007 Shawn Pearce
   3
   4class blame {
   5
   6field commit  ; # input commit to blame
   7field path    ; # input filename to view in $commit
   8
   9field w
  10field w_line
  11field w_cgrp
  12field w_load
  13field w_file
  14field w_cmit
  15field status
  16
  17field highlight_line   -1 ; # current line selected
  18field highlight_commit {} ; # sha1 of commit selected
  19
  20field total_lines       0  ; # total length of file
  21field blame_lines       0  ; # number of lines computed
  22field commit_count      0  ; # number of commits in $commit_list
  23field commit_list      {}  ; # list of commit sha1 in receipt order
  24field order                ; # array commit -> receipt order
  25field header               ; # array commit,key -> header field
  26field line_commit          ; # array line -> sha1 commit
  27field line_file            ; # array line -> file name
  28
  29field r_commit      ; # commit currently being parsed
  30field r_orig_line   ; # original line number
  31field r_final_line  ; # final line number
  32field r_line_count  ; # lines in this region
  33
  34field tooltip_wm     {} ; # Current tooltip toplevel, if open
  35field tooltip_timer  {} ; # Current timer event for our tooltip
  36field tooltip_commit {} ; # Commit in tooltip
  37field tooltip_text   {} ; # Text in current tooltip
  38
  39variable active_color #98e1a0
  40variable group_colors {
  41        #cbcbcb
  42        #e1e1e1
  43}
  44
  45constructor new {i_commit i_path} {
  46        global cursor_ptr
  47
  48        set commit $i_commit
  49        set path   $i_path
  50
  51        make_toplevel top w
  52        wm title $top "[appname] ([reponame]): File Viewer"
  53        set status "Loading $commit:$path..."
  54
  55        label $w.path -text "$commit:$path" \
  56                -anchor w \
  57                -justify left \
  58                -borderwidth 1 \
  59                -relief sunken \
  60                -font font_uibold
  61        pack $w.path -side top -fill x
  62
  63        frame $w.out
  64        set w_load $w.out.loaded_t
  65        text $w_load \
  66                -background white -borderwidth 0 \
  67                -state disabled \
  68                -wrap none \
  69                -height 40 \
  70                -width 1 \
  71                -font font_diff
  72        $w_load tag conf annotated -background grey
  73
  74        set w_line $w.out.linenumber_t
  75        text $w_line \
  76                -background white -borderwidth 0 \
  77                -state disabled \
  78                -wrap none \
  79                -height 40 \
  80                -width 5 \
  81                -font font_diff
  82        $w_line tag conf linenumber -justify right
  83
  84        set w_cgrp $w.out.commit_t
  85        text $w_cgrp \
  86                -background white -borderwidth 0 \
  87                -state disabled \
  88                -wrap none \
  89                -height 40 \
  90                -width 4 \
  91                -font font_diff
  92
  93        set w_file $w.out.file_t
  94        text $w_file \
  95                -background white -borderwidth 0 \
  96                -state disabled \
  97                -wrap none \
  98                -height 40 \
  99                -width 80 \
 100                -xscrollcommand [list $w.out.sbx set] \
 101                -font font_diff
 102
 103        scrollbar $w.out.sbx -orient h -command [list $w_file xview]
 104        scrollbar $w.out.sby -orient v \
 105                -command [list scrollbar2many [list \
 106                $w_load \
 107                $w_line \
 108                $w_cgrp \
 109                $w_file \
 110                ] yview]
 111        grid \
 112                $w_cgrp \
 113                $w_line \
 114                $w_load \
 115                $w_file \
 116                $w.out.sby \
 117                -sticky nsew
 118        grid conf $w.out.sbx -column 3 -sticky we
 119        grid columnconfigure $w.out 3 -weight 1
 120        grid rowconfigure $w.out 0 -weight 1
 121        pack $w.out -fill both -expand 1
 122
 123        label $w.status \
 124                -textvariable @status \
 125                -anchor w \
 126                -justify left \
 127                -borderwidth 1 \
 128                -relief sunken
 129        pack $w.status -side bottom -fill x
 130
 131        frame $w.cm
 132        set w_cmit $w.cm.t
 133        text $w_cmit \
 134                -background white -borderwidth 0 \
 135                -state disabled \
 136                -wrap none \
 137                -height 10 \
 138                -width 80 \
 139                -xscrollcommand [list $w.cm.sbx set] \
 140                -yscrollcommand [list $w.cm.sby set] \
 141                -font font_diff
 142        scrollbar $w.cm.sbx -orient h -command [list $w_cmit xview]
 143        scrollbar $w.cm.sby -orient v -command [list $w_cmit yview]
 144        pack $w.cm.sby -side right -fill y
 145        pack $w.cm.sbx -side bottom -fill x
 146        pack $w_cmit -expand 1 -fill both
 147        pack $w.cm -side bottom -fill x
 148
 149        menu $w.ctxm -tearoff 0
 150        $w.ctxm add command \
 151                -label "Copy Commit" \
 152                -command [cb _copycommit]
 153
 154        foreach i [list \
 155                $w_cgrp \
 156                $w_load \
 157                $w_line \
 158                $w_file] {
 159                $i conf -cursor $cursor_ptr
 160                $i conf -yscrollcommand \
 161                        [list many2scrollbar [list \
 162                        $w_cgrp \
 163                        $w_load \
 164                        $w_line \
 165                        $w_file \
 166                        ] yview $w.out.sby]
 167                bind $i <Button-1>   "
 168                        [cb _hide_tooltip]
 169                        [cb _click $i @%x,%y]
 170                        focus $i
 171                "
 172                bind $i <Any-Motion>  [cb _show_tooltip $i @%x,%y]
 173                bind $i <Any-Enter>   [cb _hide_tooltip]
 174                bind $i <Any-Leave>   [cb _hide_tooltip]
 175                bind_button3 $i "
 176                        [cb _hide_tooltip]
 177                        set cursorX %x
 178                        set cursorY %y
 179                        set cursorW %W
 180                        tk_popup $w.ctxm %X %Y
 181                "
 182        }
 183
 184        foreach i [list \
 185                $w_cgrp \
 186                $w_load \
 187                $w_line \
 188                $w_file \
 189                $w_cmit] {
 190                bind $i <Key-Up>        {catch {%W yview scroll -1 units};break}
 191                bind $i <Key-Down>      {catch {%W yview scroll  1 units};break}
 192                bind $i <Key-Left>      {catch {%W xview scroll -1 units};break}
 193                bind $i <Key-Right>     {catch {%W xview scroll  1 units};break}
 194                bind $i <Key-k>         {catch {%W yview scroll -1 units};break}
 195                bind $i <Key-j>         {catch {%W yview scroll  1 units};break}
 196                bind $i <Key-h>         {catch {%W xview scroll -1 units};break}
 197                bind $i <Key-l>         {catch {%W xview scroll  1 units};break}
 198                bind $i <Control-Key-b> {catch {%W yview scroll -1 pages};break}
 199                bind $i <Control-Key-f> {catch {%W yview scroll  1 pages};break}
 200        }
 201
 202        bind $w_cmit <Button-1> [list focus $w_cmit]
 203        bind $top <Visibility> [list focus $top]
 204        bind $top <Destroy> [list delete_this $this]
 205
 206        if {$commit eq {}} {
 207                set fd [open $path r]
 208        } else {
 209                set cmd [list git cat-file blob "$commit:$path"]
 210                set fd [open "| $cmd" r]
 211        }
 212        fconfigure $fd -blocking 0 -translation lf -encoding binary
 213        fileevent $fd readable [cb _read_file $fd]
 214}
 215
 216method _read_file {fd} {
 217        $w_load conf -state normal
 218        $w_cgrp conf -state normal
 219        $w_line conf -state normal
 220        $w_file conf -state normal
 221        while {[gets $fd line] >= 0} {
 222                regsub "\r\$" $line {} line
 223                incr total_lines
 224
 225                if {$total_lines > 1} {
 226                        $w_load insert end "\n"
 227                        $w_cgrp insert end "\n"
 228                        $w_line insert end "\n"
 229                        $w_file insert end "\n"
 230                }
 231
 232                $w_line insert end "$total_lines" linenumber
 233                $w_file insert end "$line"
 234        }
 235        $w_load conf -state disabled
 236        $w_cgrp conf -state disabled
 237        $w_line conf -state disabled
 238        $w_file conf -state disabled
 239
 240        if {[eof $fd]} {
 241                close $fd
 242                _status $this
 243                set cmd [list git blame -M -C --incremental]
 244                if {$commit eq {}} {
 245                        lappend cmd --contents $path
 246                } else {
 247                        lappend cmd $commit
 248                }
 249                lappend cmd -- $path
 250                set fd [open "| $cmd" r]
 251                fconfigure $fd -blocking 0 -translation lf -encoding binary
 252                fileevent $fd readable [cb _read_blame $fd]
 253        }
 254} ifdeleted { catch {close $fd} }
 255
 256method _read_blame {fd} {
 257        variable group_colors
 258
 259        $w_cgrp conf -state normal
 260        while {[gets $fd line] >= 0} {
 261                if {[regexp {^([a-z0-9]{40}) (\d+) (\d+) (\d+)$} $line line \
 262                        cmit original_line final_line line_count]} {
 263                        set r_commit     $cmit
 264                        set r_orig_line  $original_line
 265                        set r_final_line $final_line
 266                        set r_line_count $line_count
 267
 268                        if {[catch {set g $order($cmit)}]} {
 269                                set bg [lindex $group_colors 0]
 270                                set group_colors [lrange $group_colors 1 end]
 271                                lappend group_colors $bg
 272
 273                                $w_cgrp tag conf g$cmit -background $bg
 274                                $w_line tag conf g$cmit -background $bg
 275                                $w_file tag conf g$cmit -background $bg
 276
 277                                set order($cmit) $commit_count
 278                                incr commit_count
 279                                lappend commit_list $cmit
 280                        }
 281                } elseif {[string match {filename *} $line]} {
 282                        set file [string range $line 9 end]
 283                        set n    $r_line_count
 284                        set lno  $r_final_line
 285                        set cmit $r_commit
 286
 287                        if {[regexp {^0{40}$} $cmit]} {
 288                                set abbr work
 289                        } else {
 290                                set abbr [string range $cmit 0 4]
 291                        }
 292
 293                        if {![catch {set ncmit $line_commit([expr {$lno - 1}])}]} {
 294                                if {$ncmit eq $cmit} {
 295                                        set abbr |
 296                                }
 297                        }
 298
 299                        while {$n > 0} {
 300                                set lno_e "$lno.0 lineend + 1c"
 301                                if {[catch {set g g$line_commit($lno)}]} {
 302                                        $w_load tag add annotated $lno.0 $lno_e
 303                                } else {
 304                                        $w_cgrp tag remove g$g $lno.0 $lno_e
 305                                        $w_line tag remove g$g $lno.0 $lno_e
 306                                        $w_file tag remove g$g $lno.0 $lno_e
 307
 308                                        $w_cgrp tag remove a$g $lno.0 $lno_e
 309                                        $w_line tag remove a$g $lno.0 $lno_e
 310                                        $w_file tag remove a$g $lno.0 $lno_e
 311                                }
 312
 313                                set line_commit($lno) $cmit
 314                                set line_file($lno)   $file
 315
 316                                $w_cgrp delete $lno.0 "$lno.0 lineend"
 317                                $w_cgrp insert $lno.0 $abbr
 318                                set abbr |
 319
 320                                $w_cgrp tag add g$cmit $lno.0 $lno_e
 321                                $w_line tag add g$cmit $lno.0 $lno_e
 322                                $w_file tag add g$cmit $lno.0 $lno_e
 323
 324                                $w_cgrp tag add a$cmit $lno.0 $lno_e
 325                                $w_line tag add a$cmit $lno.0 $lno_e
 326                                $w_file tag add a$cmit $lno.0 $lno_e
 327
 328                                if {$highlight_line == -1} {
 329                                        if {[lindex [$w_file yview] 0] == 0} {
 330                                                $w_file see $lno.0
 331                                                _showcommit $this $lno
 332                                        }
 333                                } elseif {$highlight_line == $lno} {
 334                                        _showcommit $this $lno
 335                                }
 336
 337                                incr n -1
 338                                incr lno
 339                                incr blame_lines
 340                        }
 341
 342                        if {![catch {set ncmit $line_commit($lno)}]} {
 343                                if {$ncmit eq $cmit} {
 344                                        $w_cgrp delete $lno.0 "$lno.0 lineend + 1c"
 345                                        $w_cgrp insert $lno.0 "|\n"
 346                                }
 347                        }
 348
 349                        set hc $highlight_commit
 350                        if {$hc ne {}
 351                                && [expr {$order($hc) + 1}] == $order($cmit)} {
 352                                _showcommit $this $highlight_line
 353                        }
 354                } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
 355                        set header($r_commit,$key) $data
 356                }
 357        }
 358        $w_cgrp conf -state disabled
 359
 360        if {[eof $fd]} {
 361                close $fd
 362                set status {Annotation complete.}
 363        } else {
 364                _status $this
 365        }
 366} ifdeleted { catch {close $fd} }
 367
 368method _status {} {
 369        set have  $blame_lines
 370        set total $total_lines
 371        set pdone 0
 372        if {$total} {set pdone [expr {100 * $have / $total}]}
 373
 374        set status [format \
 375                "Loading annotations... %i of %i lines annotated (%2i%%)" \
 376                $have $total $pdone]
 377}
 378
 379method _click {cur_w pos} {
 380        set lno [lindex [split [$cur_w index $pos] .] 0]
 381        if {$lno eq {}} return
 382        _showcommit $this $lno
 383}
 384
 385method _showcommit {lno} {
 386        global repo_config
 387        variable active_color
 388
 389        if {$highlight_commit ne {}} {
 390                set cmit $highlight_commit
 391                $w_cgrp tag conf a$cmit -background {}
 392                $w_line tag conf a$cmit -background {}
 393                $w_file tag conf a$cmit -background {}
 394        }
 395
 396        $w_cmit conf -state normal
 397        $w_cmit delete 0.0 end
 398        if {[catch {set cmit $line_commit($lno)}]} {
 399                set cmit {}
 400                $w_cmit insert end "Loading annotation..."
 401        } else {
 402                $w_cgrp tag conf a$cmit -background $active_color
 403                $w_line tag conf a$cmit -background $active_color
 404                $w_file tag conf a$cmit -background $active_color
 405
 406                set author_name {}
 407                set author_email {}
 408                set author_time {}
 409                catch {set author_name $header($cmit,author)}
 410                catch {set author_email $header($cmit,author-mail)}
 411                catch {set author_time [clock format \
 412                        $header($cmit,author-time) \
 413                        -format {%Y-%m-%d %H:%M:%S}
 414                ]}
 415
 416                set committer_name {}
 417                set committer_email {}
 418                set committer_time {}
 419                catch {set committer_name $header($cmit,committer)}
 420                catch {set committer_email $header($cmit,committer-mail)}
 421                catch {set committer_time [clock format \
 422                        $header($cmit,committer-time) \
 423                        -format {%Y-%m-%d %H:%M:%S}
 424                ]}
 425
 426                if {[catch {set msg $header($cmit,message)}]} {
 427                        set msg {}
 428                        catch {
 429                                set fd [open "| git cat-file commit $cmit" r]
 430                                fconfigure $fd -encoding binary -translation lf
 431                                if {[catch {set enc $repo_config(i18n.commitencoding)}]} {
 432                                        set enc utf-8
 433                                }
 434                                while {[gets $fd line] > 0} {
 435                                        if {[string match {encoding *} $line]} {
 436                                                set enc [string tolower [string range $line 9 end]]
 437                                        }
 438                                }
 439                                set msg [encoding convertfrom $enc [read $fd]]
 440                                set msg [string trim $msg]
 441                                close $fd
 442
 443                                set author_name [encoding convertfrom $enc $author_name]
 444                                set committer_name [encoding convertfrom $enc $committer_name]
 445
 446                                set header($cmit,author) $author_name
 447                                set header($cmit,committer) $committer_name
 448                        }
 449                        set header($cmit,message) $msg
 450                }
 451
 452                $w_cmit insert end "commit $cmit
 453Author: $author_name $author_email  $author_time
 454Committer: $committer_name $committer_email  $committer_time
 455Original File: [escape_path $line_file($lno)]
 456
 457$msg"
 458        }
 459        $w_cmit conf -state disabled
 460
 461        set highlight_line $lno
 462        set highlight_commit $cmit
 463
 464        if {$highlight_commit eq $tooltip_commit} {
 465                _hide_tooltip $this
 466        }
 467}
 468
 469method _copycommit {} {
 470        set pos @$::cursorX,$::cursorY
 471        set lno [lindex [split [$::cursorW index $pos] .] 0]
 472        if {![catch {set commit $line_commit($lno)}]} {
 473                clipboard clear
 474                clipboard append \
 475                        -format STRING \
 476                        -type STRING \
 477                        -- $commit
 478        }
 479}
 480
 481method _show_tooltip {cur_w pos} {
 482        set lno [lindex [split [$cur_w index $pos] .] 0]
 483        if {[catch {set cmit $line_commit($lno)}]} {
 484                _hide_tooltip $this
 485                return
 486        }
 487
 488        if {$cmit eq $highlight_commit} {
 489                _hide_tooltip $this
 490                return
 491        }
 492
 493        if {$cmit eq $tooltip_commit} {
 494                _position_tooltip $this
 495        } elseif {$tooltip_wm ne {}} {
 496                _open_tooltip $this $cur_w
 497        } elseif {$tooltip_timer eq {}} {
 498                set tooltip_timer [after 1000 [cb _open_tooltip $cur_w]]
 499        }
 500}
 501
 502method _open_tooltip {cur_w} {
 503        set tooltip_timer {}
 504        set pos_x [winfo pointerx $cur_w]
 505        set pos_y [winfo pointery $cur_w]
 506        if {[winfo containing $pos_x $pos_y] ne $cur_w} {
 507                _hide_tooltip $this
 508                return
 509        }
 510
 511        set pos @[join [list \
 512                [expr {$pos_x - [winfo rootx $cur_w]}] \
 513                [expr {$pos_y - [winfo rooty $cur_w]}]] ,]
 514        set lno [lindex [split [$cur_w index $pos] .] 0]
 515        set cmit $line_commit($lno)
 516
 517        set author_name {}
 518        set author_email {}
 519        set author_time {}
 520        catch {set author_name $header($cmit,author)}
 521        catch {set author_email $header($cmit,author-mail)}
 522        catch {set author_time [clock format \
 523                $header($cmit,author-time) \
 524                -format {%Y-%m-%d %H:%M:%S}
 525        ]}
 526
 527        set committer_name {}
 528        set committer_email {}
 529        set committer_time {}
 530        catch {set committer_name $header($cmit,committer)}
 531        catch {set committer_email $header($cmit,committer-mail)}
 532        catch {set committer_time [clock format \
 533                $header($cmit,committer-time) \
 534                -format {%Y-%m-%d %H:%M:%S}
 535        ]}
 536
 537        set summary {}
 538        catch {set summary $header($cmit,summary)}
 539
 540        set tooltip_commit $cmit
 541        set tooltip_text "commit $cmit
 542$author_name $author_email  $author_time
 543$summary"
 544
 545        if {$tooltip_wm ne "$cur_w.tooltip"} {
 546                _hide_tooltip $this
 547
 548                set tooltip_wm [toplevel $cur_w.tooltip -borderwidth 1]
 549                wm overrideredirect $tooltip_wm 1
 550                wm transient $tooltip_wm [winfo toplevel $cur_w]
 551                pack [label $tooltip_wm.label \
 552                        -background lightyellow \
 553                        -foreground black \
 554                        -textvariable @tooltip_text \
 555                        -justify left]
 556        }
 557        _position_tooltip $this
 558}
 559
 560method _position_tooltip {} {
 561        set req_w [winfo reqwidth  $tooltip_wm.label]
 562        set req_h [winfo reqheight $tooltip_wm.label]
 563        set pos_x [expr {[winfo pointerx .] +  5}]
 564        set pos_y [expr {[winfo pointery .] + 10}]
 565
 566        set g "${req_w}x${req_h}"
 567        if {$pos_x >= 0} {append g +}
 568        append g $pos_x
 569        if {$pos_y >= 0} {append g +}
 570        append g $pos_y
 571
 572        wm geometry $tooltip_wm $g
 573        raise $tooltip_wm
 574}
 575
 576method _hide_tooltip {} {
 577        if {$tooltip_wm ne {}} {
 578                destroy $tooltip_wm
 579                set tooltip_wm {}
 580                set tooltip_commit {}
 581        }
 582        if {$tooltip_timer ne {}} {
 583                after cancel $tooltip_timer
 584                set tooltip_timer {}
 585        }
 586}
 587
 588}