#---------------------------------------------------------- -*- tcl -*-
#
# Eskil, a Graphical frontend to diff
#
# Copyright (c) 1998-2015, Peter Spjuth (peter.spjuth@gmail.com)
#
# Usage
# Do 'eskil' for interactive mode
# Do 'eskil --help' for command line usage
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; see the file COPYING. If not, write to
# the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
# Boston, MA 02111-1307, USA.
#
#----------------------------------------------------------------------
# This function is called when a toplevel is closed.
# If it is the last remaining toplevel, the application quits.
# If top = "all" it means quit.
# If eskil is embedded, this should be used to close an eskil toplevel.
proc cleanupAndExit {top} {
# A security thing to make sure we can exit.
set cont 0
if {[catch {
if {$top != "all"} {
set i [lsearch $::eskil(diffWindows) $top]
if {$i >= 0} {
set ::eskil(diffWindows) [lreplace $::eskil(diffWindows) $i $i]
}
set i [lsearch $::widgets(toolbars) $top.f]
if {$i >= 0} {
set ::widgets(toolbars) [lreplace $::widgets(toolbars) $i $i]
}
destroy $top
array unset ::eskil $top,*
# Any windows remaining?
if {[llength $::eskil(diffWindows)] > 0} {
set cont 1
}
}
} errMsg]} {
tk_messageBox -icon error -title "Eskil Error" -message \
"An error occured in the close process.\n$errMsg\n\
(This is a bug)\nTerminating application." -type ok
}
if {$cont} return
clearTmp
exit
}
# If embedding, tell eskil about any other toplevel, then
# cleanupAndExit can be used to get rid of it.
proc eskilRegisterToplevel {top} {
lappend ::eskil(diffWindows) $top
}
# Format a line number
proc myFormL {lineNo} {
if { ! [string is integer -strict $lineNo]} {return "$lineNo\n"}
return [format "%3d: \n" $lineNo]
}
# Get a name for a temporary file
# A tail can be given to make the file more recognisable.
proc tmpFile {{tail {}}} {
if {[info exists ::tmpcnt]} {
incr ::tmpcnt
} else {
set ::tmpcnt 0
}
set name "tmpd[pid]a$::tmpcnt"
if {$tail ne ""} {
append name " [file tail $tail]"
}
set name [file join $::eskil(tmpdir) $name]
lappend ::tmpfiles $name
return $name
}
# Delete temporary files
proc clearTmp {args} {
if { ! [info exists ::tmpfiles]} {
set ::tmpfiles {}
return
}
if {[llength $args] > 0} {
foreach f $args {
set i [lsearch -exact $::tmpfiles $f]
if {$i >= 0} {
catch {file delete $f}
set ::tmpfiles [lreplace $::tmpfiles $i $i]
}
}
} else {
foreach f $::tmpfiles {
catch {file delete -force $f}
}
set ::tmpfiles {}
}
}
# insertLine, when in table mode
proc insertLineTable {top side line text {tag equal}} {
set RE $::eskil($top,separator)
set words [split $text $RE]
set id [$::widgets($top,wTable) insert end $words]
if {$tag ne "equal"} {
set col 0
foreach word $words {
if {$side == 1} {
# TBD TABLE, r is faked here for now
dict set ::eskil($top,tablechanges) $id,$col w1 $word
dict set ::eskil($top,tablechanges) $id,$col w2 ""
dict set ::eskil($top,tablechanges) $id,$col r "0 0 1 1"
} else {
dict set ::eskil($top,tablechanges) $id,$col w1 ""
dict set ::eskil($top,tablechanges) $id,$col w2 $word
dict set ::eskil($top,tablechanges) $id,$col r "0 0 1 1"
}
incr col
}
}
}
# Insert lineno and text
proc insertLine {top side line text {tag {equal}} {linetag {}}} {
if {$::eskil($top,view) eq "table"} {
insertLineTable $top $side $line $text $tag
return
}
$::widgets($top,wDiff$side) insert end "$text\n" $tag
if {$linetag eq ""} {
set linetag $tag
}
if {$tag != "equal"} {
set linetag "hl$::HighLightCount $linetag"
}
$::widgets($top,wLine$side) insert end [myFormL $line] $linetag
}
# Insert an empty line on one side of the diff.
proc emptyLine {top side {highlight 1}} {
if {$::eskil($top,view) eq "table"} {
# This should be ignored for table
return
}
if {$highlight} {
$::widgets($top,wLine$side) insert end "\n" hl$::HighLightCount
} else {
$::widgets($top,wLine$side) insert end "*****\n"
}
$::widgets($top,wDiff$side) insert end "\n" padding
}
# Helper to take care of -sep case
# This can be used when diffing e.g. a CSV file.
# Each column will be handled separately, so differences will never be shown
# crossing a separator
proc diffWithSeparator {RE line1 line2 opts} {
set ixs1 [regexp -all -inline -indices -- $RE $line1]
set ixs2 [regexp -all -inline -indices -- $RE $line2]
# Fake a separator after end of line, makes the loop below simpler
lappend ixs1 [list [string length $line1] [string length $line1]]
lappend ixs2 [list [string length $line2] [string length $line2]]
# Res is at all times starting and ending with an equal pair
# i.e. same format as the result from DiffStrings
set res [list {} {}]
set s1 0
set s2 0
foreach ix1 $ixs1 ix2 $ixs2 {
# Handle if one index list is shorter
if {$ix1 eq ""} {
set str1 ""
set sep1 ""
} else {
lassign $ix1 e1 ns1
incr e1 -1
set str1 [string range $line1 $s1 $e1]
set sep1 [string range $line1 {*}$ix1]
}
if {$ix2 eq ""} {
set str2 ""
set sep2 ""
} else {
lassign $ix2 e2 ns2
incr e2 -1
set str2 [string range $line2 $s2 $e2]
set sep2 [string range $line2 {*}$ix2]
}
if {$str1 eq $str2} {
# Merge this equality with end of res
set resEq1 [lindex $res end-1]
set resEq2 [lindex $res end]
lset res end-1 $resEq1$str1$sep1
lset res end $resEq2$str2$sep2
} else {
set r [DiffUtil::diffStrings {*}$opts $str1 $str2]
# Starting equal pair
set rEq1a [lindex $r 0]
set rEq2a [lindex $r 1]
# Ending equal pair
set rEq1b [lindex $r end-1]
set rEq2b [lindex $r end]
# Mid part
set r [lrange $r 2 end-2]
# Merge starting equalities with end of res
set resEq1 [lindex $res end-1]
set resEq2 [lindex $res end]
lset res end-1 $resEq1$rEq1a
lset res end $resEq2$rEq2a
# Merge equality at end with separator
lappend res {*}$r $rEq1b$sep1 $rEq2b$sep2
}
set s1 [expr {$ns1 + 1}]
set s2 [expr {$ns2 + 1}]
}
#puts "RES '$res'"
return $res
}
# This is called from the table view whenever a cell is drawn.
# Add color as needed.
proc tblModeColorCallback {win W key row col tabIdx1 tabIdx2 inStripe selected} {
set cellX $key,$col
set top [winfo toplevel $win]
if { ! [dict exists $::eskil($top,tablechanges) $cellX]} {
# No changes, nothing to do here
return
}
set cinfo [dict get $::eskil($top,tablechanges) $cellX]
set w1 [dict get $cinfo w1]
set w2 [dict get $cinfo w2]
#puts "COLOR UPDATE W $win K $key R $row C $col TB1 $tabIdx1 TB2 $tabIdx2"
#puts " [string length $xxx] '$xxx'"
#puts " CHANGEME"
# Currently the displayed string is just $w1$w2
# The table might have cut of display of a cell so make sure to stay
# within the boundaries.
set txIdx1 [$W index $tabIdx1+1c]
set l1 [string length $w1]
set mid "$txIdx1 + $l1 char"
if {[$W compare $mid >= $tabIdx2]} {
set mid $tabIdx2
}
$W tag add new1 $txIdx1 $mid
$W tag add new2 $mid $tabIdx2
# Get the displayed string
set xxx [$W get $txIdx1 $tabIdx2]
if {$xxx ne "$w1$w2"} {
# Make sure dots are coloured
$W tag add change "$tabIdx2 - 3c" $tabIdx2
}
}
# insertMatchingLines, when in table mode
proc insertMatchingLinesTable {top line1 line2} {
global doingLine1 doingLine2
set opts $::Pref(ignore)
if {$::Pref(nocase)} {lappend opts -nocase}
if {$::Pref(lineparsewords)} {lappend opts -words}
set RE $::eskil($top,separator)
set words1 [split $line1 $RE]
set words2 [split $line2 $RE]
# Lap 1, make row data
set rs {}
set row {}
foreach w1 $words1 w2 $words2 {
set r [DiffUtil::diffStrings {*}$opts $w1 $w2]
# Store for next lap
lappend rs $r
if {[llength $r] <= 2} {
# Equal
lappend row $w1
} else {
# TBD TABLE, simple display for now
lappend row $w1$w2
}
}
set id [$::widgets($top,wTable) insert end $row]
# Lap 2, collect cell changes once we have the row id
set col -1
foreach w1 $words1 w2 $words2 r $rs {
incr col
# Equal? Skip
if {[llength $r] <= 2} continue
dict set ::eskil($top,tablechanges) $id,$col "w1" $w1
dict set ::eskil($top,tablechanges) $id,$col "w2" $w2
dict set ::eskil($top,tablechanges) $id,$col "r" $r
}
incr doingLine1
incr doingLine2
}
# Insert one line in each text widget.
# Mark them as changed, and optionally parse them.
proc insertMatchingLines {top line1 line2} {
global doingLine1 doingLine2
if {$::eskil($top,view) eq "table"} {
insertMatchingLinesTable $top $line1 $line2
return
}
if {$::Pref(parse) != 0} {
set opts $::Pref(ignore)
if {$::Pref(nocase)} {lappend opts -nocase}
if {$::Pref(lineparsewords)} {lappend opts -words}
if {$::eskil($top,separator) ne ""} {
set res [diffWithSeparator $::eskil($top,separator) $line1 $line2 \
$opts]
} else {
set res [DiffUtil::diffStrings {*}$opts $line1 $line2]
}
set dotag 0
set n [expr {[llength $res] / 2}]
$::widgets($top,wLine1) insert end [myFormL $doingLine1] \
"hl$::HighLightCount change"
$::widgets($top,wLine2) insert end [myFormL $doingLine2] \
"hl$::HighLightCount change"
set new1 "new1"
set new2 "new2"
set change "change"
foreach {i1 i2} $res {
incr n -1
if {$dotag} {
if {$n == 1 && $::Pref(marklast)} {
lappend new1 last
lappend new2 last
lappend change last
}
if {$i1 eq ""} {
$::widgets($top,wDiff2) insert end $i2 $new2
} elseif {$i2 eq ""} {
$::widgets($top,wDiff1) insert end $i1 $new1
} else {
$::widgets($top,wDiff1) insert end $i1 $change
$::widgets($top,wDiff2) insert end $i2 $change
}
set dotag 0
} else {
$::widgets($top,wDiff1) insert end $i1
$::widgets($top,wDiff2) insert end $i2
set dotag 1
}
}
$::widgets($top,wDiff1) insert end "\n"
$::widgets($top,wDiff2) insert end "\n"
} else {
insertLine $top 1 $doingLine1 $line1 "change"
insertLine $top 2 $doingLine2 $line2 "change"
}
incr doingLine1
incr doingLine2
}
# Detect if only newlines has changed within the block, e.g.
# when rearranging newlines.
# Rearranging newlines in comment blocks usually leads to
# words moving across "*", ignore * too.
# Returns 0 if the block in not handled here, non-zero if the block is done,
# negative if the block is considered not a change.
proc ParseBlocksAcrossNewline {top block1 block2} {
global doingLine1 doingLine2
set map {{ } {} \t {}}
set RE {\n\s*\*?|\s}
set equal 0
set visible [expr {$::eskil(ignorenewline) == 1}]
if 1 {
set block1nospace [regsub -all $RE [join $block1 \n] {}]
set block2nospace [regsub -all $RE [join $block2 \n] {}]
if {$block1nospace eq $block2nospace} {
set equal 1
}
} else {
set block1nospace [string map $map [join $block1 ""]]
set block2nospace [string map $map [join $block2 ""]]
if {$block1nospace eq $block2nospace} {
set equal 1
} else {
# Look for newlines rearranged in a comment block.
set block1nostar [string map {* {}} $block1nospace]
set block2nostar [string map {* {}} $block2nospace]
if {$block1nostar eq $block2nostar} {
set equal 1
}
}
}
if { ! $equal} {
return 0
}
if {$visible} {
set tag change
} else {
set tag {}
}
# Just insert the blocks
foreach line $block1 {
insertLine $top 1 $doingLine1 $line {} $tag
incr doingLine1
}
foreach line $block2 {
insertLine $top 2 $doingLine2 $line {} $tag
incr doingLine2
}
set n1 [llength $block1]
set n2 [llength $block2]
if {$n1 <= $n2} {
for {set t $n1} {$t < $n2} {incr t} {
if {$visible} {
$::widgets($top,wDiff1) insert end "\n" "padding change"
$::widgets($top,wLine1) insert end "\n" hl$::HighLightCount
} else {
emptyLine $top 1
}
}
} elseif {$n2 < $n1} {
if {$visible} {
for {set t $n2} {$t < $n1} {incr t} {
$::widgets($top,wDiff2) insert end "\n" "padding change"
$::widgets($top,wLine2) insert end "\n" hl$::HighLightCount
}
} else {
emptyLine $top 2
}
}
if {$visible} {
$::widgets($top,wDiff1) insert end "\n" "padding change"
$::widgets($top,wDiff2) insert end "\n" "padding change"
$::widgets($top,wLine1) insert end "\n" hl$::HighLightCount
$::widgets($top,wLine2) insert end "\n" hl$::HighLightCount
return [expr {($n1 > $n2 ? $n1 : $n2) + 1}]
} else {
return [expr {-($n1 > $n2 ? $n1 : $n2)}]
}
}
# Insert two blocks of lines in the compare windows.
# No extra parsing at all.
proc insertMatchingBlocksNoParse {top block1 block2 line1 line2 details} {
global doingLine1 doingLine2
set n1 [llength $block1]
set n2 [llength $block2]
# Is this a change block, a delete block or an insert block?
if {$n1 == 0} {set tag2 new2} else {set tag2 change}
if {$n2 == 0} {set tag1 new1} else {set tag1 change}
if {$n1 == $n2} {
# This should only happen for equal sized blocks that were deemed
# too large for block parsing.
foreach line1 $block1 line2 $block2 {
insertMatchingLines $top $line1 $line2
}
} else {
foreach line $block1 {
insertLine $top 1 $doingLine1 $line $tag1
incr doingLine1
}
foreach line $block2 {
insertLine $top 2 $doingLine2 $line $tag2
incr doingLine2
}
}
if {$n1 <= $n2} {
for {set t $n1} {$t < $n2} {incr t} {
emptyLine $top 1
}
addChange $top $n2 $tag2 $line1 $n1 $line2 $n2
nextHighlight $top
} elseif {$n2 < $n1} {
for {set t $n2} {$t < $n1} {incr t} {
emptyLine $top 2
}
addChange $top $n1 $tag1 $line1 $n1 $line2 $n2
nextHighlight $top
}
}
# Insert two blocks of lines in the compare windows.
proc insertMatchingBlocks {top block1 block2 line1 line2 details} {
global doingLine1 doingLine2
set n1 [llength $block1]
set n2 [llength $block2]
set large [expr {$n1 * $n2 > 5000}]
if {$n1 == 0 || $n2 == 0 || $::Pref(parse) < 2 || \
($large && $::Pref(parse) < 3)} {
# No extra parsing at all.
insertMatchingBlocksNoParse $top $block1 $block2 $line1 $line2 $details
return
}
# A large block may take time. Give a small warning.
if {$n1 * $n2 > 2000} {
set ::widgets($top,eqLabel) "!"
update idletasks
}
# Detect if only newlines has changed within the block, e.g.
# when rearranging newlines.
if {$::eskil(ignorenewline)} {
set res [ParseBlocksAcrossNewline $top $block1 $block2]
if {$res != 0} {
# FIXA: move this to ParseBlocksAcrossNewline ?
if {$res > 0 && $details} {
addChange $top $res change $line1 $n1 $line2 $n2
nextHighlight $top
} else {
addMapLines $top [expr {abs($res)}]
}
return
}
}
set apa [compareBlocks $block1 $block2]
# Fine grained changes means that each line is considered its own
# chunk. This is used for merging better to avoid the same decision
# for an entire block.
set finegrain [expr {$::Pref(finegrainchunks) && $details}]
if {$finegrain && $::eskil($top,ancestorFile) ne ""} {
# Avoid fine grain depending on relation to ancestor
set leftChange 0
set leftChangeOrAdd 0
for {set t $line1} {$t < $line1 + $n1} {incr t} {
if {[info exists ::eskil($top,ancestorLeft,$t)]} {
set leftChangeOrAdd 1
if {$::eskil($top,ancestorLeft,$t) eq "c"} {
set leftChange 1
break
}
}
}
set rightChange 0
set rightChangeOrAdd 0
for {set t $line2} {$t < $line2 + $n2} {incr t} {
if {[info exists ::eskil($top,ancestorRight,$t)]} {
set rightChangeOrAdd 1
if {$::eskil($top,ancestorRight,$t) eq "c"} {
set rightChange 1
break
}
}
}
# Avoid fine grain if either side has no changes against ancestor
if { ! $leftChangeOrAdd || !$rightChangeOrAdd} {
set finegrain 0
}
# Avoid fine grain if both sides have at most additions
if { ! $leftChange && !$rightChange} {
set finegrain 0
}
}
set t1 0
set t2 0
foreach c $apa {
if {$c eq "c"} {
set textline1 [lindex $block1 $t1]
set textline2 [lindex $block2 $t2]
insertMatchingLines $top $textline1 $textline2
if {$finegrain} {
addChange $top 1 change [expr {$line1 + $t1}] 1 \
[expr {$line2 + $t2}] 1
nextHighlight $top
}
incr t1
incr t2
} elseif {$c eq "C"} {
# This is two lines that the block matching considered
# too different to use line parsing on them.
# Marked the whole line as deleted/inserted
set textline1 [lindex $block1 $t1]
set textline2 [lindex $block2 $t2]
if {$::eskil($top,view) eq "table"} {
# Fall back to proc that handles table
insertMatchingLinesTable $top $textline1 $textline2
} else {
insertLine $top 1 $doingLine1 $textline1 new1 change
insertLine $top 2 $doingLine2 $textline2 new2 change
incr doingLine1
incr doingLine2
}
if {$finegrain} {
addChange $top 1 change [expr {$line1 + $t1}] 1 \
[expr {$line2 + $t2}] 1
nextHighlight $top
}
incr t1
incr t2
} elseif {$c eq "d"} {
set bepa [lindex $block1 $t1]
if {$::eskil($top,view) eq "table"} {
insertLineTable $top 1 $doingLine1 $bepa new1
} else {
$::widgets($top,wLine1) insert end [myFormL $doingLine1] \
"hl$::HighLightCount change"
$::widgets($top,wDiff1) insert end "$bepa\n" new1
emptyLine $top 2
}
incr doingLine1
if {$finegrain} {
addChange $top 1 new1 [expr {$line1 + $t1}] 1 \
[expr {$line2 + $t2}] 0
nextHighlight $top
}
incr t1
} elseif {$c eq "a"} {
set bepa [lindex $block2 $t2]
if {$::eskil($top,view) eq "table"} {
insertLineTable $top 2 $doingLine2 $bepa new2
} else {
$::widgets($top,wLine2) insert end [myFormL $doingLine2] \
"hl$::HighLightCount change"
$::widgets($top,wDiff2) insert end "$bepa\n" new2
emptyLine $top 1
}
incr doingLine2
if {$finegrain} {
addChange $top 1 new2 [expr {$line1 + $t1}] 0 \
[expr {$line2 + $t2}] 1
nextHighlight $top
}
incr t2
}
}
if { ! $finegrain} {
if {$details} {
addChange $top [llength $apa] change $line1 $n1 $line2 $n2
nextHighlight $top
} else {
addMapLines $top [llength $apa]
}
}
}
# Process one of the change/add/delete blocks reported by diff.
# ch1 is a file channel for the left file
# ch2 is a file channel for the right file
# n1/n2 is the number of lines involved
# line1/line2 says on what lines this block starts
# If n1/n2 are both 0, it means that this is the last lines to be displayed.
# In that case line1/line2, if non-zero says the last line to display.
proc doText {top ch1 ch2 n1 n2 line1 line2} {
global doingLine1 doingLine2
if {$n1 == 0 && $n2 == 0} {
# All blocks have been processed. Continue until end of file.
# If "show all" is not on, just display a couple of context lines.
set limit -1
if {$::Pref(context) >= 0} {
set limit $::Pref(context)
}
# Consider any total limit on displayed lines.
if {$::eskil($top,limitlines)} {
set limit [expr {$::eskil($top,limitlines) - $::eskil($top,mapMax)}]
if {$limit < 0} {
set limit 0
}
}
if {$limit >= 0} {disallowEdit $top}
# Unless we are in "only diffs", display remaining lines to the limit
if {$limit != 0} {
set t 0
while {[gets $ch2 apa] != -1} {
if {$line2 > 0 && $doingLine2 > $line2} break
insertLine $top 2 $doingLine2 $apa
incr doingLine2
addMapLines $top 1
incr t
if {$limit >= 0 && $t >= $limit} break
}
if {$::eskil($top,view) ne "table"} {
set t 0
while {[gets $ch1 apa] != -1} {
if {$line1 > 0 && $doingLine1 > $line1} break
insertLine $top 1 $doingLine1 $apa
incr doingLine1
incr t
if {$limit >= 0 && $t >= $limit} break
}
}
}
return
}
# Is this a change block, a delete block or an insert block?
if {$n1 == 0} {set tag2 new2} else {set tag2 change}
if {$n2 == 0} {set tag1 new1} else {set tag1 change}
# Display all equal lines before next diff, or skip if context is set.
# If context is on, only skip a section if the blank
# line replaces at least 3 lines.
set limit -1
if {$::Pref(context) == 0} {
set limit 0
} elseif {$::Pref(context) > 0 && \
($line1 - $doingLine1 > (2 * $::Pref(context) + 2))} {
set limit $::Pref(context)
}
if {$doingLine1 == 1} {
set allowStartFill 0
} else {
set allowStartFill 1
}
set t 0
while {$doingLine1 < $line1} {
gets $ch1 apa
gets $ch2 bepa
if {$limit < 0 || ($t < $limit && $allowStartFill) || \
($line1 - $doingLine1) <= $limit} {
if {$::eskil($top,view) ne "table"} {
insertLine $top 1 $doingLine1 $apa
insertLine $top 2 $doingLine2 $bepa
} else {
insertLineTable $top 1 $doingLine1 $apa
}
addMapLines $top 1
} elseif {$t == $limit && $allowStartFill} {
# If zero context is shown, skip the filler to keep display tight.
if {$limit > 0} {
emptyLine $top 1 0
emptyLine $top 2 0
addMapLines $top 1
}
}
incr doingLine1
incr doingLine2
incr t
if {$::eskil($top,limitlines) && \
($::eskil($top,mapMax) > $::eskil($top,limitlines))} {
return
}
}
# This should not happen unless something is wrong...
if {$doingLine2 != $line2} {
disallowEdit $top
$::widgets($top,wDiff1) insert end \
"**Bad alignment here!! $doingLine2 $line2**\n"
$::widgets($top,wDiff2) insert end \
"**Bad alignment here!! $doingLine2 $line2**\n"
$::widgets($top,wLine1) insert end "\n"
$::widgets($top,wLine2) insert end "\n"
}
# Process the block
if {$n1 == $n2 && ($n1 == 1 || $::Pref(parse) < 2)} {
# Never do block parsing for one line blocks.
# If block parsing is turned off, only do line parsing for
# blocks of equal size.
for {set t 0} {$t < $n1} {incr t} {
gets $ch1 textline1
gets $ch2 textline2
insertMatchingLines $top $textline1 $textline2
}
addChange $top $n1 change $line1 $n1 $line2 $n2
nextHighlight $top
} else {
# Collect blocks
set block1 {}
for {set t 0} {$t < $n1} {incr t} {
gets $ch1 apa
lappend block1 $apa
}
set block2 {}
for {set t 0} {$t < $n2} {incr t} {
gets $ch2 apa
lappend block2 $apa
}
insertMatchingBlocks $top $block1 $block2 $line1 $line2 1
}
# Empty return value
return
}
proc enableRedo {top} {
{*}$::widgets($top,configureRedoDiffCmd) -state normal
{*}$::widgets($top,configureMergeCmd) -state normal
}
proc disableRedo {top} {
{*}$::widgets($top,configureRedoDiffCmd) -state disabled
{*}$::widgets($top,configureMergeCmd) -state disabled
}
proc busyCursor {top} {
global oldcursor oldcursor2
if {$::eskil($top,view) eq "table"} {
set items wTable
} else {
set items {wLine1 wDiff1 wLine2 wDiff2}
}
if { ! [info exists oldcursor]} {
set oldcursor [$top cget -cursor]
set i1 [lindex $items 0]
set oldcursor2 [$::widgets($top,$i1) cget -cursor]
}
$top config -cursor watch
foreach item $items {
if {[info exists ::widgets($top,$item)]} {
set W $::widgets($top,$item)
$W config -cursor watch
}
}
}
proc normalCursor {top} {
global oldcursor oldcursor2
if {$::eskil($top,view) eq "table"} {
set items wTable
} else {
set items {wLine1 wDiff1 wLine2 wDiff2}
}
$top config -cursor $oldcursor
foreach item $items {
if {[info exists ::widgets($top,$item)]} {
set W $::widgets($top,$item)
$W config -cursor $oldcursor2
}
}
}
#####################################
# Special cases. Conflict/patch
#####################################
proc startConflictDiff {top file} {
set ::eskil($top,mode) "conflict"
set ::eskil($top,modetype) ""
set ::eskil($top,view) ""
set ::eskil($top,conflictFile) $file
set ::eskil($top,rightDir) [file dirname $file]
set ::eskil($top,rightOK) 1
set ::eskil($top,rightLabel) $file
set ::eskil($top,leftLabel) $file
set ::eskil($top,leftOK) 0
# Turn off ignore
set ::Pref(ignore) " "
set ::Pref(nocase) 0
set ::Pref(noempty) 0
# Try to autodetect line endings in file
detectLineEnd $top $file mergetranslation lf
}
# Read a conflict file and extract the two versions.
proc prepareConflict {top} {
disallowEdit $top
set ::eskil($top,leftFile) [tmpFile]
set ::eskil($top,rightFile) [tmpFile]
set ch1 [open $::eskil($top,leftFile) w]
set ch2 [open $::eskil($top,rightFile) w]
set ch [open $::eskil($top,conflictFile) r]
set ::eskil($top,conflictDiff) {}
set leftLine 1
set rightLine 1
set state both
set rightName ""
set leftName ""
while {[gets $ch line] != -1} {
if {[string match <<<<<<* $line]} {
set state right
regexp {<*\s*(.*)} $line -> rightName
set start2 $rightLine
} elseif {[string match ======* $line] && $state in "right ancestor"} {
if {$state eq "right"} {
set end2 [expr {$rightLine - 1}]
}
set state left
set start1 $leftLine
} elseif {[string match ||||||* $line] && $state eq "right"} {
set end2 [expr {$rightLine - 1}]
set state ancestor
} elseif {[string match >>>>>>* $line] && $state eq "left"} {
set state both
regexp {>*\s*(.*)} $line -> leftName
set end1 [expr {$leftLine - 1}]
lappend ::eskil($top,conflictDiff) [list \
$start1 [expr {$end1 - $start1 + 1}] \
$start2 [expr {$end2 - $start2 + 1}]]
} elseif {$state eq "both"} {
puts $ch1 $line
puts $ch2 $line
incr leftLine
incr rightLine
} elseif {$state eq "left"} {
puts $ch1 $line
incr leftLine
} elseif {$state eq "right"} {
puts $ch2 $line
incr rightLine
}
}
close $ch
close $ch1
close $ch2
if {$leftName eq "" && $rightName eq ""} {
set leftName "No Conflict: [file tail $::eskil($top,conflictFile)]"
set rightName $leftName
}
set ::eskil($top,leftLabel) $leftName
set ::eskil($top,rightLabel) $rightName
update idletasks
}
# Clean up after a conflict diff.
proc cleanupConflict {top} {
clearTmp $::eskil($top,rightFile) $::eskil($top,leftFile)
set ::eskil($top,rightFile) $::eskil($top,conflictFile)
set ::eskil($top,leftFile) $::eskil($top,conflictFile)
}
# Display one chunk from a patch file
proc displayOnePatch {top leftLines rightLines leftLine rightLine} {
mapNoChange $top 1
emptyLine $top 1
emptyLine $top 2
set leftlen [llength $leftLines]
set rightlen [llength $rightLines]
set leftc 0
set rightc 0
set lblock {}
set lblockl 0
set rblock {}
set rblockl 0
while {$leftc < $leftlen || $rightc < $rightlen} {
lassign [lindex $leftLines $leftc] lline lmode lstr
lassign [lindex $rightLines $rightc] rline rmode rstr
# Fix the case where one side's block is empty.
# That means that each line not marked should show up on both sides.
if {$leftc >= $leftlen} {
set lline $leftLine
incr leftLine
set lmode ""
set lstr $rstr
}
if {$rightc >= $rightlen} {
set rline $rightLine
incr rightLine
set rmode ""
set rstr $lstr
}
# Treat the combination "-" and "+" as a "!"
if {$lmode == "-" && $rmode == "+"} {
set lmode "!"
set rmode "!"
}
if {$lmode == "-" && [llength $lblock] > 0} {
set lmode "!"
}
if {$rmode == "+" && [llength $rblock] > 0} {
set rmode "!"
}
# If we are in a change block, gather up all lines
if {$lmode == "!" || $rmode == "!"} {
if {$lmode == "!"} {
if {[llength $lblock] == 0} {
set lblockl $lline
}
lappend lblock $lstr
incr leftc
}
if {$rmode == "!"} {
if {[llength $rblock] == 0} {
set rblockl $rline
}
lappend rblock $rstr
incr rightc
}
continue
}
# No change block anymore. If one just ended, display it.
if {[llength $lblock] > 0 || [llength $rblock] > 0} {
set ::doingLine1 $lblockl
set ::doingLine2 $rblockl
insertMatchingBlocks $top $lblock $rblock $lblockl $rblockl 0
set lblock {}
set rblock {}
}
if {$lmode == "" && $rmode == ""} {
insertLine $top 1 $lline $lstr
insertLine $top 2 $rline $rstr
incr leftc
incr rightc
addMapLines $top 1
continue
}
if {$lmode == "-"} {
insertLine $top 1 $lline $lstr new1
emptyLine $top 2
incr leftc
addMapLines $top 1
continue
}
if {$rmode == "+"} {
insertLine $top 2 $rline $rstr new2
emptyLine $top 1
incr rightc
addMapLines $top 1
continue
}
}
# If the patch ended with a change block, display it.
if {[llength $lblock] > 0 || [llength $rblock] > 0} {
set ::doingLine1 $lblockl
set ::doingLine2 $rblockl
insertMatchingBlocks $top $lblock $rblock $lblockl $rblockl 0
set lblock {}
set rblock {}
}
mapNoChange $top 0
}
# Read a patch file and display it
proc displayPatch {top} {
set ::eskil($top,leftLabel) "Patch $::eskil($top,patchFile): old"
set ::eskil($top,rightLabel) "Patch $::eskil($top,patchFile): new"
set ::eskil($top,patchFilelist) {}
update idletasks
if {$::eskil($top,patchFile) eq ""} {
if {$::eskil($top,patchData) eq ""} {
set data [getFullPatch $top]
} else {
set data $::eskil($top,patchData)
}
} elseif {$::eskil($top,patchFile) eq "-"} {
set data [read stdin]
} else {
set ch [open $::eskil($top,patchFile) r]
set data [read $ch]
close $ch
}
set style ""
set divider "-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-"
set leftLine 1
set rightLine 1
set leftLines {}
set rightLines {}
set state none
set fname ""
foreach line [split $data \n] {
# Detect a new file or file name
# "diff *" handles at least GIT and HG output
# "Index:" and "=====*" handles at least FOSSIL and SVN output
set newFile 0
set newName ""
if {[string match ======* $line]} {
set newFile 1
} elseif {[string match "diff *" $line]} {
set newFile 1
# Extract the last non-space. Works reasonably well.
regexp {\S+\s*$} $line newName
} elseif {[string match "Index: *" $line]} {
set newName [string range $line 7 end]
}
if {$newFile} {
if {$state != "none"} {
displayOnePatch $top $leftLines $rightLines $leftLine $rightLine
}
set leftLines {}
set rightLines {}
set state none
}
if {$newName ne ""} {
# If fname is set, a file that had no contents has passed.
# It could be a binary file or some other that the diffing source
# could not handle.
# Display the name to see that it is involved.
if {$fname ne ""} {
foreach side {1 2} {
emptyLine $top $side
insertLine $top $side "" $divider patch
insertLine $top $side "" $fname patch
insertLine $top $side "" $divider patch
}
addChange $top 4 change 0 0 0 0
}
set fname $newName
}
if {$newFile || $newName ne ""} {
continue
}
# Detect the first line in a -c style diff
if {[regexp {^\*\*\* } $line]} {
if {$state eq "right"} {
displayOnePatch $top $leftLines $rightLines $leftLine $rightLine
set leftLines {}
set rightLines {}
set state none
}
if {$state eq "none"} {
set state newfile
set style c
set leftRE {^\*\*\*\s+(.*)$}
set rightRE {^---\s+(.*)$}
}
}
# Detect the first line in a -u style diff
if {[regexp {^--- } $line] && $state eq "none"} {
if {$state eq "right" || $state eq "both"} {
displayOnePatch $top $leftLines $rightLines $leftLine $rightLine
set leftLines {}
set rightLines {}
set state none
}
if {$state eq "none"} {
set state newfile
set style u
set leftRE {^---\s+(.*)$}
set rightRE {^\+\+\+\s+(.*)$}
}
}
if {$state eq "newfile" && [regexp $leftRE $line -> sub]} {
set fname ""
emptyLine $top 1
insertLine $top 1 "" $divider patch
insertLine $top 1 "" $sub patch
insertLine $top 1 "" $divider patch
addChange $top 4 change 0 0 0 0
continue
}
if {$state eq "newfile" && [regexp $rightRE $line -> sub]} {
set fname ""
emptyLine $top 2
insertLine $top 2 "" $divider patch
insertLine $top 2 "" $sub patch
insertLine $top 2 "" $divider patch
continue
}
# A new section in a -u style diff
# Normally the chunk starts with @@
# From some tools the chunk starts with ##
if {[regexp {^(?:@@|\#\#)\s+-(\d+)(?:,\d+)?\s+\+(\d+)} $line ->\
sub1 sub2]} {
if {$state eq "both"} {
displayOnePatch $top $leftLines $rightLines \
$leftLine $rightLine
}
# Look for c function annotation in -u style
if {[regexp {^@@.*@@(.*)$} $line -> cfun]} {
set cfun [string trim $cfun]
if {$cfun ne ""} {
insertLine $top 1 "" $cfun patch
insertLine $top 2 "" $cfun patch
}
}
set state both
set leftLine $sub1
set rightLine $sub2
set leftLines {}
set rightLines {}
continue
}
# A new section in a -c style diff
if {[regexp {^\*\*\*\*\*} $line]} {
if {$state eq "right"} {
displayOnePatch $top $leftLines $rightLines $leftLine $rightLine
}
# Look for c function annotation in -c style
if {[regexp {^\*\*\*\*\*\S*\s+(.*)$} $line -> cfun]} {
set cfun [string trim $cfun]
if {$cfun ne ""} {
insertLine $top 1 "" $cfun patch
insertLine $top 2 "" $cfun patch
}
}
set leftLines {}
set rightLines {}
set state left
continue
}
# We are in the left part of a -c style diff
if {$state eq "left"} {
if {[regexp {^\*\*\*\s*(\d*)} $line -> sub]} {
if {$sub != ""} {
set leftLine $sub
}
continue
}
if {[regexp {^---\s*(\d*)} $line -> sub]} {
if {$sub != ""} {
set rightLine $sub
}
set state right
continue
}
if { ! [regexp {^[\s!+-]} $line]} continue
lappend leftLines [list $leftLine \
[string trim [string range $line 0 1]] \
[string range $line 2 end]]
incr leftLine
continue
}
# We are in the right part of a -c style diff
if {$state eq "right"} {
if { ! [regexp {^[\s!+-]} $line]} continue
lappend rightLines [list $rightLine \
[string trim [string range $line 0 1]] \
[string range $line 2 end]]
incr rightLine
continue
}
# We are in a -u style diff
if {$state eq "both"} {
if { ! [regexp {^[\s+-]} $line]} continue
set sig [string trim [string index $line 0]]
set str [string range $line 1 end]
if {$sig eq ""} {
lappend leftLines [list $leftLine "" $str]
lappend rightLines [list $rightLine "" $str]
incr leftLine
incr rightLine
} elseif {$sig eq "-"} {
lappend leftLines [list $leftLine "-" $str]
incr leftLine
} else {
lappend rightLines [list $rightLine "+" $str]
incr rightLine
}
continue
}
}
if {$state != "none"} {
displayOnePatch $top $leftLines $rightLines $leftLine $rightLine
}
if {$fname ne ""} {
foreach side {1 2} {
emptyLine $top $side
insertLine $top $side "" $divider patch
insertLine $top $side "" $fname patch
insertLine $top $side "" $divider patch
}
addChange $top 4 change 0 0 0 0
}
}
#####################################
# Main diff
#####################################
proc highlightTabs {top} {
foreach item {wDiff1 wDiff2} {
set W $::widgets($top,$item)
##nagelfar vartype W _obj,text
set count {}
set x [$W search -regexp -all -count count {\t+} 1.0]
foreach si $x l $count {
$W tag add tab $si "$si + $l chars"
}
$W tag configure tab -background bisque
$W tag raise tab
}
}
# Prepare for a diff by creating needed temporary files
proc prepareFiles {top} {
set ::eskil($top,cleanup) {}
if {$::eskil($top,mode) eq "rev"} {
prepareRev $top
lappend ::eskil($top,cleanup) "rev"
} elseif {$::eskil($top,mode) eq "conflict"} {
prepareConflict $top
lappend ::eskil($top,cleanup) "conflict"
}
# Try to autodetect line endings in files
detectLineEnd $top $::eskil($top,rightFile) righttranslation
detectLineEnd $top $::eskil($top,leftFile) lefttranslation
# Prepare Separator
set ::eskil($top,separator) \
[subst -nocommands -novariables $::eskil($top,separatorview)]
# Autodetect separator before any plugin processing
if {$::eskil($top,view) eq "table" && $::eskil($top,separator) eq ""} {
set ch1 [open $::eskil($top,leftFile)]
if {$::eskil($top,gz)} {
zlib push gunzip $ch1
}
gets $ch1 line1
close $ch1
# Any tab, comma or semicolon?
if {[regsub -all "\t" $line1 "\t" _] >= 2} {
set ::eskil($top,separator) "\t"
set ::eskil($top,separatorview) "\\t"
} elseif {[regsub -all "," $line1 "," _] >= 2} {
set ::eskil($top,separator) ","
set ::eskil($top,separatorview) ","
lappend ::eskil(argv) -sep ","
} elseif {[regsub -all ";" $line1 ";" _] >= 2} {
set ::eskil($top,separator) ";"
set ::eskil($top,separatorview) ";"
lappend ::eskil(argv) -sep ";"
}
}
# Make it look like it came from command line
# It could come from the GUI or auto-detect, put it in the command line
# to make it visible for plugins.
set i [lsearch -exact $::eskil(argv) "-sep"]
if {$i >= 0} {
incr i
lset ::eskil(argv) $i $::eskil($top,separatorview)
} else {
lappend ::eskil(argv) -sep $::eskil($top,separatorview)
}
# Prepare plugin, if any
if {[preparePlugin $top]} {
set ::eskil($top,cleanup) "plugin $::eskil($top,cleanup)"
}
}
# Clean up after a diff
proc cleanupFiles {top} {
foreach keyword $::eskil($top,cleanup) {
switch $keyword {
"rev" {cleanupRev $top}
"conflict" {cleanupConflict $top}
"plugin" {cleanupPlugin $top}
}
}
set ::eskil($top,cleanup) {}
}
# Redo Diff command
proc redoDiff {top} {
if {$::eskil($top,view) eq "table"} {
# TBD TABLE
doDiff $top
# Restore view
return
}
# Note what rows are being displayed
set W $::widgets($top,wDiff1)
set width [winfo width $W]
set height [winfo height $W]
set first [$W index @0,0]
set last [$W index @[- $width 4],[- $height 4]]
set first [lindex [split $first .] 0]
set last [lindex [split $last .] 0]
# Narrow it 5 lines since seeText will try to view 5 lines extra
incr first 5
incr last -5
if {$last < $first} {
set last $first
}
doDiff $top
# Restore view
foreach item {wLine1 wDiff1 wLine2 wDiff2} {
set W $::widgets($top,$item)
seeText $W $first.0 $last.0
}
}
# Make an appropriate tail for a window title, depending on mode and files.
proc TitleTail {top} {
set tail1 [file tail $::eskil($top,rightLabel)]
set tail2 [file tail $::eskil($top,leftLabel)]
if {$::eskil($top,mode) ne "" || $tail1 eq $tail2} {
if {$::eskil($top,mode) eq "rev"} {
set tail1 [file tail $::eskil($top,RevFile)]
} elseif {$::eskil($top,mode) eq "conflict"} {
set tail1 [file tail $::eskil($top,conflictFile)]
}
return $tail1
} else {
return "$tail2 vs $tail1"
}
}
# Main diff function.
proc doDiff {top} {
global doingLine1 doingLine2
if {$::eskil($top,mode) eq "" && ($::eskil($top,leftOK) == 0 || $::eskil($top,rightOK) == 0)} {
disableRedo $top
return
} else {
enableRedo $top
}
busyCursor $top
resetEdit $top
# Clear up everything before starting processing
if {$::eskil($top,view) eq "table"} {
set W $::widgets($top,wTable)
# TBD TABLE
$W configure -state normal
$W delete 0 end
set ::eskil($top,tablechanges) {}
} else {
foreach item {wLine1 wDiff1 wLine2 wDiff2 wTb} {
set W $::widgets($top,$item)
$W configure -state normal
$W delete 1.0 end
}
}
clearMap $top
set ::HighLightCount 0
highLightChange $top -1
# Display a star during diff execution, to know when the internal
# processing starts, and when the label is "valid".
set ::widgets($top,eqLabel) "*"
wm title $top "Eskil:"
update idletasks
if {$::eskil($top,mode) eq "patch"} {
disallowEdit $top
displayPatch $top
drawMap $top -1
#drawEditButtons $top
foreach item {wLine1 wLine2} {
set W $::widgets($top,$item)
$W configure -state disabled
}
update idletasks
wm title $top "Eskil: [file tail $::eskil($top,patchFile)]"
# TBD TABLE
$::widgets($top,wLine2) see 1.0
if {$::eskil($top,printFileCmd) && $::eskil($top,printFile) ne ""} {
after idle "doPrint $top 1 ; cleanupAndExit all"
}
normalCursor $top
return
} else {
prepareFiles $top
}
wm title $top "Eskil: [TitleTail $top]"
# Run diff and parse the result.
set opts $::Pref(ignore)
if {$::Pref(nocase)} {lappend opts -nocase}
if {$::Pref(noempty)} {lappend opts -noempty}
if {$::Pref(pivot) > 0} {lappend opts -pivot $::Pref(pivot)}
if {$::eskil($top,gz)} {lappend opts -gz}
if {[info exists ::eskil($top,aligns)] && \
[llength $::eskil($top,aligns)] > 0} {
lappend opts -align $::eskil($top,aligns)
}
set range {}
if {[info exists ::eskil($top,range)] && \
[llength $::eskil($top,range)] == 4} {
set range $::eskil($top,range)
lappend opts -range $range
}
foreach {RE sub side} [getActivePreprocess $top] {
lappend opts -regsub$side [list $RE $sub]
}
# Apply nodigit after preprocess
if {$::Pref(nodigit)} {lappend opts -nodigit}
# If a special file for diffing is present, use it.
if {[info exists ::eskil($top,leftFileDiff)]} {
set dFile1 $::eskil($top,leftFileDiff)
} else {
set dFile1 $::eskil($top,leftFile)
}
if {[info exists ::eskil($top,rightFileDiff)]} {
set dFile2 $::eskil($top,rightFileDiff)
} else {
set dFile2 $::eskil($top,rightFile)
}
set differr [catch {DiffUtil::diffFiles {*}$opts \
$dFile1 $dFile2} diffres]
# In conflict mode we can use the diff information collected when
# parsing the conflict file. This makes sure the blocks in the conflict
# file become change-blocks during merge.
if {$::eskil($top,mode) eq "conflict" && $::eskil($top,modetype) eq "Pure"} {
set diffres $::eskil($top,conflictDiff)
}
if {$differr != 0} {
if {$::eskil($top,view) eq "table"} {
# TBD TABLE
} else {
$::widgets($top,wDiff1) insert end $diffres
}
normalCursor $top
return
}
if {[llength $diffres] == 0} {
set ::widgets($top,eqLabel) "="
# Automatically close if equal
if {$::eskil(autoclose)} {
after idle cleanupAndExit $top
return
}
} else {
set ::widgets($top,eqLabel) " "
}
# Update the equal label immediately for better feedback
update idletasks
if {$::eskil($top,ancestorFile) ne ""} {
collectAncestorInfo $top $dFile1 $dFile2 $opts
}
set firstview 1
set ch1 [open $::eskil($top,leftFile)]
set ch2 [open $::eskil($top,rightFile)]
if {$::eskil($top,gz)} {
disallowEdit $top
zlib push gunzip $ch1
zlib push gunzip $ch2
}
set doingLine1 1
set doingLine2 1
if {$::eskil($top,view) eq "table"} {
# Look for table header line
set i [lindex $diffres 0]
lassign $i line1 n1 line2 n2
if {$line1 == 1 || $line2 == 1} {
# Hide header line of widget TBD TABLE
#$::widgets($top,wTable) configure
# Set up columns??
$::widgets($top,wTable) configure \
-columns "0 Table 0 without 0 header 0 not 0 implemented 0 yet"
} else {
# First lines are equal, treat them as header
# Consume table header line
gets $ch1 line1
incr doingLine1
gets $ch2 line
incr doingLine2
set headings [split $line1 $::eskil($top,separator)]
set columns {}
foreach heading $headings {
lappend columns 0 $heading
}
$::widgets($top,wTable) configure -columns $columns
if {$::eskil($top,maxwidth) > 0} {
set col -1
foreach {_ _} $columns {
incr col
$::widgets($top,wTable) columnconfigure $col \
-maxwidth $::eskil($top,maxwidth)
}
}
}
}
# If there is a range, skip lines up to the range
if {[llength $range] != 0} {
disallowEdit $top
lassign $range start1 end1 start2 end2
while {$doingLine1 < $start1 && [gets $ch1 line] >= 0} {
incr doingLine1
}
while {$doingLine2 < $start2 && [gets $ch2 line] >= 0} {
incr doingLine2
}
}
set t 0
foreach i $diffres {
lassign $i line1 n1 line2 n2
doText $top $ch1 $ch2 $n1 $n2 $line1 $line2
if {$::eskil($top,limitlines) && \
($::eskil($top,mapMax) > $::eskil($top,limitlines))} {
break
}
# Get one update when the screen has been filled.
# Show the first diff.
if {$firstview && $::eskil($top,mapMax) > 100} {
set firstview 0
showDiff $top 0
update idletasks
}
}
# If there is a range, just display the range
if {[llength $range] != 0} {
lassign $range start1 end1 start2 end2
} else {
set end1 0
set end2 0
}
doText $top $ch1 $ch2 0 0 $end1 $end2
if {$::eskil($top,view) ne "table"} {
# Make sure all text widgets have the same number of lines.
# The common y scroll doesn't work well if not.
set max 0.0
foreach item {wLine1 wDiff1 wLine2 wDiff2} {
set W $::widgets($top,$item)
if {[$W index end] > $max} {
set max [$W index end]
}
}
foreach item {wLine1 wDiff1 wLine2 wDiff2} {
set W $::widgets($top,$item)
set d [expr {int($max) - int([$W index end])}]
for {set t 0} {$t < $d} {incr t} {
$W insert end \n padding
}
}
}
close $ch1
close $ch2
# We can turn off editing in the text windows after everything
# is displayed.
noEdit $top
# Mark aligned lines TBD TABLE
if {[info exists ::eskil($top,aligns)] && \
[llength $::eskil($top,aligns)] > 0} {
foreach {align1 align2} $::eskil($top,aligns) {
set i [$::widgets($top,wLine1) search -regexp "\\m$align1\\M" 1.0]
if {$i != ""} {
$::widgets($top,wLine1) tag add align \
"$i linestart" "$i lineend"
}
set i [$::widgets($top,wLine2) search -regexp "\\m$align2\\M" 1.0]
if {$i != ""} {
$::widgets($top,wLine2) tag add align \
"$i linestart" "$i lineend"
}
}
}
drawMap $top -1
#drawEditButtons $top
if {$::eskil($top,view) ne "table"} {
foreach item {wLine1 wLine2 wTb} {
set W $::widgets($top,$item)
$W configure -state disabled
}
update idletasks
$::widgets($top,wLine2) see 1.0
}
normalCursor $top
showDiff $top 0
if {$::widgets($top,eqLabel) eq "!"} {
set ::widgets($top,eqLabel) " "
}
cleanupFiles $top
if {$::eskil($top,mode) eq "conflict"} {
if {$::widgets($top,eqLabel) != "="} {
makeMergeWin $top
}
} elseif {$::eskil($top,ancestorFile) ne ""} {
if {$::widgets($top,eqLabel) != "="} {
makeMergeWin $top
}
}
if {$::eskil($top,printFileCmd) && $::eskil($top,printFile) ne ""} {
# TBD TABLE
after idle "doPrint $top 1 ; cleanupAndExit all"
}
}
# This is the entrypoint to do a diff via DDE or Send
proc remoteDiff {file1 file2} {
newDiff $file1 $file2
}
#####################################
# Highlight and navigation stuff
#####################################
# Scroll windows to next/previous diff
proc findDiff {top delta} {
showDiff $top [expr {$::eskil($top,currHighLight) + $delta}]
}
# Scroll a text window to view a certain range, and possibly some
# lines before and after.
proc seeText {W si ei} {
$W see $ei
$W see $si
$W see $si-5lines
$W see $ei+5lines
if {[llength [$W bbox $si]] == 0} {
$W yview $si-5lines
}
if {[llength [$W bbox $ei]] == 0} {
$W yview $si
}
}
# Highlight a diff
proc highLightChange {top changeIndex} {
if {[info exists ::eskil($top,currHighLight)] && \
$::eskil($top,currHighLight) >= 0} {
$::widgets($top,wLine1) tag configure hl$::eskil($top,currHighLight) \
-background {}
$::widgets($top,wLine2) tag configure hl$::eskil($top,currHighLight) \
-background {}
}
set ::eskil($top,currHighLight) $changeIndex
if {$::eskil($top,currHighLight) < 0} {
set ::eskil($top,currHighLight) -1
} elseif {$::eskil($top,currHighLight) >= [llength $::eskil($top,changes)]} {
set ::eskil($top,currHighLight) [llength $::eskil($top,changes)]
} else {
$::widgets($top,wLine1) tag configure hl$::eskil($top,currHighLight) \
-background yellow
$::widgets($top,wLine2) tag configure hl$::eskil($top,currHighLight) \
-background yellow
}
}
# Highlight a diff and scroll windows to it.
proc showDiff {top changeIndex} {
# TBD TABLE
if {$::eskil($top,view) eq "table"} return
highLightChange $top $changeIndex
set change [lindex $::eskil($top,changes) $::eskil($top,currHighLight)]
set line1 [lindex $change 0]
if {$::eskil($top,currHighLight) < 0} {
set line1 1.0
set line2 1.0
} elseif {$line1 eq ""} {
set line1 end
set line2 end
} else {
set line2 [expr {$line1 + [lindex $change 1]}]
incr line1
set line1 $line1.0
set line2 $line2.0
}
foreach item {wLine1 wDiff1 wLine2 wDiff2} {
set W $::widgets($top,$item)
seeText $W $line1 $line2
}
}
#####################################
# Editing
#####################################
# FIXA: Use snit to adapt text widget instead of using wcb
# include seeText in such a snidget.
# Try to autodetect line endings in file
proc detectLineEnd {top file field {def {}}} {
set ch [open $file rb]
set data [read $ch 1000]
close $ch
if {[string first \r\n $data] >= 0} {
set ::eskil($top,$field) crlf
} elseif {[string first \n $data] >= 0} {
set ::eskil($top,$field) lf
} else {
set ::eskil($top,$field) $def
}
}
# Clear Editing state
proc resetEdit {top} {
set ::eskil($top,leftEdit) 0
set ::eskil($top,rightEdit) 0
{*}$::widgets($top,configureEditModeCmd) -state normal
if {$::eskil($top,view) eq "table"} {
return
}
resetEditW $::widgets($top,wDiff1)
resetEditW $::widgets($top,wDiff2)
}
# Clear Editing state for a Text widget
proc resetEditW {W} {
$W tag configure padding -background {}
$W edit reset
$W configure -undo 0
set ::eskil($W,allowChange) all
wcb::callback $W before insert {}
wcb::callback $W before delete {}
}
# Do not allow any editing
proc noEdit {top} {
if {$::eskil($top,view) eq "table"} {
return
}
noEditW $::widgets($top,wDiff1)
noEditW $::widgets($top,wDiff2)
}
# Do not allow any editing in a Text widget
proc noEditW {W} {
set ::eskil($W,allowChange) none
wcb::callback $W before insert [list TextInterceptInsert $W]
wcb::callback $W before delete [list TextInterceptDelete $W]
}
proc TextInterceptInsert {W oW index str args} {
if {$::eskil($W,allowChange) eq "none"} {
wcb::cancel
return
}
if {$::eskil($W,allowChange) eq "all"} return
#wcb::cancel - Cancel a widget command
#wcb::replace - Replace arguments of a widget command with new ones
# Disallow all new lines
if {[string first "\n" $str] >= 0} {
wcb::cancel
return
}
foreach {tag str2} $args {
if {[string first "\n" $str2] >= 0} {
wcb::cancel
return
}
}
}
proc TextInterceptDelete {W oW from {to {}}} {
if {$::eskil($W,allowChange) eq "none"} {
wcb::cancel
return
}
if {$::eskil($W,allowChange) eq "all"} return
if {$to eq ""} {
set to $from+1char
}
set text [$oW get $from $to]
# Disallow all new lines
if {[string first "\n" $text] >= 0} {
wcb::cancel
return
}
}
# Turn on editing for a Text widget
proc turnOnEdit {W} {
$W tag configure padding -background \#f0f0f0
$W configure -undo 1
set ::eskil($W,allowChange) line
}
# Turn on editing on sides where it has not been disallowed
proc allowEdit {top} {
{*}$::widgets($top,configureEditModeCmd) -state disable
if {$::eskil($top,leftEdit) == 0} {
set ::eskil($top,leftEdit) 1
turnOnEdit $::widgets($top,wDiff1)
}
if {$::eskil($top,rightEdit) == 0} {
set ::eskil($top,rightEdit) 1
turnOnEdit $::widgets($top,wDiff2)
}
}
# Turn off editing on sides that do not correspond to a file
proc disallowEdit {top {side 0}} {
if {$side == 0 || $side == 1} {
set ::eskil($top,leftEdit) -1
}
if {$side == 0 || $side == 2} {
set ::eskil($top,rightEdit) -1
}
if {$::eskil($top,leftEdit) == -1 && $::eskil($top,rightEdit) == -1} {
{*}$::widgets($top,configureEditModeCmd) -state disabled
}
}
# Ask if editing is allowed on a side
proc mayEdit {top side} {
if {$side == 1} {
return [expr {$::eskil($top,leftEdit) == 1}]
} else {
return [expr {$::eskil($top,rightEdit) == 1}]
}
}
# Start an undo block in a bunch of text widgets
proc startUndoBlock {args} {
foreach W $args {
$W configure -autoseparators 0
# Open up editing for copy functions
set ::eskil($W,allowChange) all
}
}
# End an undo block in a bunch of text widgets
proc endUndoBlock {args} {
foreach W $args {
$W configure -autoseparators 1
$W edit separator
set ::eskil($W,allowChange) line
}
}
# Copy a block
proc copyBlock {top from first last} {
set to [expr {$from == 1 ? 2 : 1}]
set wfrom $::widgets($top,wDiff$from)
set wto $::widgets($top,wDiff$to)
set tags ""
set dump [$wfrom dump -all $first.0 $last.end+1c]
startUndoBlock $wfrom $wto
$wfrom delete $first.0 $last.end+1c
$wto delete $first.0 $last.end+1c
foreach {key value index} $dump {
switch -- $key {
text {
$wfrom insert $index $value $tags
$wto insert $index $value $tags
}
tagon {
if {$value eq "padding"} {
set tags "padding"
}
}
tagoff {
if {$value eq "padding"} {
set tags 0
}
}
}
}
endUndoBlock $wfrom $wto
}
# Copy a row between text widgets
proc copyRow {top from row} {
set to [expr {$from == 1 ? 2 : 1}]
set wfrom $::widgets($top,wDiff$from)
set wto $::widgets($top,wDiff$to)
set text [$wfrom get $row.0 $row.end+1c]
startUndoBlock $wfrom $wto
$wto delete $row.0 $row.end+1c
$wto insert $row.0 $text ""
# Rewrite the source row to remove any tags
$wfrom delete $row.0 $row.end+1c
$wfrom insert $row.0 $text ""
endUndoBlock $wfrom $wto
}
# Delete a row filling it with padding
proc deleteBlock {top side from {to {}}} {
set W $::widgets($top,wDiff$side)
if {$to eq ""} {set to $from}
startUndoBlock $W
$W delete $from.0 $to.end+1c
$W insert $from.0 [string repeat \n [expr {$to - $from + 1}]] padding
endUndoBlock $W
}
# Get the lines involved in the display
proc getLinesFromRange {W range} {
set from [lindex $range 0]
set to [lindex $range 1]
lassign [split $from "."] fromr fromi
lassign [split $to "."] tor toi
if {$toi == 0} {incr tor -1}
# Get the corresponding lines in the file
set t [$W get $fromr.0 $tor.end]
set lines [lsort -integer [regexp -all -inline {\d+} $t]]
set froml [lindex $lines 0]
set tol [lindex $lines end]
return [list $fromr $tor $froml $tol]
}
# Called by popup menus over row numbers to add commands for editing.
# Returns 1 if nothing was added.
proc editMenu {mW top side changeIndex x y} {
if { ! [mayEdit $top $side]} {return 1}
set other [expr {$side == 1 ? 2 : 1}]
set editOther [mayEdit $top $other]
set dW $::widgets($top,wDiff$side)
set lW $::widgets($top,wLine$side)
set oW $::widgets($top,wLine$other)
set changed 1
if {$changeIndex eq "_"} {
# The popup is on unchanged line numbers
set changed 0
# Get the row that was clicked
set index [$lW index @$x,$y]
set row [lindex [split $index "."] 0]
# Range is that row
set range [list $row.0 $row.end]
set rangeo [list $row.0 $row.end]
} elseif {$changeIndex eq ""} {
# The popup is on selected text.
# Get the row that was clicked
set index [$dW index @$x,$y]
set row [lindex [split $index "."] 0]
# Figure out the rows involved in the selection.
set range [$dW tag ranges sel]
set from [lindex $range 0]
set to [lindex $range 1]
lassign [split $from "."] froml fromi
lassign [split $to "."] tol toi
if {$toi == 0} {incr tol -1}
set range [list $froml.0 $tol.end]
set rangeo [list $froml.0 $tol.end]
} else {
# The popup is on a change block in line numbers
# Get the row that was clicked
set index [$lW index @$x,$y]
set row [lindex [split $index "."] 0]
# Get ranges for the change block
set range [$lW tag ranges hl$changeIndex]
set rangeo [$oW tag ranges hl$changeIndex]
}
set line [regexp -inline {\d+} [$lW get $row.0 $row.end]]
set lineo [regexp -inline {\d+} [$oW get $row.0 $row.end]]
# Row copy
if {$lineo ne ""} {
$mW add command -label "Copy Row from other side" \
-command [list copyRow $top $other $row]
}
$mW add command -label "Delete Row" \
-command [list deleteBlock $top $side $row]
if {$line ne "" && $editOther && $changed} {
$mW add command -label "Copy Row to other side" \
-command [list copyRow $top $side $row]
}
if {$changed} {
# Get the lines involved in the block
lassign [getLinesFromRange $lW $range ] from to froml tol
lassign [getLinesFromRange $oW $rangeo] fromo too fromlo tolo
# More than one line in the block?
set thisSize 0
set otherSize 0
if {$froml ne "" && $tol ne ""} {
set thisSize [expr {$tol - $froml + 1}]
}
if {$fromlo ne "" && $tolo ne ""} {
set otherSize [expr {$tolo - $fromlo + 1}]
}
if {$thisSize > 1 || $otherSize > 1} {
if {$otherSize > 0} {
$mW add command -label "Copy Block from other side" \
-command [list copyBlock $top $other $fromo $too]
} else {
$mW add command -label "Delete Block" \
-command [list deleteBlock $top $side $from $to]
}
if {$editOther && $thisSize > 0} {
$mW add command -label "Copy Block to other side" \
-command [list copyBlock $top $side $from $to]
}
}
}
$mW add command -label "Save File" -command [list saveFile $top $side]
$mW add command -label "Save File, Reload" -command [list saveFileR $top $side]
return 0
}
proc saveFile {top side} {
if {$side == 1} {
if { ! $::eskil($top,leftEdit)} return
set fileName $::eskil($top,leftFile)
set trans $::eskil($top,lefttranslation)
} else {
if { ! $::eskil($top,rightEdit)} return
set fileName $::eskil($top,rightFile)
set trans $::eskil($top,righttranslation)
}
set W $::widgets($top,wDiff$side)
# Confirm dialog
set apa no
if {$::Pref(askOverwrite)} {
set apa [tk_messageBox -parent $top -icon question \
-title "Overwrite file" -type yesnocancel -message \
"Overwriting file [file tail $fileName]\nDo you want to\
create a backup copy ?"]
}
if {$apa eq "yes"} {
set backup [file rootname $fileName].bak
if {[catch {file copy -force $fileName $backup} result]} {
tk_messageBox -parent $top -icon error \
-title "File error" -type ok -message \
"Error creating backup file $backup:\n$result"
return
}
} elseif {$apa ne "no"} {
return
}
set ch [open $fileName "w"]
if {$trans ne ""} {
fconfigure $ch -translation $trans
}
set save 1
foreach {key value index} [$W dump -all 1.0 end-1c] {
switch -- $key {
text {
if {$save} {
puts -nonewline $ch $value
}
}
tagon {
if {$value eq "padding"} {
set save 0
}
}
tagoff {
if {$value eq "padding"} {
set save 1
}
}
}
}
close $ch
}
# Save file and reload
proc saveFileR {top side} {
saveFile $top $side
# Redo
redoDiff $top
allowEdit $top
}
#####################################
# File dialog stuff
#####################################
# Check if a filename is a directory and handle starkits
proc FileIsDirectory {file {kitcheck 0}} {
# Skip directories
if {[file isdirectory $file]} {return 1}
# This detects .kit but how to detect starpacks?
if {[file extension $file] eq ".kit" || $kitcheck} {
if { ! [catch {package require vfs::mk4}]} {
if { ! [catch {vfs::mk4::Mount $file $file -readonly}]} {
# Check for contents to ensure it is a kit
if {[llength [glob -nocomplain $file/*]] == 0} {
vfs::unmount $file
}
}
# Now it is possible that the isdirectory status has changed
return [file isdirectory $file]
}
}
return 0
}
# A wrapper for tk_getOpenFile
proc myOpenFile {args} {
array set opts $args
set isVfs 0
if {[info exists opts(-initialdir)]} {
if {[string match tclvfs* [file system $opts(-initialdir)]]} {
set isVfs 1
}
}
# When in a vfs, make sure the Tcl file dialog is used
# to be able to access the files in a starkit.
if {$isVfs} {
# Only do this if tk_getOpenFile is not a proc.
if {[info procs tk_getOpenFile] eq ""} {
# If there is any problem, call the real one
if { ! [catch {set res [::tk::dialog::file:: open {*}$args]}]} {
return $res
}
}
}
return [tk_getOpenFile {*}$args]
}
proc doOpenLeft {top {forget 0}} {
if { ! $forget && [info exists ::eskil($top,leftDir)]} {
set initDir $::eskil($top,leftDir)
} elseif {[info exists ::eskil($top,rightDir)]} {
set initDir $::eskil($top,rightDir)
} else {
set initDir [pwd]
}
set apa [myOpenFile -title "Select left file" -initialdir $initDir \
-parent $top]
if {$apa != ""} {
set ::eskil($top,leftDir) [file dirname $apa]
set ::eskil($top,leftFile) $apa
set ::eskil($top,leftLabel) $apa
set ::eskil($top,leftOK) 1
return 1
}
return 0
}
proc doOpenRight {top {forget 0}} {
if { ! $forget && [info exists ::eskil($top,rightDir)]} {
set initDir $::eskil($top,rightDir)
} elseif {[info exists ::eskil($top,leftDir)]} {
set initDir $::eskil($top,leftDir)
} else {
set initDir [pwd]
}
set apa [myOpenFile -title "Select right file" -initialdir $initDir \
-parent $top]
if {$apa != ""} {
set ::eskil($top,rightDir) [file dirname $apa]
set ::eskil($top,rightFile) $apa
set ::eskil($top,rightLabel) $apa
set ::eskil($top,rightOK) 1
return 1
}
return 0
}
proc doOpenAncestor {top} {
if {$::eskil($top,ancestorFile) ne ""} {
set initDir [file dirname $::eskil($top,ancestorFile)]
} elseif {[info exists ::eskil($top,leftDir)]} {
set initDir $::eskil($top,leftDir)
} elseif {[info exists ::eskil($top,rightDir)]} {
set initDir $::eskil($top,rightDir)
} else {
set initDir [pwd]
}
set apa [myOpenFile -title "Select ancestor file" -initialdir $initDir \
-parent $top]
if {$apa != ""} {
set ::eskil($top,ancestorFile) $apa
return 1
}
return 0
}
proc openLeft {top} {
if {[doOpenLeft $top]} {
set ::eskil($top,mode) ""
set ::eskil($top,mergeFile) ""
doDiff $top
}
}
proc openRight {top} {
if {[doOpenRight $top]} {
set ::eskil($top,mode) ""
set ::eskil($top,mergeFile) ""
doDiff $top
}
}
proc openAncestor {top} {
if {[doOpenAncestor $top]} {
# Redo diff with ancestor
doDiff $top
}
}
proc openConflict {top} {
if {[doOpenRight $top]} {
startConflictDiff $top $::eskil($top,rightFile)
set ::eskil($top,mergeFile) ""
doDiff $top
}
}
proc openPatch {top} {
if {[doOpenLeft $top]} {
set ::eskil($top,mode) "patch"
set ::Pref(ignore) " "
set ::Pref(nocase) 0
set ::Pref(noempty) 0
set ::eskil($top,patchFile) $::eskil($top,leftFile)
set ::eskil($top,patchData) ""
doDiff $top
}
}
# Get data from clipboard and display as a patch.
proc doPastePatch {top} {
if {[catch {::tk::GetSelection $top CLIPBOARD} sel]} {
tk_messageBox -parent $top -icon error \
-title "Eskil Error" -type ok \
-message "Could not retreive clipboard"
return
}
set ::eskil($top,mode) "patch"
set ::Pref(ignore) " "
set ::Pref(nocase) 0
set ::Pref(noempty) 0
set ::eskil($top,patchFile) ""
set ::eskil($top,patchData) $sel
doDiff $top
}
proc openRev {top} {
if {[doOpenRight $top]} {
set rev [detectRevSystem $::eskil($top,rightFile)]
if {$rev eq ""} {
tk_messageBox -parent $top -icon error \
-title "Eskil Error" -type ok -message \
"Could not figure out which revison control system\
\"$::eskil($top,rightFile)\" is under."
return
}
startRevMode $top $rev $::eskil($top,rightFile)
set ::eskil($top,mergeFile) ""
doDiff $top
}
}
proc openBoth {top forget} {
if {[doOpenLeft $top]} {
if {[doOpenRight $top $forget]} {
set ::eskil($top,mode) ""
set ::eskil($top,mergeFile) ""
doDiff $top
}
}
}
# File drop using TkDnd
proc fileDrop {top side files} {
# FIXA: Maybe single drop during rev mode should stay in rev mode?
# Dropping two files mean set both
if {[llength $files] >= 2} {
set leftFile [lindex $files 0]
set rightFile [lindex $files 1]
} else {
if {$side eq "any"} {
# Dropped outside the text widgets. Try to be clever.
if { ! [info exists ::eskil($top,lastDrop)]} {
set side left
} elseif {$::eskil($top,lastDrop) eq "left"} {
set side right
} else {
set side left
}
}
if {$side eq "left"} {
set leftFile [lindex $files 0]
set rightFile ""
} else {
set leftFile ""
set rightFile [lindex $files 0]
}
set ::eskil($top,lastDrop) $side
}
if {$leftFile ne ""} {
set ::eskil($top,leftDir) [file dirname $leftFile]
set ::eskil($top,leftFile) $leftFile
set ::eskil($top,leftLabel) $leftFile
set ::eskil($top,leftOK) 1
set ::eskil($top,mode) ""
set ::eskil($top,mergeFile) ""
}
if {$rightFile ne ""} {
set ::eskil($top,rightDir) [file dirname $rightFile]
set ::eskil($top,rightFile) $rightFile
set ::eskil($top,rightLabel) $rightFile
set ::eskil($top,rightOK) 1
set ::eskil($top,mode) ""
set ::eskil($top,mergeFile) ""
}
if {$::eskil($top,leftOK) && $::eskil($top,rightOK)} {
doDiff $top
}
}
#####################################
# GUI stuff
#####################################
# A little helper to make a window with scrollbars
# It returns the name of the scrolled window
proc Scroll {dir class W args} {
switch -- $dir {
both {
set scrollx 1
set scrolly 1
}
x {
set scrollx 1
set scrolly 0
}
y {
set scrollx 0
set scrolly 1
}
default {
return -code error "Bad scrolldirection \"$dir\""
}
}
ttk::frame $W
$class $W.s {*}$args
# Move border properties to frame
set bw [$W.s cget -borderwidth]
set relief [$W.s cget -relief]
$W configure -relief $relief -borderwidth $bw
$W.s configure -borderwidth 0
grid $W.s -sticky news
if {$scrollx} {
$W.s configure -xscrollcommand [list $W.sbx set]
ttk::scrollbar $W.sbx -orient horizontal -command [list $W.s xview]
grid $W.sbx -row 1 -sticky we
}
if {$scrolly} {
$W.s configure -yscrollcommand [list $W.sby set]
ttk::scrollbar $W.sby -orient vertical -command [list $W.s yview]
grid $W.sby -row 0 -column 1 -sticky ns
}
grid columnconfigure $W 0 -weight 1
grid rowconfigure $W 0 -weight 1
return $W.s
}
# Rearrange a dynamic grid to a specified number of columns
proc DynGridRearrange {W cols} {
# Go down columns first. Thus we must know how many rows there will be.
set children [grid slaves $W._dyn]
set rows [expr {([llength $children] + $cols - 1) / $cols}]
set row 0
set col 0
foreach child $children {
grid $child -row $row -column $col
grid columnconfigure $W._dyn $col -uniform a
incr row
if {$row >= $rows} {
incr col
set row 0
}
}
# Clear other columns from uniform in case we shrunk
if {$row != 0} {
incr col
}
for {} {$col < 15} {incr col} {
grid columnconfigure $W._dyn $col -uniform ""
}
# Recalculate
update idletasks
# Propagate Height
set height [winfo reqheight $W._dyn]
$W configure -width 100 -height $height
}
# Update dynamic grid on configure event
proc DynGridRedo {W} {
set maxW 0
set children [grid slaves $W._dyn]
foreach child $children {
set maxW [expr {max($maxW,[winfo reqwidth $child])}]
}
set fW [winfo width $W]
set cols [expr {max(1,$fW / $maxW)}]
# Rerrange if needed
lassign [grid size $W._dyn] mCols mRows
if {$mCols != $cols} {
DynGridRearrange $W $cols
}
}
# Ask for widget to have its children managed by dynGrid.
proc dynGridManage {W} {
# Limit its inital requirements
pack propagate $W 0
$W configure -width 100 -height 10
set children [winfo children $W]
# Add an inner frame
ttk::frame $W._dyn
lower $W._dyn
pack $W._dyn -fill both -expand 1
# Get all children managed
grid {*}$children -in $W._dyn -padx 3 -pady 3 -sticky w
# React
bind $W <Configure> "DynGridRedo $W"
}
################
# Align function
################
proc enableAlign {top} {
{*}$::widgets($top,configureAlignCmd) -state normal
}
proc disableAlign {top} {
{*}$::widgets($top,configureAlignCmd) -state disabled
}
# Remove one or all alignment pairs
proc clearAlign {top {leftline {}}} {
if {$leftline == ""} {
set ::eskil($top,aligns) {}
} else {
set i 0
while 1 {
set i [lsearch -integer -start $i $::eskil($top,aligns) $leftline]
if {$i < 0} break
if {($i % 2) == 0} {
set ::eskil($top,aligns) [lreplace $::eskil($top,aligns) \
$i [+ $i 1]]
break
}
incr i
}
}
if {[llength $::eskil($top,aligns)] == 0} {
disableAlign $top
}
}
proc NoMarkAlign {top} {
unset -nocomplain ::eskil($top,align1)
unset -nocomplain ::eskil($top,align2)
unset -nocomplain ::eskil($top,aligntext1)
unset -nocomplain ::eskil($top,aligntext2)
}
# Mark a line as aligned.
proc markAlign {top side line text} {
set ::eskil($top,align$side) $line
set ::eskil($top,aligntext$side) $text
if {[info exists ::eskil($top,align1)] && [info exists ::eskil($top,align2)]} {
if { ! [string equal $::eskil($top,aligntext1) $::eskil($top,aligntext2)]} {
set apa [tk_messageBox -parent $top -icon question \
-title "Align" -type yesno -message \
"Those lines are not equal.\nReally align them?"]
if {$apa != "yes"} {
return 0
}
}
lappend ::eskil($top,aligns) $::eskil($top,align1) $::eskil($top,align2)
enableAlign $top
NoMarkAlign $top
return 1
}
return 0
}
# Called by popup menus over row numbers to add command for alignment.
# Returns 1 if nothing was added.
proc alignMenu {mW top side x y} {
# Get the row that was clicked
set W $::widgets($top,wLine$side)
set index [$W index @$x,$y]
set row [lindex [split $index "."] 0]
set data [$W get $row.0 $row.end]
# Must be a line number
if { ! [regexp {\d+} $data line]} {
return 1
}
set text [$::widgets($top,wDiff$side) get $row.0 $row.end]
set other [expr {$side == 1 ? 2 : 1}]
set cmd [list markAlign $top $side $line $text]
if { ! [info exists ::eskil($top,align$other)]} {
set label "Mark line for alignment"
} else {
set label "Align with line $::eskil($top,align$other) on other side"
}
if {[info exists ::eskil($top,aligns)]} {
foreach {align1 align2} $::eskil($top,aligns) {
if {$side == 1 && $line == $align1} {
set label "Remove alignment with line $align2"
set cmd [list clearAlign $top $align1]
} elseif {$side == 2 && $line == $align2} {
set label "Remove alignment with line $align1"
set cmd [list clearAlign $top $align1]
}
}
}
$mW add command -label $label -command $cmd
return 0
}
# Set up bindings to allow setting alignment using drag
proc SetupAlignDrag {top left right} {
bind $left <ButtonPress-1> [list startAlignDrag $top 1 %x %y %X %Y]\;break
bind $left <B1-Motion> [list motionAlignDrag $top 1 0 %x %y %X %Y]\;break
bind $left <Shift-B1-Motion> [list motionAlignDrag $top 1 1 %x %y %X %Y]\;break
bind $left <ButtonRelease-1> [list endAlignDrag $top 1 %x %y %X %Y]\;break
bind $left <B1-Leave> break
bind $right <ButtonPress-1> [list startAlignDrag $top 2 %x %y %X %Y]\;break
bind $right <B1-Motion> [list motionAlignDrag $top 2 0 %x %y %X %Y]\;break
bind $right <Shift-B1-Motion> [list motionAlignDrag $top 2 1 %x %y %X %Y]\;break
bind $right <ButtonRelease-1> [list endAlignDrag $top 2 %x %y %X %Y]\;break
bind $right <B1-Leave> break
}
# Button has been pressed over line window
proc startAlignDrag {top side x y X Y} {
# Get the row that was clicked
set W $::widgets($top,wLine$side)
set index [$W index @$x,$y]
set row [lindex [split $index "."] 0]
set data [$W get $row.0 $row.end]
set ::eskil($top,alignDrag,state) none
# Must be a line number
if { ! [regexp {\d+} $data line]} {
return 1
}
# Set up information about start of drag
set text [$::widgets($top,wDiff$side) get $row.0 $row.end]
set other [expr {$side == 1 ? 2 : 1}]
set ::eskil($top,alignDrag,X) $X
set ::eskil($top,alignDrag,Y) $Y
set ::eskil($top,alignDrag,from) $side
set ::eskil($top,alignDrag,line$side) $line
set ::eskil($top,alignDrag,text$side) $text
set ::eskil($top,alignDrag,line$other) "?"
set ::eskil($top,alignDrag,state) press
}
# Mouse moves with button down
proc motionAlignDrag {top side shift x y X Y} {
if {$::eskil($top,alignDrag,state) eq "press"} {
# Have we moved enough to call it dragging?
set dX [expr {abs($X - $::eskil($top,alignDrag,X))}]
set dY [expr {abs($Y - $::eskil($top,alignDrag,Y))}]
if {$dX + $dY > 3} {
# Start a drag action
set W $top.alignDrag
destroy $W
toplevel $W
wm overrideredirect $W 1
label $W.l -borderwidth 1 -relief solid -justify left
pack $W.l
set ::eskil($top,alignDrag,W) $W
set ::eskil($top,alignDrag,state) "drag"
}
}
if {$::eskil($top,alignDrag,state) eq "drag"} {
set W $::eskil($top,alignDrag,W)
# Move drag label with cursor
wm geometry $W +[expr {$X + 1}]+[expr {$Y + 1}]
set n $::eskil($top,alignDrag,from)
set other [expr {$side == 1 ? 2 : 1}]
set w2 $::widgets($top,wLine$other)
# Are we over the other line window?
if {[winfo containing $X $Y] eq $w2} {
set x [expr {$X - [winfo rootx $w2]}]
set y [expr {$Y - [winfo rooty $w2]}]
set index [$w2 index @$x,$y]
set row [lindex [split $index "."] 0]
set data [$w2 get $row.0 $row.end]
if { ! [regexp {\d+} $data line]} {
set ::eskil($top,alignDrag,line$other) "?"
} else {
set ::eskil($top,alignDrag,line$other) $line
set text [$::widgets($top,wDiff$other) get $row.0 $row.end]
set ::eskil($top,alignDrag,text$other) $text
}
} else {
set ::eskil($top,alignDrag,line$other) "?"
}
set txt "Align Left $::eskil($top,alignDrag,line1)"
append txt "\nwith Right $::eskil($top,alignDrag,line2)"
set ::eskil($top,alignDrag,shift) $shift
if {$shift} {
append txt "\nAnd Redo Diff"
}
$W.l configure -text $txt
}
}
# Button has been released
proc endAlignDrag {top side x y X Y} {
if {$::eskil($top,alignDrag,state) eq "drag"} {
destroy $::eskil($top,alignDrag,W)
# Are both line numbers valid? I.e. is this a full align operation?
if {$::eskil($top,alignDrag,line1) ne "?" && \
$::eskil($top,alignDrag,line2) ne "?"} {
NoMarkAlign $top
markAlign $top 1 $::eskil($top,alignDrag,line1) \
$::eskil($top,alignDrag,text1)
set marked [markAlign $top 2 $::eskil($top,alignDrag,line2) \
$::eskil($top,alignDrag,text2)]
if {$::eskil($top,alignDrag,shift) && $marked} {
redoDiff $top
}
}
}
set ::eskil($top,alignDrag,state) none
}
###################
# Diff highlighting
###################
proc hlSelect {top changeIndex} {
highLightChange $top $changeIndex
}
proc hlSeparate {top side changeIndex} {
set ::eskil($top,separate$side) $changeIndex
set wd $::widgets($top,wDiff$side)
set wl $::widgets($top,wLine$side)
if {$changeIndex eq ""} {
set range [$wd tag ranges sel]
} else {
set range [$wl tag ranges hl$::eskil($top,separate$side)]
}
set text [$wd get {*}$range]
set ::eskil($top,separatetext$side) $text
# Get the lines involved in the display
set from [lindex $range 0]
set to [lindex $range 1]
lassign [split $from "."] froml fromi
lassign [split $to "."] tol toi
if {$toi == 0} {incr tol -1}
# Get the corresponding lines in the file
set t [$wl get $froml.0 $tol.end]
set lines [lsort -integer [regexp -all -inline {\d+} $t]]
set froml [lindex $lines 0]
set tol [lindex $lines end]
set ::eskil($top,separatelines$side) [list $froml $tol]
if {[info exists ::eskil($top,separate1)] && \
[info exists ::eskil($top,separate2)]} {
cloneDiff $top [concat $::eskil($top,separatelines1) \
$::eskil($top,separatelines2)]
unset ::eskil($top,separate1)
unset ::eskil($top,separate2)
}
}
# No changeIndex means that the popup is over selected text rather than
# line numbers.
proc hlPopup {top side changeIndex X Y x y} {
if {[info exists ::eskil($top,nopopup)] && $::eskil($top,nopopup)} return
destroy .lpm
menu .lpm
if { ! [editMenu .lpm $top $side $changeIndex $x $y]} {
.lpm add separator
}
if {$changeIndex != ""} {
.lpm add command -label "Select" \
-command [list hlSelect $top $changeIndex]
}
set other [expr {$side == 1 ? 2 : 1}]
if { ! [info exists ::eskil($top,separate$other)]} {
set label "Mark for Separate Diff"
} else {
set label "Separate Diff"
}
.lpm add command -label $label -command [list hlSeparate $top $side $changeIndex]
alignMenu .lpm $top $side $x $y
set ::eskil($top,nopopup) 1
tk_popup .lpm $X $Y
after idle [list after 1 [list set "::eskil($top,nopopup)" 0]]
return
}
# This is called when right clicking over the line numbers which are not
# marked for changes
proc rowPopup {W X Y x y} {
set top [winfo toplevel $W]
if {[info exists ::eskil($top,nopopup)] && $::eskil($top,nopopup)} return
destroy .lpm
menu .lpm
regexp {(\d+)\D*$} $W -> side
set tmp1 [editMenu .lpm $top $side "_" $x $y]
if { ! $tmp1} {.lpm add separator}
set tmp2 [alignMenu .lpm $top $side $x $y]
if {$tmp1 && $tmp2} {
# Nothing in the menu
return
}
if { ! $tmp1 && $tmp2} {.lpm delete last}
set ::eskil($top,nopopup) 1
tk_popup .lpm $X $Y
after idle [list after 1 [list set "::eskil($top,nopopup)" 0]]
}
proc nextHighlight {top} {
# TBD TABLE, stop for now?
if {$::eskil($top,view) eq "table"} {
return
}
set tag hl$::HighLightCount
foreach side {1 2} {
set W $::widgets($top,wLine$side)
##nagelfar vartype W _obj,text
$W tag bind $tag <ButtonPress-3> \
"hlPopup $top $side $::HighLightCount %X %Y %x %y ; break"
$W tag bind $tag <ButtonPress-1> \
"hlSelect $top $::HighLightCount"
}
incr ::HighLightCount
}
#########
# Zooming
#########
proc zoomRow {W X Y x y} {
set top [winfo toplevel $W]
# Get the row that was clicked
set index [$W index @$x,$y]
set row [lindex [split $index "."] 0]
# Check if it is selected
if {[lsearch [$W tag names $index] sel] >= 0} {
regexp {(\d+)\D*$} $W -> side
hlPopup $top $side "" $X $Y $x $y
return
}
# Extract the data
set data(1) [$::widgets($top,wDiff1) dump -tag -text $row.0 $row.end]
set data(2) [$::widgets($top,wDiff2) dump -tag -text $row.0 $row.end]
if {[llength $data(1)] == 0 && [llength $data(2)] == 0} return
set font [$::widgets($top,wDiff1) cget -font]
set wx $X
set wy [expr {$Y + 4}]
destroy $top.balloon
toplevel $top.balloon -background black
wm withdraw $top.balloon
wm overrideredirect $top.balloon 1
foreach x {1 2} {
text $top.balloon.t$x -relief flat -font $font -background \#ffffcc \
-foreground black -padx 2 -pady 0 -height 1
$top.balloon.t$x tag configure new1 -foreground $::Pref(colornew1) \
-background $::Pref(bgnew1)
$top.balloon.t$x tag configure change -foreground $::Pref(colorchange) \
-background $::Pref(bgchange)
$top.balloon.t$x tag configure new2 -foreground $::Pref(colornew2) \
-background $::Pref(bgnew2)
$top.balloon.t$x tag configure equal -foreground $::Pref(colorequal) \
-background $::Pref(bgequal)
pack $top.balloon.t$x -side "top" -padx 1 -pady 1 -fill both -expand 1
set tags {}
foreach {key value index} $data($x) {
if {$key eq "tagon"} {
lappend tags $value
set tags [lsort -unique $tags]
} elseif {$key eq "tagoff"} {
set i [lsearch $tags $value]
if {$i >= 0} {
set tags [lreplace $tags $i $i]
}
} else {
$top.balloon.t$x insert end $value $tags
}
}
set text [$top.balloon.t$x get 1.0 1.end]
regsub -all "\t" $text " " text
$top.balloon.t$x configure -width [string length $text]
}
# Let geometry requests propagate
update idletasks
# Is the balloon within the diff window?
set rWidth [winfo reqwidth $top.balloon]
if {$rWidth + $wx > [winfo rootx $top] + [winfo width $top]} {
# No.
# Center on diff window
set wx [expr {([winfo width $top] - $rWidth) / 2 + [winfo rootx $top]}]
if {$wx < 0} {set wx 0}
# Is the balloon not within the screen?
if {$wx + $rWidth > [winfo screenwidth $top]} {
# Center in screen
set wx [expr {([winfo screenwidth $top] - $rWidth) / 2}]
if {$wx < 0} {set wx 0}
}
}
# Does the balloon fit within the screen?
if {$rWidth > [winfo screenwidth $top]} {
# How many rows does it take?
# Adjust ScreenWidth a bit to accomodate for padding.
set rows [expr {ceil(double($rWidth) / ([winfo screenwidth $top]-10))}]
# Add rows and fill screen width
$top.balloon.t1 configure -height $rows
$top.balloon.t2 configure -height $rows
# Let geometry requests propagate
update idletasks
wm geometry $top.balloon \
[winfo screenwidth $top]x[winfo reqheight $top.balloon]
set wx 0
}
wm geometry $top.balloon +$wx+$wy
wm deiconify $top.balloon
}
proc unzoomRow {W} {
set top [winfo toplevel $W]
destroy $top.balloon
}
# Helper for fillWindowX
proc FillWindowX {W widthName newXName} {
upvar 1 $widthName width $newXName newX
set x [winfo rootx $W]
set widths [::psballoon::FigureOutScreenWidths $W]
set nScreen [expr {[llength $widths] / 2}]
if {$nScreen <= 1} {
set width [winfo screenwidth $W]
set newX 0
return
}
if {$nScreen == 2} {
set minX [lindex $widths 0]
set maxX [lindex $widths end]
set width [expr {$maxX - $minX}]
set newX $minX
return
}
set widthList {}
set i -1
foreach {minX maxX} $widths {
incr i
lappend widthList [expr {$maxX - $minX}]
if {$minX <= $x && $x < $maxX} {
set screenI $i
}
}
if {$screenI == 0} {
set minX [lindex $widths 0]
set maxX [lindex $widths 3]
set width [expr {$maxX - $minX}]
set newX $minX
return
}
if {$screenI >= $nScreen-1} {
set minX [lindex $widths end-3]
set maxX [lindex $widths end]
set width [expr {$maxX - $minX}]
set newX $minX
return
}
set widthL [expr {[lindex $widthList $screenI] + [lindex $widthList $screenI-1]}]
set widthR [expr {[lindex $widthList $screenI] + [lindex $widthList $screenI+1]}]
if {$widthL >= $widthR} {
incr screenI -1
}
set minX [lindex $widths [* $screenI 2]]
set maxX [lindex $widths [expr {$screenI * 2 + 3}]]
set width [expr {$maxX - $minX}]
set newX $minX
}
# Maximize window in X direction, trying to fill two screens
proc fillWindowX {W} {
FillWindowX $W width newX
set newY [winfo rooty $W]
set height [winfo height $W]
puts "$W [wm geometry $W]"
puts "$W X $newX Y $newY W $width H $height"
wm geometry $W ${width}x$height+$newX+$newY
}
# Reconfigure font
proc chFont {} {
font configure myfont -size $::Pref(fontsize) -family $::Pref(fontfamily)
}
# Change color settings
proc applyColor {} {
global dirdiff
foreach top $::eskil(diffWindows) {
if {$top eq ".clipdiff"} continue
if {[string match .fourway* $top]} continue
if {$top != ".dirdiff"} {
foreach item {wLine1 wDiff1 wLine2 wDiff2} {
if { ! [info exists ::widgets($top,$item)]} continue
set W $::widgets($top,$item)
$W tag configure equal -foreground $::Pref(colorequal) \
-background $::Pref(bgequal)
$W tag configure new1 -foreground $::Pref(colornew1) \
-background $::Pref(bgnew1)
$W tag configure change -foreground $::Pref(colorchange) \
-background $::Pref(bgchange)
$W tag configure new2 -foreground $::Pref(colornew2) \
-background $::Pref(bgnew2)
}
continue
}
}
}
# Scroll text windows
proc scrollText {top args} {
# Do not scroll if focus is in a text window.
# This is for scroll bindings in the toplevel.
set class [winfo class [focus]]
if {$class in "Text TEntry"} {
return
}
$::widgets($top,wDiff1) {*}$args
if {[string index [lindex $args 0] 0] eq "x"} {
# x commands go to both since that is not synched
$::widgets($top,wDiff2) {*}$args
}
}
# Emulate a label that:
# 1 : Displays the right part of the text if there isn't enough room
# 2 : Justfify text to the left if there is enough room.
# 3 : Does not try to allocate space according to its contents
proc fileLabel {W args} {
ttk::entryX $W -style TLabel
$W configure {*}$args
$W configure -takefocus 0 -state readonly ;#-readonlybackground $bg
set i [lsearch $args -textvariable]
if {$i >= 0} {
set var [lindex $args [+ $i 1]]
uplevel \#0 "trace variable $var w \
{after idle {$W xview end} ;#}"
}
}
# Fill in default data for a diff window
proc initDiffData {top} {
set ::eskil($top,leftOK) 0
set ::eskil($top,rightOK) 0
set ::eskil($top,mode) ""
set ::eskil($top,view) ""
set ::eskil($top,printFileCmd) 0
set ::eskil($top,printFile) ""
set ::eskil($top,mergeFile) ""
set ::eskil($top,ancestorFile) ""
set ::eskil($top,separator) ""
set ::eskil($top,separatorview) ""
set ::eskil($top,conflictFile) ""
set ::eskil($top,limitlines) 0
set ::eskil($top,gz) 0
set ::eskil($top,maxwidth) 0
set ::eskil($top,plugin,1) ""
# Copy the collected options from command line
foreach {item val} $::eskil(defaultopts) {
set ::eskil($top,$item) $val
}
}
# Create a new diff window and diff two files
proc newDiff {file1 file2 {range {}}} {
set top [makeDiffWin]
update
set ::eskil($top,leftDir) [file dirname $file1]
set ::eskil($top,leftFile) $file1
set ::eskil($top,leftLabel) $file1
set ::eskil($top,leftOK) 1
set ::eskil($top,rightDir) [file dirname $file2]
set ::eskil($top,rightFile) $file2
set ::eskil($top,rightLabel) $file2
set ::eskil($top,rightOK) 1
set ::eskil($top,mode) ""
set ::eskil($top,view) ""
set ::eskil($top,range) $range
wm deiconify $top
raise $top
update
doDiff $top
return $top
}
# Create a new diff window equal to another, except for possibly a range
proc cloneDiff {other {range {}}} {
set top [makeDiffWin $other]
update
foreach item [array names ::eskil $other,*] {
regsub {^[^,]*,} $item {} item
set ::eskil($top,$item) $::eskil($other,$item)
}
if {[llength $range] != 0} {
set ::eskil($top,range) $range
}
wm deiconify $top
raise $top
update
doDiff $top
}
# A thing to easily get to debug mode
proc backDoor {top aVal} {
append ::eskil(backdoor) $aVal
set ::eskil(backdoor) [string range $::eskil(backdoor) end-9 end]
if {$::eskil(backdoor) eq "EskilDebug"} {
set ::eskil(debug) 1
catch {console show}
set ::eskil(backdoor) ""
AddDebugMenu $top
}
}
# Runtime disable of C version of DiffUtil
proc DisableDiffUtilC {} {
uplevel \#0 [list source $::eskil(thisDir)/../lib/diffutil/tcl/diffutil.tcl]
}
# Add a debug menu to a toplevel window
proc AddDebugMenu {top} {
set dMenu [debugMenu $top.m]
$dMenu add checkbutton -label "Wrap" -variable wrapstate \
-onvalue char -offvalue none -command \
"$::widgets($top,wDiff1) configure -wrap \$wrapstate ;\
$::widgets($top,wDiff2) configure -wrap \$wrapstate"
psmenu::psmenu $dMenu -top $top {
---
"&Reread Source" -cmd EskilRereadSource
---
"Normal Cursor" -cmd "normalCursor $top"
"Fill X" -cmd "fillWindowX $top"
---
# Runtime disable of C version of DiffUtil
"Tcl DiffUtil" -cmd DisableDiffUtilC
"Evalstats" -cmd {evalstats}
"_stats" -cmd {parray _stats}
}
}
# Build the main window
# "other" is related window. Currently unused
proc makeDiffWin {{other {}} args} {
global tcl_platform
# Locate a free toplevel name
if {[info exists ::eskil(topDiffCnt)]} {
set t $::eskil(topDiffCnt)
} else {
set t 0
}
while {[winfo exists .diff$t]} {
incr t
}
set top .diff$t
toplevel $top
eskilRegisterToplevel $top
initDiffData $top
if {"-table" in $args} {
set ::eskil($top,view) "table"
}
wm title $top "Eskil:"
wm protocol $top WM_DELETE_WINDOW [list cleanupAndExit $top]
ttk::frame $top.f
grid $top.f -row 0 -columnspan 5 -sticky nws
lappend ::widgets(toolbars) $top.f
if { ! $::Pref(toolbar)} {
grid remove $top.f
}
set redoState [expr {$::eskil(debug) == 1 ? "normal" : "disabled"}]
psmenu::psmenu $top {
"&File" {
"Redo &Diff" -cmd "redoDiff $top" -state $redoState \
-cfgvar ::widgets($top,configureRedoDiffCmd)
---
"&Open Both..." -cmd "openBoth $top 0"
"Open Both (forget)..." -cmd "openBoth $top 1"
"Open Left File..." -cmd "openLeft $top"
"Open Right File..." -cmd "openRight $top"
---
"Open Ancestor File..." -cmd "openAncestor $top"
"Open Conflict File..." -cmd "openConflict $top"
"Open Patch File..." -cmd "openPatch $top"
"&Revision Diff..." -cmd "openRev $top"
---
"&Print Pdf..." -cmd "doPrint $top"
---
"&Close" -cmd "list cleanupAndExit $top"
---
"&Quit" -cmd "cleanupAndExit all"
}
"&Options" {
"&Font" {
"&Select..." -command makeFontWin
_Radio -var ::Pref(fontsize) -command chFont {
6 7 8 9 10
}
}
"&Ignore" {
"No spaces" -var ::Pref(ignore) -value " "
"Space changes (-b)" -var ::Pref(ignore) -value "-b"
"All spaces (-w)" -var ::Pref(ignore) -value "-w"
---
"Case (-i)" -var ::Pref(nocase)
"Empty" -var ::Pref(noempty)
"Digits" -var ::Pref(nodigit)
}
"&Preprocess..." -cmd "EditPrefPreprocess $top"
"P&lugins..." -cmd "editPrefPlugins $top"
"P&arse" {
"Nothing" -var ::Pref(parse) -value 0
"Lines" -var ::Pref(parse) -value 1
"Blocks (small)" -var ::Pref(parse) -value 2
"Blocks" -var ::Pref(parse) -value 3
---
"Characters" -var ::Pref(lineparsewords) -value "0"
"Words" -var ::Pref(lineparsewords) -value "1"
---
"Fine chunks" -var ::Pref(finegrainchunks)
---
"Mark last" -var ::Pref(marklast)
}
"&Colours..." -cmd makePrefWin
"C&ontext" {
"Show all lines" -var ::Pref(context) -value -1
"Show only diffs" -var ::Pref(context) -value 0
---
"Context 2 lines" -var ::Pref(context) -value 2
"Context 5 lines" -var ::Pref(context) -value 5
"Context 10 lines" -var ::Pref(context) -value 10
"Context 20 lines" -var ::Pref(context) -value 20
}
"Pi&vot" {
"1" -var ::Pref(pivot) -value 1
"10" -var ::Pref(pivot) -value 10
"100" -var ::Pref(pivot) -value 100
"1000" -var ::Pref(pivot) -value 1000
"Max" -var ::Pref(pivot) -value 2000000000
}
---
"Toolbar" -var ::Pref(toolbar)
---
"Save default" -cmd "saveOptions $top"
}
"&Search" -var searchMenu {
# Added below
}
"&Tools" {
"&New Diff Window" -cmd "makeDiffWin $top"
"&Directory Diff" -cmd makeDirDiffWin
"&Clip Diff" -cmd makeClipDiffWin
"&Fourway Diff" -cmd makeFourWayWin
"&Table Diff" -cmd "makeDiffWin $top -table"
"&Merge" -cmd "makeMergeWin $top" -state disabled \
-cfgvar ::widgets($top,configureMergeCmd)
"&Edit Mode" -cmd "allowEdit $top" -acc Ctrl-E -state disabled \
-cfgvar ::widgets($top,configureEditModeCmd)
"&Paste Patch" -cmd "doPastePatch $top"
"Clear Align" -cmd "clearAlign $top" -state disabled \
-cfgvar ::widgets($top,configureAlignCmd)
"Highlight tabs" -cmd "highlightTabs $top"
if {$::tcl_platform(platform) eq "windows"} {
if { ! [catch {package require registry}]} {
---
"Setup &Registry" -cmd makeRegistryWin
}
}
}
"&Help" {
"&General" -cmd makeHelpWin
"&Tutorial" -cmd makeTutorialWin
"&Revision Control" -cmd "makeDocWin revision.txt"
"&Edit Mode" -cmd "makeDocWin editmode.txt"
"&Plugins" -cmd "makeDocWin plugins.txt"
---
"&About" -cmd makeAboutWin
}
}
if {[info procs textSearch::searchMenu] != ""} {
textSearch::searchMenu $searchMenu
} else {
$searchMenu add command -label "Text search not available" -state disabled
}
# Toolbar
ttk::label $top.lr1 -text "Rev 1"
addBalloon $top.lr1 "Revision number for version diff."
ttk::entryX $top.er1 -width 12 -textvariable ::eskil($top,doptrev1)
set ::widgets($top,rev1) $top.er1
bind $top.er1 <Key-Return> [list redoDiff $top]
ttk::label $top.lr2 -text "Rev 2"
addBalloon $top.lr2 "Revision number for version diff."
ttk::entryX $top.er2 -width 12 -textvariable ::eskil($top,doptrev2)
set ::widgets($top,rev2) $top.er2
bind $top.er2 <Key-Return> [list redoDiff $top]
ttk::button $top.bcm -text Commit -command [list revCommit $top] \
-state disabled -underline 0
set ::widgets($top,commit) $top.bcm
ttk::button $top.brv -text Revert -command [list revRevert $top] \
-state disabled
set ::widgets($top,revert) $top.brv
ttk::button $top.blg -text Log -command [list revLog $top] \
-state disabled -underline 0
set ::widgets($top,log) $top.blg
ttk::button $top.bfp -text "Prev Diff" \
-command [list findDiff $top -1] \
-underline 0
ttk::button $top.bfn -text "Next Diff" \
-command [list findDiff $top 1] \
-underline 0
bind $top <Alt-n> [list findDiff $top 1]
bind $top <Alt-p> [list findDiff $top -1]
bind $top <Alt-c> [list revCommit $top]
bind $top <Alt-l> [list revLog $top]
pack $top.bfn -in $top.f -side right -padx {3 6}
pack $top.bfp $top.bcm $top.brv $top.blg \
$top.er2 $top.lr2 $top.er1 $top.lr1 \
-in $top.f -side right -padx 3
# Adjust
pack $top.bfn $top.bfp $top.bcm -ipadx 15
# Add a separator entry in toolbar if table mode is on
if {$::eskil($top,view) eq "table"} {
ttk::label $top.lsep -text "Sep"
addBalloon $top.lsep "Separator for interpreting file as table"
ttk::entryX $top.esep -width 2 -textvariable ::eskil($top,separatorview)
set ::widgets($top,sep) $top.esep
bind $top.esep <Key-Return> [list redoDiff $top]
pack $top.esep $top.lsep \
-in $top.f -side right -padx 3
}
# File and progress indicators
catch {font delete myfont}
font create myfont -family $::Pref(fontfamily) -size $::Pref(fontsize)
fileLabel $top.l1 -textvariable ::eskil($top,leftLabel)
fileLabel $top.l2 -textvariable ::eskil($top,rightLabel)
ttk::label $top.le -textvariable ::widgets($top,eqLabel) -width 1
addBalloon $top.le -fmt {
* means external diff is running.\n
= means files do not differ.\n
! means a large block is being processed.\n
Blank means files differ.
}
# Main window
if {$::eskil($top,view) eq "table"} {
# Single frame for contents
ttk::frame $top.ft -borderwidth 2 -relief sunken
grid $top.l1 $top.le $top.l2 -row 1 -sticky news
grid $top.ft - - -row 2 -sticky news
grid columnconfigure $top "0 2" -weight 1
grid rowconfigure $top $top.ft -weight 1
# TBD TABLE
tablelist::tablelist $top.ft.tab -height 25 -width 100 \
-font myfont -labelfont myfont \
-movablecolumns no -setgrid no -showseparators no \
-fullseparators yes -selectmode extended \
-colorizecommand tblModeColorCallback
ttk::scrollbar $top.ft.vsb -orient vertical \
-command "$top.ft.tab yview"
ttk::scrollbar $top.ft.hsb -orient horizontal \
-command "$top.ft.tab xview"
$top.ft.tab configure -yscrollcommand "$top.ft.vsb set" \
-xscrollcommand "$top.ft.hsb set"
set body [$top.ft.tab bodypath]
$body tag configure new1 -foreground $::Pref(colornew1) \
-background $::Pref(bgnew1)
$body tag configure new2 -foreground $::Pref(colornew2) \
-background $::Pref(bgnew2)
$body tag configure change -foreground $::Pref(colorchange) \
-background $::Pref(bgchange)
set bg [ttk::style configure . -background]
set map [createMap $top $bg]
grid $top.ft.tab $top.ft.vsb $map -sticky news
grid $top.ft.hsb x x -sticky news
grid columnconfigure $top.ft 0 -weight 1
grid rowconfigure $top.ft 0 -weight 1
grid $map -pady [expr {[winfo reqwidth $top.ft.vsb] - 2}]
set ::widgets($top,wTable) $top.ft.tab
} else {
ttk::frame $top.ft1 -borderwidth 2 -relief sunken
text $top.ft1.tl -height $::Pref(lines) -width 5 -wrap none \
-font myfont -borderwidth 0 -padx 0 -highlightthickness 0 \
-takefocus 0
text $top.ft1.tt -height $::Pref(lines) -width $::Pref(linewidth) \
-wrap none \
-xscrollcommand [list $top.sbx1 set] \
-font myfont -borderwidth 0 -padx 1 \
-highlightthickness 0
$top.ft1.tt configure -tabstyle wordprocessor
tk::frame $top.ft1.f -width 2 -height 2 -background lightgray
pack $top.ft1.tl -side left -fill y
pack $top.ft1.f -side left -fill y
pack $top.ft1.tt -side right -fill both -expand 1
ttk::scrollbar $top.sby -orient vertical
ttk::scrollbar $top.sbx1 -orient horizontal \
-command [list $top.ft1.tt xview]
set ::widgets($top,wLine1) $top.ft1.tl
set ::widgets($top,wDiff1) $top.ft1.tt
ttk::frame $top.ft2 -borderwidth 2 -relief sunken
text $top.ft2.tl -height $::Pref(lines) -width 5 -wrap none \
-font myfont -borderwidth 0 -padx 0 -highlightthickness 0 \
-takefocus 0
text $top.ft2.tt -height $::Pref(lines) -width $::Pref(linewidth) \
-wrap none \
-xscrollcommand [list $top.sbx2 set] \
-font myfont -borderwidth 0 -padx 1 \
-highlightthickness 0
$top.ft2.tt configure -tabstyle wordprocessor
tk::frame $top.ft2.f -width 2 -height 2 -background lightgray
pack $top.ft2.tl -side left -fill y
pack $top.ft2.f -side left -fill y
pack $top.ft2.tt -side right -fill both -expand 1
ttk::scrollbar $top.sbx2 -orient horizontal \
-command [list $top.ft2.tt xview]
set ::widgets($top,wLine2) $top.ft2.tl
set ::widgets($top,wDiff2) $top.ft2.tt
# Set up a tag for incremental search bindings
if {[info procs textSearch::enableSearch] != ""} {
textSearch::enableSearch $top.ft1.tt -label ::widgets($top,isearchLabel)
textSearch::enableSearch $top.ft2.tt -label ::widgets($top,isearchLabel)
}
# Set up file dropping in text windows if TkDnd is available
if { ! [catch {package require tkdnd}]} {
dnd bindtarget $top text/uri-list <Drop> "fileDrop $top any %D"
dnd bindtarget $top.ft1.tl text/uri-list <Drop> "fileDrop $top left %D"
dnd bindtarget $top.ft1.tt text/uri-list <Drop> "fileDrop $top left %D"
dnd bindtarget $top.ft2.tl text/uri-list <Drop> "fileDrop $top right %D"
dnd bindtarget $top.ft2.tt text/uri-list <Drop> "fileDrop $top right %D"
}
# FIXA: verify that this label is ok after Tile migration
ttk::label $top.ls -width 1 \
-textvariable ::widgets($top,isearchLabel)
addBalloon $top.ls "Incremental search indicator"
set bg [ttk::style configure . -background]
set map [createMap $top $bg]
# Edit buttons widget
set ::widgets($top,wTb) $top.tb
text $top.tb -width 4 -wrap none -background $bg \
-font myfont -borderwidth 0 -padx 0 -highlightthickness 0 \
-takefocus 0
commonYScroll $top.sby $top.ft1.tl $top.ft1.tt $top.ft2.tl $top.ft2.tt \
;#$top.tb
applyColor
foreach W [list $top.ft1.tt $top.ft2.tt] {
# The last change in a row is underlined
$W tag configure last -underline 1
# Each file in a patch view starts with a block of this type
$W tag configure patch -background gray
# Make sure selection is visible
$W tag raise sel
bind $W <ButtonPress-3> "zoomRow %W %X %Y %x %y"
bind $W <ButtonRelease-3> "unzoomRow %W"
}
foreach W [list $top.ft1.tl $top.ft2.tl] {
$W tag configure align -underline 1
bind $W <ButtonPress-3> "rowPopup %W %X %Y %x %y"
}
SetupAlignDrag $top $top.ft1.tl $top.ft2.tl
grid $top.l1 $top.le - - $top.l2 -row 1 -sticky news
grid $top.ft1 $top.tb $map $top.sby $top.ft2 -row 2 -sticky news
grid $top.sbx1 $top.ls - - $top.sbx2 -row 3 -sticky news
grid columnconfigure $top "$top.ft1 $top.ft2" -weight 1
grid rowconfigure $top $top.ft1 -weight 1
grid $top.tb -pady 2
grid $map -pady [expr {[winfo reqwidth $top.sby] - 2}]
grid $top.ls -sticky ""
grid remove $top.tb ;# Hide until done
# Allow scrolling from keys at toplevel
bind $top <Key-Up> [list scrollText $top yview scroll -1 u]
bind $top <Key-k> [list scrollText $top yview scroll -1 u]
bind $top <Key-Down> [list scrollText $top yview scroll 1 u]
bind $top <Key-j> [list scrollText $top yview scroll 1 u]
bind $top <Key-Prior> [list scrollText $top yview scroll -1 pa]
bind $top <Key-b> [list scrollText $top yview scroll -1 pa]
bind $top <Key-Next> [list scrollText $top yview scroll 1 pa]
bind $top <Key-space> [list scrollText $top yview scroll 1 pa]
bind $top <Key-Left> [list scrollText $top xview scroll -5 u]
bind $top <Key-h> [list scrollText $top xview scroll -5 u]
bind $top <Key-Right> [list scrollText $top xview scroll 5 u]
bind $top <Key-l> [list scrollText $top xview scroll 5 u]
bind $top <Key-Home> [list scrollText $top yview moveto 0]
bind $top <Key-g> [list scrollText $top yview moveto 0]
bind $top <Key-End> [list scrollText $top yview moveto 1]
}
# Go out to toplevel with escape, whereever you are
bind $top <Key-Escape> [list focus $top]
if {$::eskil(debug) == 0} {
set val [bindtags $top]
lappend val backDoor$top
bindtags $top $val
# Keep this binding on a separate tag, so that other key
# bindings on the top does not steal the keys
bind backDoor$top <Key> "backDoor $top %A"
}
if {$::eskil(debug) == 1} {
AddDebugMenu $top
}
resetEdit $top
return $top
}
proc ValidateNewColors {} {
foreach item {colorchange bgchange colornew1 bgnew1
colornew2 bgnew2 colorequal bgequal} {
if { ! [info exists ::TmpPref($item)]} continue
set col $::TmpPref($item)
if {$col eq ""} continue
if {[catch {winfo rgb . $col}]} {
# FIXA: Error message
# Just restore for now
set ::TmpPref($item) $::Pref($item)
}
}
}
# Set new preferences.
proc applyPref {} {
ValidateNewColors
array set ::Pref [array get ::TmpPref]
applyColor
}
# Update test color fields.
proc testColor {} {
ValidateNewColors
.pr.fc.t1 tag configure change -foreground $::TmpPref(colorchange) \
-background $::TmpPref(bgchange)
.pr.fc.t2 tag configure new1 -foreground $::TmpPref(colornew1) \
-background $::TmpPref(bgnew1)
.pr.fc.t3 tag configure new2 -foreground $::TmpPref(colornew2) \
-background $::TmpPref(bgnew2)
.pr.fc.t4 tag configure equal -foreground $::TmpPref(colorequal) \
-background $::TmpPref(bgequal)
}
# Color dialog.
proc selColor {name} {
set old $::TmpPref($name)
if {$old eq ""} {
set t [tk_chooseColor -parent .pr]
} else {
set t [tk_chooseColor -parent .pr -initialcolor $old]
}
if {$t != ""} {
set ::TmpPref($name) $t
}
}
# Create a window for changing preferences.
# Currently only colors are changed in this dialog.
proc makePrefWin {} {
array set ::TmpPref [array get ::Pref]
destroy .pr
toplevel .pr
wm title .pr "Eskil Preferences"
ttk::frame .pr.fc -borderwidth 1 -relief solid
ttk::label .pr.fc.l1 -text "Colours" -anchor w
ttk::label .pr.fc.l2 -text "Text" -anchor w
ttk::label .pr.fc.l3 -text "Background" -anchor w
ttk::entryX .pr.fc.e1 -textvariable "::TmpPref(colorchange)" -width 10
ttk::entryX .pr.fc.e2 -textvariable "::TmpPref(colornew1)" -width 10
ttk::entryX .pr.fc.e3 -textvariable "::TmpPref(colornew2)" -width 10
ttk::entryX .pr.fc.e4 -textvariable "::TmpPref(colorequal)" -width 10
ttk::button .pr.fc.b1 -text "Sel" -command "selColor colorchange"
ttk::button .pr.fc.b2 -text "Sel" -command "selColor colornew1"
ttk::button .pr.fc.b3 -text "Sel" -command "selColor colornew2"
ttk::button .pr.fc.b4 -text "Sel" -command "selColor colorequal"
ttk::entryX .pr.fc.e5 -textvariable "::TmpPref(bgchange)" -width 10
ttk::entryX .pr.fc.e6 -textvariable "::TmpPref(bgnew1)" -width 10
ttk::entryX .pr.fc.e7 -textvariable "::TmpPref(bgnew2)" -width 10
ttk::entryX .pr.fc.e8 -textvariable "::TmpPref(bgequal)" -width 10
ttk::button .pr.fc.b5 -text "Sel" -command "selColor bgchange"
ttk::button .pr.fc.b6 -text "Sel" -command "selColor bgnew1"
ttk::button .pr.fc.b7 -text "Sel" -command "selColor bgnew2"
ttk::button .pr.fc.b8 -text "Sel" -command "selColor bgequal"
text .pr.fc.t1 -width 12 -height 1 -font myfont -takefocus 0
text .pr.fc.t2 -width 12 -height 1 -font myfont -takefocus 0
text .pr.fc.t3 -width 12 -height 1 -font myfont -takefocus 0
text .pr.fc.t4 -width 12 -height 1 -font myfont -takefocus 0
.pr.fc.t1 tag configure change -foreground $::TmpPref(colorchange) \
-background $::TmpPref(bgchange)
.pr.fc.t2 tag configure new1 -foreground $::TmpPref(colornew1) \
-background $::TmpPref(bgnew1)
.pr.fc.t3 tag configure new2 -foreground $::TmpPref(colornew2) \
-background $::TmpPref(bgnew2)
.pr.fc.t4 tag configure equal -foreground $::TmpPref(colorequal) \
-background $::TmpPref(bgequal)
.pr.fc.t1 insert end "Changed text" change
.pr.fc.t2 insert end "Deleted text" new1
.pr.fc.t3 insert end "Added text" new2
.pr.fc.t4 insert end "Equal text" equal
.pr.fc.t1 configure -state disabled
.pr.fc.t2 configure -state disabled
.pr.fc.t3 configure -state disabled
.pr.fc.t4 configure -state disabled
ttk::button .pr.b1 -text "Apply" -command applyPref
ttk::button .pr.b2 -text "Test" -command testColor
ttk::button .pr.b3 -text "Close" -command {destroy .pr}
grid .pr.fc.l1 .pr.fc.l2 x .pr.fc.l3 x -row 0 -sticky ew -padx 1 -pady 1
grid .pr.fc.t1 .pr.fc.e1 .pr.fc.b1 .pr.fc.e5 .pr.fc.b5 -row 1 \
-sticky nsew -padx 1 -pady 1
grid .pr.fc.t2 .pr.fc.e2 .pr.fc.b2 .pr.fc.e6 .pr.fc.b6 -row 2 \
-sticky nsew -padx 1 -pady 1
grid .pr.fc.t3 .pr.fc.e3 .pr.fc.b3 .pr.fc.e7 .pr.fc.b7 -row 3 \
-sticky nsew -padx 1 -pady 1
grid .pr.fc.t4 .pr.fc.e4 .pr.fc.b4 .pr.fc.e8 .pr.fc.b8 -row 4 \
-sticky nsew -padx 1 -pady 1
grid columnconfigure .pr.fc {1 3} -weight 1
pack .pr.fc -side top -fill x
pack .pr.b1 .pr.b2 .pr.b3 -side left -expand 1 -fill x -anchor s \
-padx 2 -pady 2
}
# Change font preference
proc applyFont {lb} {
set ::Pref(fontsize) $::TmpPref(fontsize)
set i [lindex [$lb curselection] 0]
set ::Pref(fontfamily) [$lb get $i]
chFont
}
# Update example font
proc exampleFont {lb} {
set i [lindex [$lb curselection] 0]
if {$i eq ""} return
set ::TmpPref(fontfamily) [$lb get $i]
font configure tmpfont -family $::TmpPref(fontfamily)
if {[string is integer -strict $::TmpPref(fontsize)]} {
font configure tmpfont -size $::TmpPref(fontsize)
}
}
proc UpdateFontBox {lb} {
$lb delete 0 end
foreach {f fixed} $::FontCache {
if {$fixed || !$::eskil(fixedfont)} {
$lb insert end $f
if {[string equal -nocase $f $::Pref(fontfamily)]} {
$lb selection set end
$lb see end
}
}
}
}
# Font dialog
proc makeFontWin {} {
global FontCache
destroy .fo
toplevel .fo -padx 3 -pady 3
wm title .fo "Select Font"
ttk::label .fo.ltmp -text "Searching for fonts..."
pack .fo.ltmp -padx {10 50} -pady {10 50}
update
catch {font delete tmpfont}
font create tmpfont
array set ::TmpPref [array get ::Pref]
ttk::labelframe .fo.lf -text "Family" -padding 3
set lb [Scroll y listbox .fo.lf.lb -width 15 -height 10 \
-exportselection no -selectmode single]
bind $lb <<ListboxSelect>> [list exampleFont $lb]
pack .fo.lf.lb -fill both -expand 1
ttk::labelframe .fo.ls -text "Size" -padding 3
spinbox .fo.ls.sp -from 1 -to 30 -increment 1 -width 3 -state readonly \
-textvariable ::TmpPref(fontsize) -command [list exampleFont $lb]
pack .fo.ls.sp -fill both -expand 1
ttk::label .fo.le -text "Example\n0Ooi1Il" -anchor w -font tmpfont \
-width 1 -justify left
if { ! [info exists ::eskil(fixedfont)]} {set ::eskil(fixedfont) 1}
ttk::checkbutton .fo.cb -text "Fixed" -variable ::eskil(fixedfont) \
-command [list UpdateFontBox $lb]
ttk::button .fo.bo -text "Ok" -command "applyFont $lb ; destroy .fo"
ttk::button .fo.ba -text "Apply" -command "applyFont $lb"
ttk::button .fo.bc -text "Close" -command "destroy .fo"
if { ! [info exists FontCache]} {
set fam [lsort -dictionary [font families]]
font create testfont
foreach f $fam {
if { ! [string equal $f ""]} {
font configure testfont -family $f
lappend FontCache $f [font metrics testfont -fixed]
}
}
font delete testfont
}
UpdateFontBox $lb
destroy .fo.ltmp
grid .fo.lf .fo.ls -sticky news -padx 3 -pady 3
grid x .fo.cb -sticky nwe -padx 3 -pady 3
grid x .fo.bo -sticky we -padx 3 -pady 3 -ipadx 10
grid x .fo.ba -sticky we -padx 3 -pady 3 -ipadx 10
grid x .fo.bc -sticky we -padx 3 -pady 3 -ipadx 10
grid .fo.le - -sticky nwe -padx 3 -pady 3
grid .fo.lf -sticky news -rowspan 5
grid columnconfigure .fo 0 -weight 1
grid rowconfigure .fo 1 -weight 1
exampleFont $lb
}