git-gui: Display tooltips in blame viewer
[git/jrn.git] / lib / blame.tcl
blobfef28a347e84752f0357d061c95db307ad3caf8b
1 # git-gui blame viewer
2 # Copyright (C) 2006, 2007 Shawn Pearce
4 class blame {
6 field commit ; # input commit to blame
7 field path ; # input filename to view in $commit
9 field w
10 field w_line
11 field w_cgrp
12 field w_load
13 field w_file
14 field w_cmit
15 field status
17 field highlight_line -1 ; # current line selected
18 field highlight_commit {} ; # sha1 of commit selected
20 field total_lines 0 ; # total length of file
21 field blame_lines 0 ; # number of lines computed
22 field commit_count 0 ; # number of commits in $commit_list
23 field commit_list {} ; # list of commit sha1 in receipt order
24 field order ; # array commit -> receipt order
25 field header ; # array commit,key -> header field
26 field line_commit ; # array line -> sha1 commit
27 field line_file ; # array line -> file name
29 field r_commit ; # commit currently being parsed
30 field r_orig_line ; # original line number
31 field r_final_line ; # final line number
32 field r_line_count ; # lines in this region
34 field tooltip_wm {} ; # Current tooltip toplevel, if open
35 field tooltip_timer {} ; # Current timer event for our tooltip
36 field tooltip_commit {} ; # Commit in tooltip
37 field tooltip_text {} ; # Text in current tooltip
39 variable active_color #98e1a0
40 variable group_colors {
41 #cbcbcb
42 #e1e1e1
45 constructor new {i_commit i_path} {
46 global cursor_ptr
48 set commit $i_commit
49 set path $i_path
51 make_toplevel top w
52 wm title $top "[appname] ([reponame]): File Viewer"
53 set status "Loading $commit:$path..."
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
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
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
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
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
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
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
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
149 menu $w.ctxm -tearoff 0
150 $w.ctxm add command \
151 -label "Copy Commit" \
152 -command [cb _copycommit]
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
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
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}
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]
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]
212 fconfigure $fd -blocking 0 -translation lf -encoding binary
213 fileevent $fd readable [cb _read_file $fd]
216 method _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
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"
232 $w_line insert end "$total_lines" linenumber
233 $w_file insert end "$line"
235 $w_load conf -state disabled
236 $w_cgrp conf -state disabled
237 $w_line conf -state disabled
238 $w_file conf -state disabled
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
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]
254 } ifdeleted { catch {close $fd} }
256 method _read_blame {fd} {
257 variable group_colors
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
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
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
277 set order($cmit) $commit_count
278 incr commit_count
279 lappend commit_list $cmit
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
287 if {[regexp {^0{40}$} $cmit]} {
288 set abbr work
289 } else {
290 set abbr [string range $cmit 0 4]
293 if {![catch {set ncmit $line_commit([expr {$lno - 1}])}]} {
294 if {$ncmit eq $cmit} {
295 set abbr |
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
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
313 set line_commit($lno) $cmit
314 set line_file($lno) $file
316 $w_cgrp delete $lno.0 "$lno.0 lineend"
317 $w_cgrp insert $lno.0 $abbr
318 set abbr |
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
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
328 if {$highlight_line == -1} {
329 if {[lindex [$w_file yview] 0] == 0} {
330 $w_file see $lno.0
331 _showcommit $this $lno
333 } elseif {$highlight_line == $lno} {
334 _showcommit $this $lno
337 incr n -1
338 incr lno
339 incr blame_lines
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"
349 set hc $highlight_commit
350 if {$hc ne {}
351 && [expr {$order($hc) + 1}] == $order($cmit)} {
352 _showcommit $this $highlight_line
354 } elseif {[regexp {^([a-z-]+) (.*)$} $line line key data]} {
355 set header($r_commit,$key) $data
358 $w_cgrp conf -state disabled
360 if {[eof $fd]} {
361 close $fd
362 set status {Annotation complete.}
363 } else {
364 _status $this
366 } ifdeleted { catch {close $fd} }
368 method _status {} {
369 set have $blame_lines
370 set total $total_lines
371 set pdone 0
372 if {$total} {set pdone [expr {100 * $have / $total}]}
374 set status [format \
375 "Loading annotations... %i of %i lines annotated (%2i%%)" \
376 $have $total $pdone]
379 method _click {cur_w pos} {
380 set lno [lindex [split [$cur_w index $pos] .] 0]
381 if {$lno eq {}} return
382 _showcommit $this $lno
385 method _showcommit {lno} {
386 global repo_config
387 variable active_color
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 {}
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
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}
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}
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
434 while {[gets $fd line] > 0} {
435 if {[string match {encoding *} $line]} {
436 set enc [string tolower [string range $line 9 end]]
439 set msg [encoding convertfrom $enc [read $fd]]
440 set msg [string trim $msg]
441 close $fd
443 set author_name [encoding convertfrom $enc $author_name]
444 set committer_name [encoding convertfrom $enc $committer_name]
446 set header($cmit,author) $author_name
447 set header($cmit,committer) $committer_name
449 set header($cmit,message) $msg
452 $w_cmit insert end "commit $cmit
453 Author: $author_name $author_email $author_time
454 Committer: $committer_name $committer_email $committer_time
455 Original File: [escape_path $line_file($lno)]
457 $msg"
459 $w_cmit conf -state disabled
461 set highlight_line $lno
462 set highlight_commit $cmit
464 if {$highlight_commit eq $tooltip_commit} {
465 _hide_tooltip $this
469 method _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
481 method _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
488 if {$cmit eq $highlight_commit} {
489 _hide_tooltip $this
490 return
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]]
502 method _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
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)
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}
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}
537 set summary {}
538 catch {set summary $header($cmit,summary)}
540 set tooltip_commit $cmit
541 set tooltip_text "commit $cmit
542 $author_name $author_email $author_time
543 $summary"
545 if {$tooltip_wm ne "$cur_w.tooltip"} {
546 _hide_tooltip $this
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]
557 _position_tooltip $this
560 method _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}]
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
572 wm geometry $tooltip_wm $g
573 raise $tooltip_wm
576 method _hide_tooltip {} {
577 if {$tooltip_wm ne {}} {
578 destroy $tooltip_wm
579 set tooltip_wm {}
580 set tooltip_commit {}
582 if {$tooltip_timer ne {}} {
583 after cancel $tooltip_timer
584 set tooltip_timer {}