Doubleclick Generalized

Difference between version 9 and 10 - Previous - Next
[wdb] This package `DoubleClick` extends text widget by generalized behaviour: deals with quotes `""` as well as international quotes such as „Deutsche Gänsefüßchen“, moreover all kinds of nesting parens – `(){}[[]]<>` – where the angles `<…>` are treatened depending of XML situation: either as comments – `<!--…-->` – or – or DOM tag elements – `<p> … </p>` – or as special markup.

Triple click selects grammatical sentence.
Put bindings to a single widget, or to arbitrary tag `ComfortableText`, or to a newly-defined Tag to use with [bindtags]. Iʼd say, quite nützlich. I found it neccessary for editing master files of [One Hand]; and I guess it should be usable for PHP files too (but not tested, any volunteer?). In German we call such thing “Eierlegende Wollmilchsau”. Ask one who can translate it.

======
# 
# file: DoubleClick-0.3.tm
# Usage: 
#   package require DoubleClick
#   bindDoubleClick .textwindow ?yes|no?
#   bindDoubleClick ComfortableText ?yes|no?
#
# option yes is default
# if "yes" then triple-click selects grammatical sentence
# if "no" then no specific triple-click
# 
if false {
  Double-click behaves as Emacs:
  
  Double-click on quote (") selects balanced counterpart,
  opening or closing depends on environment.
  double-click on opening brace \{ selects 
  to balanced closing counterbrace \} and vice versa.
  Round parens ( and ) dito.
  
  Moreover:
  
  Double-click on international quote such as “this” selects to 
  balanced counter-quote, back or forward depends on environment.
  (Caution! Reliable only if no different national quotes mixed.)
  
  Double-click on opening CSS comment such as "/*" selects to "*/".
  Double-click on internetprotocol such as "http://wolf-dieter-busch.de"
  selects complete URI.
  
  Double-click on less-than < selects to counterpart:
  XML comment such as <!-- ... --> to closing char ">",
  "<? ..." to "?>, "<! ..." to ">", "<![CDATA[ ..." to "]]>",
  opening tag such as "<a>" to closing counterpart such as "</a>",
  on closing tag "</a>" vice versa to opening tag "<a>".
  
  Double-click on leading "&" of entity such as "&amp;" selects to ";".
  
  Tcl-specific: Double-click on leading "$" selects Tcl string.
  Double-click on widget name such as ".t.menu" selects complete name.
}
# 

package require Tcl 8.6.1
package provide DoubleClick 0.3

namespace eval DoubleClick {
  variable quotes { „ “ ‚ ‘ “ ” ‘ ’ » « › ‹ « » ‹ › }
  variable click none
  variable index 1.0
}

proc DoubleClick::backslashed {win {index insert}} {
  # \{ \}
  set target [$win search -elide -backwards -regexp {[^\\]} $index 1.0]
  if {$target eq ""} then {
    return false
  }
  set found [$win get $target+1chars $index]
  set length [string length $found]
  if {$length % 2 == 1} then {
    return true
  } else {
    return false
  }
}

proc DoubleClick::extendSel {win target} {
  lassign "[$win tag ranges sel] insert" start
  $win tag add sel $start $target
}

proc DoubleClick::findCSScomment {win {index insert}} {
  if {[$win get $index $index+2chars] ne "/*"} then {
    return false
  }
  set target [$win search -elide */ $index end]
  if {$target eq ""} then {
    return false
  } else {
    extendSel $win $target+2chars
    return true
  }
}

proc DoubleClick::findTagComment {win {index insert}} {
  # -- comment ... --
  if {[$win get $index $index+2chars] ne "--"} then {
    return false
  } else {
    set idx [$win search -elide -- -- $index+2chars end]
    if {$idx eq ""} then {
      return false
    } else {
      $win mark set insert $index
      extendSel $win $idx+2chars
      return true
    }
  }
}

proc DoubleClick::findUrl {win {index insert}} {
  # "http://wolf-dieter-busch.de/blog/index.htm"
  set wordStart [$win index "$index wordstart"]
  set wordEnd [$win index "$index wordend"]
  set word [$win get $wordStart $wordEnd+3chars]
  if {[string match *:// $word]} then {
    set urlEnd [$win search -regexp -elide {[\s<>]} $wordStart end]
    #
    # quotes
    #
    set q [$win get $wordStart-1chars]
    if {$q in {' {"}}} then {
      set idx [$win search -elide $q $wordStart $urlEnd]
      if {$idx ne ""} then {
        set urlEnd $idx
      } else {
        return false
      }
    }
    $win mark set insert $wordStart
    if {
      ([$win get $urlEnd-1char] eq {"} && $q ne {"}) ||
      ([$win get $urlEnd-1char] eq {'} && $q ne {'})
    } then {
      set urlEnd $urlEnd-1char
    }
    extendSel $win $urlEnd
    return true
  } else {
    return false
  }
}

proc DoubleClick::findEntity {win {index insert}} {
  if {[$win get $index] eq "&"} then {
    set endIdx [$win search ";" $index end]
    if {$endIdx eq ""} then {
      return false
    } else {
      set name [$win get $index+1char $endIdx]
      if {
        [regexp {^[a-zA-Z]+$} $name] ||
        [regexp {^#[[:digit:]]+$} $name] ||
        [regexp {^#x[[:xdigit:]]+$} $name]
      } then {
        extendSel $win $endIdx+1chars
        return true
      } else {
        return false
      }
    }
  } else {
    return false
  }
}

proc DoubleClick::startOfExpression {win {index insert}} {
  set startIdx [$win search -elide -backwards \{ $index 1.0 ]
  if {$startIdx eq ""} then {
    $win index 1.0
  } elseif {[info complete [$win get 1.0 $startIdx]]} then {
    $win index $startIdx+1char
  } else {
    startOfExpression $win $startIdx
  }
}

proc DoubleClick::openOrClosedQuote? {win {index insert}} {
  set start [startOfExpression $win $index]
  set txt [string map [list \{ " " \} " " ] [$win get $start $index+1char]]
  if {[$win get $index] == "'"} then {
    set txt [string map [list \" " " ' \"] $txt]
  }
  expr {[info complete $txt] ? "closing" : "opening"}
}

proc DoubleClick::findOpenQuote {win {index insert}} {
  set char [$win get $index]
  set startIdx [startOfExpression $win $index]
  set quoteIdx [$win search -backward $char $index-1char $startIdx]
  while {$quoteIdx ne "" && [backslashed $win $quoteIdx]} {
    set quoteIdx [$win search -backward $char $quoteIdx-1char $startIdx]
  }
  $win tag add sel $quoteIdx $index+1char
  return true
}

proc DoubleClick::findCloseDquote {win {index insert}} {
  # " ... "
  if {[openOrClosedQuote? $win $index] eq "closing"} then {
    return [findOpenQuote $win $index]
  }
  set start $index
  while true {
    set target [$win search -elide \u0022 $start+2chars end]
    if {$target eq ""} then {
      return false
    }
    if {[info complete [$win get $index $target+2chars]]} then {
      extendSel $win  $target+1chars
      return true
    }
    set start $target+1chars
  }
}

proc DoubleClick::findCloseSquote {win {index insert}} {
  # ' ... '
  if {[openOrClosedQuote? $win $index] eq "closing"} then {
    return [findOpenQuote $win $index]
  }
  set start $index
  while true {
    set target [$win search -elide ' $start+1chars end]
    if {$target eq ""} then {
      return false
    }
    set txt [$win get $index $target+1chars]
    set txt\
      [string map [list \u0022 " " \{ " " \} " " ' \u0022] $txt]
    if {[info complete $txt]} then {
      extendSel $win  $target+1chars
      return true
    }
    set start $target+2chars
  }
}

proc DoubleClick::findOpenBrace {win {index insert}} {
  # { ... }
  set start $index
  while true {
    set start [$win search -elide -backward \{ $start 1.0]
    if {$start eq ""} break
    set target [closeBraceIdx $win $start]
    if {[backslashed $win $start]} continue
    if {[$win compare [closeBraceIdx $win $start] >= $index]} then {
      $win tag add sel $start $index+1char
      return true
    }
  }
  return false
}

proc DoubleClick::closeBraceIdx {win {index insert}} {
  # { ... }
  set start $index
  while true {
    set target [$win search -elide \u007d $start end]
    if {$target eq ""} break
    if {[info complete [$win get $index $target+1chars]]} then {
      return $target
    }
    set start $target+1chars
  }
}

proc DoubleClick::findCloseBrace {win {index insert}} {
  set target [closeBraceIdx $win $index]
  if {$target eq ""} then {
    return false
  } else {
    extendSel $win $target+1char
    return true
  }
}

proc DoubleClick::findCloseBracket {win {index insert}} {
  # [ ... ]
  lappend map \[ \{ \] \}
  set start $index
  while true {
    set target [$win search -elide \u005d $start end]
    if {$target eq ""} then {
      return false 
    }
    set txt [$win get $index $target+1chars]
    if {[info complete [string map $map $txt]]} then {
      extendSel $win  $target+1chars
      return true
    }
    set start $target+1chars
  }
}

proc DoubleClick::closeParenIndex {win {index insert}} {
  # at opening of ( ... )
  lappend map \" " " \{ " " \} " " ( \{ ) \}
  set start $index
  while true {
    set target [$win search -elide ) $start end]
    if {$target eq ""} break
    set txt [$win get $index $target+1chars]
    if {[info complete [string map $map $txt]]} then {
      return $target
    }
    set start $target+1chars
  }
}

proc DoubleClick::findCloseParen {win {index insert}} {
  # at opening of ( ... )
  set endIdx [closeParenIndex $win $index]
  if {$endIdx eq ""} then {
    return false
  }
  $win tag add sel $index $endIdx+1char
  return true
}

proc DoubleClick::findOpenParen {win {index insert}} {
  # ( ... ) at closing
  set map [list \{ " " \} " " \" " " ( \{ ) \}]
  set top [startOfExpression $win $index]
  set start $index
  while true {
    set start [$win search -elide -backward \( $start $top]
    if {$start eq ""} break
    if {[backslashed $win $start]} continue
    set target [closeParenIndex $win $start]
    if {[$win compare $target >= $index]} then {
      $win tag add sel $start $index+1char
      return true
    }
  }
  return false
}

proc DoubleClick::findIntlCloseQuote {win {index insert}} {
  # „international“
  variable quotes
  set open [$win get $index]
  set close [dict get $quotes $open]
  set target [$win search -elide $close $index end]
  if {$target eq ""} then {
    return false
  }
  extendSel $win $target+1chars
  return true
}

proc DoubleClick::findIntlOpenQuote {win {index insert}} {
  # „international“
  variable quotes
  set map [concat {*}[lmap {a b} $quotes {list $b $a}]]
  set closeChar [$win get $index]
  set openQuote [dict get $map $closeChar]
  set startIdx [$win search -elide -backward $openQuote $index 1.0]
  if {$startIdx eq ""} then {
    return false
  }
  $win tag add sel $startIdx $index+1char
  return true
}

proc DoubleClick::documentIntQuotes win {
  # which national quotes used in doc? E. g. "de"
  set idx [$win search -elide -regexp {[»«›‹„‚“‘]} 1.0 end]
  switch -exact -- [$win get $idx] {
    „ - ‚ {
      return de
    }
    “ - ‘ {
      return en
    }
    » - › {
      return fr
    }
    « - ‹ {
      return ch
    }
    default {
      return C
    }
  }
}

proc DoubleClick::findMatchIntlQuote {win {index insert}} {
  set char [$win get $index]
  set closingQuotes { “ ‘ ” ’ » › « ‹ }
  set lang [documentIntQuotes $win]
  if {$char in { “ ‘ }} then {
    if {$lang eq "en"} then {
      set dir forw
    } else {
      set dir back
    }
  } elseif {$char in { » › }} then {
    if {$lang eq "fr"} then {
      set dir forw
    } else {
      set dir back
    }
  } elseif {$char in { « ‹ }} then {
    if {$lang eq "ch"} then {
      set dir forw
    } else {
      set dir back
    }
  } elseif {$char in { ’ ” }} then {
    set dir back
  } else {
    set dir forw
  }
  if {$dir eq "forw"} then {
    findIntlCloseQuote $win $index
  } else {
    findIntlOpenQuote $win $index
  }
}

proc DoubleClick::findCloseAngleExcl {win {index insert}} {
  # <!DOCTYPE ... >
  set target [$win search -elide > $index end]
  if {$target eq ""} then {
    return false
  }
  extendSel $win $target+1chars
  return true
}

proc DoubleClick::findCloseAngleQuest {win {index insert}} {
  # <?xml version="1.0" encoding="UTF-8"?>
  set target [$win search -elide ?> $index end]
  if {$target eq ""} then {
    return false
  }
  extendSel $win $target+2chars
  return true
}

proc DoubleClick::findEndOfComment {win {index insert}} {
  # <!-- comment -->
  set target [$win search -elide -- --> $index end]
  if {$target eq ""} then {
    return false
  }
  extendSel $win  $target+3chars
  return true
}

proc DoubleClick::findEndOfCdata {win {index insert}} {
  # <![CDATA[ ... ]]>
  set target [$win search -elide {]]>} $index end]
  if {$target eq ""} then {
    return false
  }
  extendSel $win $target+3chars
  return true
}

proc DoubleClick::findNestedCloseAngle {win {index insert}} {
  # <<ContextMenu>>
  set start $index
  lappend map \u007b " " \u007d ""  \u0022 " " < \u007b > \u007d
  while true {
    set target [$win search -elide > $start end]
    if {$target eq ""} then {
      return false
    }
    set txt [$win get $index $target+1chars]
    set txt [string map $map $txt]
    if {[info complete $txt]} then {
      extendSel $win $target+1chars
      return true
    }
    set start $target+1chars
  }
}

proc DoubleClick::insideComment {win {index insert}} {
  set start [$win search -elide -backwards <!-- $index 1.0]
  if {$start eq ""} then {
    return false
  }
  set end [$win search -elide -- --> $start end]
  if {$end eq ""} then {
    return false
  }
  if {[$win compare $end+3chars > $index]} then {
    return true
  } else {
    return false
  }
}

proc DoubleClick::insideCdata {win {index insert}} {
  set start [$win search -elide -backwards {<![CDATA[} $index 1.0]
  if {$start eq ""} then {
    return false
  }
  set end [$win search -elide {]]>} $start end]
  if {$end eq ""} then {
    return false
  }
  if {[$win compare $end+3chars > $index]} then {
    return true
  } else {
    return false
  }
}

proc DoubleClick::insideQuestionMarkup {win {index insert}} {
  set start [$win search -elide -backwards <? $index 1.0]
  if {$start eq ""} then {
    return false
  }
  set end [$win search -elide ?> $start end]
  if {$end eq ""} then {
    return false
  }
  if {[$win compare $end+2chars > $index]} then {
    return true
  } else {
    return false
  }
}

proc DoubleClick::findEndOfEmptyTag {win {index insert}} {
  set pat {<[[:alnum:]:]+[^>]*/>}
  set target\
    [$win search -elide -regexp -nolinestop -count c $pat $index end]
  if {$target eq ""} then {
    return false
  } elseif {[$win compare $index == $target]} then {
    extendSel $win "$index + $c chars"
    return true
  } else {
    return false
  }
}

proc DoubleClick::closingTagIndex {win {from insert}} {
  # find tagname
  $win search -count len -regexp -elide -- {\m[[:alnum:]]+\M} $from end 
  set tagName [$win get "$from + 1 char" "$from + $len chars + 1 char"]
  set pat [subst -nocommand -nobackslash {</?$tagName\M[^>]*>}]
  set indices [$win search -regexp -elide -all -- $pat $from end]
  set level 0
  foreach index $indices {
    if {
      [insideComment $win $index] ||
      [insideQuestionMarkup $win $index] ||
      [insideCdata $win $index]
    } continue
    if {[$win get $index+1char] eq "/"} then {
      incr level -1
    } else {
      incr level
    }
    if {$level <= 0} then {
      return $index
    }
  }
}

proc DoubleClick::findClosingTag {win {index insert}} {
  set tagIdx [closingTagIndex $win $index]
  if {$tagIdx ne ""} then {
    set endIdx [$win search -elide > $tagIdx end]
    if {$endIdx ne ""} then {
      $win tag add sel $index $endIdx+1char
      return true
    } else {
      return false
    }
  } else {
    return false
  }
}

proc DoubleClick::openingTagIndex {win {index insert}} {
  $win search -count len -regexp -- {\m[[:alnum:]]+\M} $index end
  set name [$win get "$index + 2 chars" "$index + 2 chars + $len chars"]
  set pat [subst -nocommand -nobackslashes {<$name\M[^>]*>}]
  set start $index
  while true {
    set start [$win search -regexp -elide -backwards -- $pat $start 1.0]
    if {
      [insideComment $win $start] ||
      [insideQuestionMarkup $win $start] ||
      [insideCdata $win $start]
    } continue
    if {[$win compare [closingTagIndex $win $start] >= $index]} break
  }
  set start
}

proc DoubleClick::findOpeningTag {win {index insert}} {
  set start [openingTagIndex $win $index]
  if {$start eq ""} then {
    return false
  } else {
    set end [$win search -elide > $index end]
    if {$end eq ""} then {
      return false
    } else {
      $win tag add sel $start $end+1char
      return true
    }
  }
}

proc DoubleClick::findCloseAngle {win {index insert}} {
  set cdata <!\u005bCDATA\u005b
  set comment <!--
  set excl <!
  set quest <?
  set close </
  set angle <
  if {[$win get $index $index+9chars] eq $cdata} then {
    # <![CDATA[ ... ]]>
    findEndOfCdata $win $index
  } elseif {[$win get $index $index+4chars] eq $comment} then {
    # <!-- ... -->
    findEndOfComment $win $index
  } elseif {[$win get $index $index+2chars] eq $excl} then {
    # <! ... >
    findCloseAngleExcl $win $index
  } elseif {[$win get $index $index+2chars] eq $quest} then {
    # <? ... ?>
    findCloseAngleQuest $win $index
  } elseif {[$win get $index $index+2chars] eq $close} then {
    # </
    findOpeningTag $win $index
  } elseif {[$win get $index] eq $angle} then {
    # <...
    if {[findEndOfEmptyTag $win $index]} then {
      # <br />
      return true
    } elseif {[findClosingTag $win $index]} then {
      # <a> ... </a>
      return true
    } else {
      # <<ContextMenu>>
      findNestedCloseAngle $win $index
    }
  } else {
    return false
  }
}

proc DoubleClick::findWidgetName {win {index insert}} {
  set prev [$win search -backwards -regexp {[^[:alnum:].]|^} $index 1.0]
  if {$prev eq ""} then {
    set prev [$win index $index]
  } elseif {[$win get $prev] ne "."} then {
    set prev [$win index $prev+1chars]
  }
  if {
    [$win get $prev] ne "." || 
    ![string is alnum [$win get $prev+1chars]]
  } then {
    return false
  }
  set end [$win search -regexp {[^[:alnum:].]|\n} $prev end]
  if {[$win compare $index > $end]} then {
    return false
  }
  $win tag add sel $prev
  extendSel $win $end
  return true
}

proc DoubleClick::findStringName {win {index insert}} {
  if {[$win get $index] ne "$"} then {
    return false
  }
  set endIdx $index+1char
  if {[$win get $endIdx $endIdx+2chars] eq "::"} then {
    set endIdx $endIdx+2chars
  }
  if {[$win get $endIdx] eq "\u7b"} then {
    set endIdx [$win search \u7d $endIdx end]
    if {$endIdx eq ""} then {
      return false
    } else {
      set endIdx $endIdx+1char
    }
  } elseif {[string is alnum [$win get $endIdx]]} then {
    set endIdx [$win search -regexp {\M} $endIdx end]
  }
  if {[$win get $endIdx] eq "("} then {
    set endIdx [$win search ")" $endIdx end]
    if {$endIdx eq ""} then {
      return false
    }
    set endIdx $endIdx+1char
  }
  extendSel $win $endIdx
  return true
}

proc DoubleClick::processChar {win {index insert}} {
  if {[$win get $index] eq "\n"} then {
    set index $index-1char
  }
  set char [$win get $index]
  switch -exact -- $char {
    < {
      findCloseAngle $win $index
    }
    „ - “ - ‚ - ‘ - “ - ” - ‘ - ’ - » - « - › - ‹ {
      findMatchIntlQuote $win $index
      # findIntlCloseQuote $win $index
    }
    default {
      if {
        [findStringName $win $index] ||
        [findWidgetName $win $index] ||
        [findUrl $win $index] ||
        [findTagComment $win $index] || 
        [findCSScomment $win $index] ||
        [findEntity $win $index]
      } then {
        return true
      } else {
        if {[backslashed $win $index]} then {
          return false
        }
        switch -exact -- $char {
          \u0022 {
            # " ... "
            findCloseDquote $win $index
          }
          ' {
            findCloseSquote $win $index
          }
          ( {
            findCloseParen $win $index
          }
          ) {
            findOpenParen $win $index
          }
          \u007b {
            # start of { ... }
            findCloseBrace $win $index
          }
          \u007d {
            # end of { ... } 
            findOpenBrace $win $index
          }
          \u005b {
            # [ ... ]
            findCloseBracket $win $index
          }
          default {
            return false
          }
        }
      }
    }
  }
}

proc DoubleClick::sentenceStartIndex {win {index insert}} {
  set startPat {(?:[.:?!](?:[›‹‘’][[:punct:]]*)?[»«“”]?)}
  set lfIdx [$win search -backwards -elide \n\n $index 1.0]
  set punctIdx\
    [$win search -backwards -elide -regexp -count len $startPat $index 1.0]
  if {$punctIdx eq ""} then {
    lassign {1.0 0} punctIdx len
  }
  if {"$lfIdx$punctIdx" eq ""} then {
    set target 1.0
  } elseif {$lfIdx eq ""} then {
    set target [$win index "$punctIdx + $len chars"]
  } elseif {[$win compare $lfIdx > $punctIdx]} then {
    set target $lfIdx
  } else {
    set target [$win index "$punctIdx + $len chars"]
  }
  set tagPat {\s*(?:(?:<!--.*?-->|<!\[CDATA\[.*?\]\]>|<[^>]+>)\s*)+}
  set nextIdx [$win search -elide -regexp -count len $tagPat $target end]
  if {$nextIdx eq $target} then {
    set target [$win index "$target + $len chars"]
  } else {
    set nextIdx [$win search -regexp -elide {\S} $target end]
    if {$nextIdx ne ""} then {
      set target $nextIdx
    } else {
      set target [$win index $index]
    }
  }
  set target
}

proc DoubleClick::sentenceEndIndex {win {index insert}} {
  set endPat {(?:[….:?!](?:[›‹‘’][[:punct:]]*)?[»«“”]?)}
  set lfIdx [$win search -elide \n\n $index end]
  set punctIdx [$win search -elide -regexp -count len $endPat $index end]
  if {$lfIdx eq ""} then {
    if {$punctIdx eq ""} then {
      set target [$win index end-1char]
    } else {
      set target $punctIdx
    }
  } else {
    if {$punctIdx eq ""} then {
      set target $lfIdx
    } elseif {[$win compare $punctIdx < $lfIdx]} then {
      set target [$win index "$punctIdx + $len chars"]
    } else {
      set target $lfIdx
    }
  }
  while {[$win compare $target > 1.0] &&
         [string is space -strict [$win get "$target - 1 char"]]} {
    set target [$win index $target-1chars]
  }
  set tagPat {\s*(<[^>]*>\s*)+}
  set prevIdx\
    [$win search -elide -regexp -backwards -nolinestop $tagPat $target $index]
  if {$prevIdx ne ""} then {
    set target $prevIdx
  }
  while {[string is punct [$win get $target]]} {
    set target [$win index $target+1char]
  }
  set target
}

proc DoubleClick::selectSentence {win {clickpoint insert}} {
  set start [sentenceStartIndex $win]
  set end [sentenceEndIndex $win]
  $win tag remove sel 1.0 end
  $win tag add sel $start $end
}

proc DoubleClick::extendSentenceRange {win {clickpoint insert}} {
  variable index
  if {[$win compare $clickpoint < $index]} then {
    set start [sentenceStartIndex $win $clickpoint]
    set end [sentenceEndIndex $win $index]
  } else {
    set start [sentenceStartIndex $win $index]
    set end [sentenceEndIndex $win $clickpoint]
  }
  $win tag remove sel 1.0 end
  $win tag add sel $start $end
}

proc DoubleClick::bind+ {win event script} {
  bind $win $event +$script
}

proc DoubleClick::strCat args {join $args ""}

proc DoubleClick::bindDoubleClick {tag {sentence yes}} {
  foreach pat {
    <Button-1>
    <Double-Button-1>
    <Shift-Button-1>
    <B1-Motion>
    <Triple-Button-1>
  } {
    bind $tag $pat ""
  }
  bind $tag <Button-1> {
    set ::DoubleClick::click single
  }
  bind $tag <Double-Button-1> {
    %W mark set insert [%W index @%x,%y]
    set ::DoubleClick::index [%W index insert]
    set ::DoubleClick::click double
    if {[::DoubleClick::processChar %W insert]} break
  }
  bind $tag <Shift-Button-1> {
    if {
      $DoubleClick::click eq "double" &&
      [::DoubleClick::processChar %W @%x,%y]
    } then {
      %W mark set insert $::DoubleClick::index
      %W tag add sel insert sel.last
      break
    }
  }
  bind $tag <B1-Motion> {
    if {$DoubleClick::click eq "double" &&
      [::DoubleClick::processChar %W @%x,%y]} then {
      %W mark set insert $::DoubleClick::index
      %W tag add sel insert sel.last
      break
    }
  }
  if {$sentence} then {
    bind $tag <Triple-Button-1> {
      if {[%W cget -wrap] eq "word"} then {
        set ::DoubleClick::click triple
        DoubleClick::selectSentence %W @%x,%y
        break
      }
    }
    bind+ $tag <Shift-Button-1> {
      if {[%W cget -wrap] eq "word"} then {
        if {$DoubleClick::click eq "triple"} then {
          ::DoubleClick::extendSentenceRange %W @%x,%y
          break
        }
      }
    }
    bind+ $tag <B1-Motion> {
      if {[%W cget -wrap] eq "word"} then {
        if {$DoubleClick::click eq "triple"} then {
          ::DoubleClick::extendSentenceRange %W @%x,%y
          break
        }
      }
    }
  }
}

namespace eval DoubleClick namespace export bindDoubleClick

namespace import DoubleClick::*
======