hl_tcl.tcl
hl_tcl.tcl

hl_tcl.tcl

  • hl_tcl.tcl
  • Common data
  • STATIC highlighting
    • AuxEnding my::AuxEnding : Auxiliary procedure to process the ending comments in the line. kName - variable's name for 'k' lineName - variable's name for 'line' iName - variable's name for 'i' See also: HighlightLine
    • NotEscaped my::NotEscaped : Checks if a character escaped in a line. line - line i - the character's position in 'line' Returns "1" if the character not escaped.
    • RemoveTags my::RemoveTags : Removes tags in text. txt - text widget's path from - starting index to - ending index
    • HighlightCmd my::HighlightCmd : Highlights Tcl/Tk commands. txt - text widget's path line - line to be highlighted ln - line number pri - column number to highlighted from i - current position in 'line'
    • HighlightStr my::HighlightStr : Highlights strings. txt - text widget's path p1 - starting index of the string in 'txt' p2 - ending index of the string in 'txt'
    • FirstQtd my::FirstQtd : Searches the quote characters in line. lineName - variable's name for 'line' iName - variable's name for 'i' currQtd - yes, if searching inside the quoted Returns "yes" if a quote character was found.
    • HighlightComment my::HighlightComment : Highlights comments. txt - text widget's path line - current line ln - line's number k - comment's starting position in line
    • HighlightLine my::HighlightLine : Highlights a line in text. txt - text widget's path ln - line's number prevQtd - flag of "being quoted" from the previous line
    • HighlightAll my::HighlightAll : Highlights all of a text. txt - text widget's path Makes a coroutine from this. See also: CoroHighlightAll let them work one by one:
    • CoroHighlightAll my::CoroHighlightAll : Highlights all of a text as a coroutine. txt - text widget's path See also: HighlightAll
    • BindToEvent my::BindToEvent : Binds an event on a widget to a command. w - the widget's path event - the event args - the command
  • DYNAMIC highlighting
    • CountQSH my::CountQSH : Counts quotes, slashes, hashes in a line txt - text widget's path ln - line's index
    • IsCurline my::IsCurline : Sets / gets "highlight a current line" flag for a text. txt - the text's path flag - the flag
    • ShowCurrentLine my::ShowCurrentLine : Shows the current line. txt - text widget's path doit - if yes, forces updating current line's background Returns a current position of cursor.
    • MemPos1 my::MemPos1 : Checks and sets the cursor's width, depending on its position. txt - text widget's path donorm - if yes, forces "normal" cursor K - key (%K of bind) s - state (%s of bind) This fixes an issue with text cursor: less width at 0th column.
    • MemPos my::MemPos : Remembers the state of current line. txt - text widget's path doit - argument for ShowCurrentLine See also: ShowCurrentLine
    • RunCoroAfterIdle my::RunCoroAfterIdle : Runs a "modified" corotine after idle.
    • Modified my::Modified : Handles modifications of text. txt - text widget's path Makes a coroutine from this. See also: CoroModified
    • CoroRun my::CoroRun
    • CoroModified my::CoroModified : Handles modifications of text. txt - text widget's path i1 - 1st index of changes i2 - 2nd index of changes args - arguments for a command called after the modifications See also: Modified
    • InRange my::InRange : Checks if a text position is in a range of text positions. p1 - 1st position of range p2 - 2nd position of range l - line position to check (or 'l.c' if 'c' not set) c - column position to check
    • SearchTag my::SearchTag : Searches a position in tag ranges. tagpos - tag position ranges l1 - the position to find Returns a found range's index of -1 if not found.
    • LineState my::LineState : Gets an initial state of line. txt - text widget's path tSTR - ranges of string tags tCMN - ranges of comment tags l1 - the line's index Returns: 0 if no tags for the line; 1 if the line is a string's continuation; -1 if the line is a comment's continuation.
  • HEROIC EFFORTS to highlight the matching brackets
    • MergePosList my::MergePosList : Merges lists of numbers that are not-coinciding and sorted. none - a number to be not allowed in the lists (e.g. less than minimal) args - list of the lists to be merged Returns a list of pairs: index of list + item of list.
    • CountChar my::CountChar : Counts a character in a string. str - the string ch - the character plistName - variable name for a list of positions of *ch* escaped - true, if the character is escaped. Returns a number of any occurences of character *ch* in string *str* if the character is escaped, but if it is not escaped, only non-escaped characters are counted.
    • Escaped my::Escaped : Checks if a character is escaped in a string. line - the string curpos - position of the character in the line Returns 1 if the character is escaped in the string.
    • MatchedBrackets my::MatchedBrackets : Finds a match of characters (dchar for schar). w - text widget's path inplist - list of strings where to find a match curpos - position of schar in nl.nc form where nl=1.., nc=0.. schar - source character dchar - destination character dir - search direction: 1 to the end, -1 to the beginning of list
    • HighlightBrackets my::HighlightBrackets : Highlights matching brackets if any. w - text widget's path
  • INTERFACE procedures
    • hl_readonly hl_tcl::hl_readonly : Makes the text widget be readonly or gets its 'read-only' state. txt - text widget's path ro - flag "the text widget is readonly" com2 - command to be called at viewing and after changes If 'ro' argument is omitted, returns the widget's 'read-only' state.
    • hl_init hl_tcl::hl_init : Initializes highlighting. txt - text widget's path args - dict of options The 'args' options include: -- - means that only args' options will be initialized (defaults skipped) -dark - flag "the text widget has dark background" -readonly - flag "read-only" -optRE - flag "use of RE to highlight options" -multiline - flag "allowed multi-line strings" -cmd - command to watch editing/viewing -cmdpos - command to watch cursor positioning -colors - list of colors as set in hl_tcl::hl_colorNames -font - attributes of font -seen - lines seen at start -keywords - additional commands to highlight (as Tk ones) This procedure has to be called before writing a text in the text widget. See also: hl_colorNames
    • hl_text hl_tcl::hl_text : Highlights Tcl code of a text widget. txt - text widget's path
    • hl_all hl_tcl::hl_all : Updates ("rehighlights") all highlighted and existing text widgets. args - dict of options See also: hl_init
    • hl_colorNames hl_tcl::hl_colorNames : Returns a list of color names for syntax highlighting.
    • hl_colors hl_tcl::hl_colors : Gets/sets the main colors for highlighting (except for "curr.line"). txt - text widget's path or {} or an index of default colors dark - flag "dark scheme" args - a list of colors to set for *txt* Returns a list of colors for COM COMTK STR VAR CMN PROC OPT BRAC \
    • hl_line hl_tcl::hl_line : Updates a current line's highlighting. txt - text's path
    • addingColors hl_tcl::addingColors : Sets/gets colors for a text syntax highlighting. dark - yes, if the current theme is dark txt - path to the text or {} cs - color scheme If *txt* omitted, returns a list of resting colors. The resting colors are: - current line's background - #TODO and #! comment's foreground
    • hl_commands hl_tcl::hl_commands : Lists all Tcl/Tk commands registered here.
    • iscurline hl_tcl::iscurline : Sets / gets "highlight a current line" flag for a text. txt - the text's path flag - the flag
    • isdone hl_tcl::isdone : Checks if the highlighting of the text is done. txt - text's path
    • clearup hl_tcl::clearup : Clears data related to text (esp. at deleting it). txt - text's path
    • cget hl_tcl::cget : Gets a highlighting option's value. txt - text's path opt - option's name
    • configure hl_tcl::configure : Sets a highlighting option's value. txt - text's path opt - option's name val - option's value
  • Helpers
    • OptName hl_tcl::OptName : Gets hl_tcl option's name. txt - text's path opt - option
  • EOF
###########################################################
# Name:    hl_tcl.tcl
# Author:  Alex Plotnikov  (aplsimple@gmail.com)
# Date:    06/16/2021
# Brief:   Handles highlighting Tcl code.
# License: MIT.
###########################################################

package provide hl_tcl 1.2.2

# ______________________ Common data ____________________ #

namespace eval ::hl_tcl {

  namespace eval my {

    variable data;  array set data [list]

    # Tcl commands
    set data(PROC_TCL) [lsort [list \
      return proc method self my coroutine yield yieldto constructor destructor \
      break continue namespace test \
      oo::define oo::class oo::objdefine oo::object oo::abstract
    ]]
    set data(CMD_TCL) [lsort [list \
      set incr if else elseif string expr list lindex lrange llength lappend \
      lreplace lsearch lassign append split info array dict foreach for while \
      switch default linsert lsort lset lmap lrepeat catch variable \
      concat format scan regexp regsub upvar uplevel try finally throw read eval \
      after update error global puts file chan open close eof seek flush mixin \
      msgcat gets rename glob fconfigure fblocked fcopy cd pwd mathfunc then \
      mathop apply fileevent unset join next exec refchan package source \
      exit vwait binary lreverse registry auto_execok subst encoding load \
      auto_load tell auto_mkindex memory trace time clock timerate auto_qualify \
      auto_reset socket bgerror oo::copy superclass unload history tailcall \
      interp parray pid transchan nextto unknown dde pkg_mkIndex zlib auto_import \
      coroinject coroprobe const ledit lpop lremove lseq tcl::process \
      readFile writeFile foreachLine pkg::create tcl::prefix \
      http::config http::geturl http::formatQuery http::reset http::wait \
      http::status http::size http::code http::ncode http::meta http::data \
      http::error http::cleanup http::register http::unregister * \
    ]]

    # Ttk commands
    set data(CMD_TTK) [list \
      ttk::button ttk::frame ttk::label ttk::entry ttk::checkbutton \
      ttk::radiobutton ttk::combobox ttk::labelframe ttk::scrollbar \
      tk_optionMenu ttk::menubutton ttk::style ttk::notebook ttk::panedwindow \
      ttk::separator ttk::progressbar ttk::scale ttk::sizegrip ttk::spinbox \
      ttk::treeview ttk::intro ttk::widget tk_focusNext tk_getOpenFile \
    ]

    # Tk commands
    set data(CMD_TK2) [list \
      tk_popup tk tkwait tkerror tk_setPalette tk_textCut tk_textCopy tk_bisque \
      tk_chooseDirectory tk_textPaste ttk_vsapi tk_focusPrev tk_messageBox \
      tk_focusFollowsMouse tk_getSaveFile tk_menuSetFocus tk_dialog tk_chooseColor \
    ]

    # Tk/ttk commands united
    set data(CMD_TK) [concat $data(CMD_TTK) $data(CMD_TK2) [list \
      button entry checkbutton radiobutton label menubutton menu wm winfo bind \
      grid pack event bell text canvas frame listbox grab scale scrollbar \
      labelframe focus font bindtags image selection toplevel destroy \
      option options spinbox bitmap photo keysyms send lower clipboard colors \
      console message cursors panedwindow place raise \
    ]]

    # allowed edges of string (as one and only)
    set data(S_LEFT) [list \{ \[]
    set data(S_RIGHT) [list \} \]]
    # allowed edges of string (as one or both)
    set data(S_SPACE) [list {} { } \t {;}]
    set data(S_SPACE2) [concat $data(S_SPACE) [list \{]]
    set data(S_BOTH) [concat $data(S_SPACE) [list \" \}]]

#    set data(RE0) {(^|[\{\}\[;]+)\s*([:\w*]+)(\s|\]|\}|\\|$|;)}
#    set data(RE0) {(^|[\{\}\[;]+)\s*([:\w*]+)(\s|\]|\}|\\|$|;)?}
    set data(RE0) {(^|[\{\}\[;]+)\s*([:\w*]+)(\s|\]|\}|\\|$|;){0}} ;# test: pwd;pwd;pwd
    set data(RE1) {([\{\}\[;])+\s*([:\w*]+)(\s|\]|\}|\\|$)}
    set data(RE5) {(^|[^\\])(\[|\]|\$|\{|\})}
    set data(RETODO) {^\s*#\s*(!|TODO)}

    set data(LBR) {\{(\[\"}
    set data(RBR) {\})\]\"}

    # default syntax colors arrays (for a light & black themes)
    #  COM     COMTK    STR      VAR     CMN     PROC    OPT    BRAC
    set data(SYNTAXCOLORS,0) {
      {#121212 #000000 #0c560c #4A181B #606060 #923B23 #463e11 #FF0000}
      {#e0e0e0 #efefef #b1fabd #eebabf #888888 #ffa500 #c2ba8d #ff33ff}
    }
    set data(SYNTAXCOLORS,1) {
      {#923B23 #7d1c00 #035103 #4A181B #4b5d50 #ca14ca #463e11 #FF0000}
      {#ffa500 #ff7e00 #90ee90 #f1b479 #76a396 #fe6efe #b9b96e #ff33ff}
    }
    set data(SYNTAXCOLORS,2) {
      {#3a6797 #134070 #8b2a0e #1b1baa #4b5d50 #ca14ca #653760 #FF0000}
      {#95c2f2 #73a0d0 #ffc27e #a9a9f7 #76a396 #fe6efe #e2b4dd #ff33ff}
    }
    set data(SYNTAXCOLORS,3) {
      {#2b6b2b #0b4b0b #8e0e8e #004080 #606060 #8a3407 #463e11 #FF0000}
      {#aad5ab #86c686 #ff86ff #96c5f8 #888888 #fab481 #b1a97c #ff33ff}
    }
  }
}

# _________________________ STATIC highlighting _________________________ #

proc ::hl_tcl::my::AuxEnding {kName lineName iName} {
  # Auxiliary procedure to process the ending comments in the line.
  #   kName - variable's name for 'k'
  #   lineName - variable's name for 'line'
  #   iName - variable's name for 'i'
  # See also: HighlightLine

  upvar 1 $kName k $lineName line $iName i
  if {[set k [string first # $line $i]]>-1 && \
  [string index [string trimleft $line] 0] eq {#}} {
    return 1
  }
  # not found a full line comment => try to find "good" ending comments i.e. ";# ..."
  # at that even proper comments like
  #   if {cond} { #...
  #   }
  # are skipped as "bad"
  set k [lindex [regexp -inline -indices {;\s*#} [string range $line $i end]] 0 0]
  if {$k eq {}} {
    set k -1
    return 0
  }
  set k [expr {$k+$i+1}]
  return 1
}
#_______________________

proc ::hl_tcl::my::NotEscaped {line i} {
  # Checks if a character escaped in a line.
  #   line - line
  #   i - the character's position in 'line'
  # Returns "1" if the character not escaped.

  set cntq 0
  while {$i>0} {
    if {[string index $line [incr i -1]] ne "\\"} {
      return [expr {($cntq%2)==0}]
    }
    incr cntq
  }
  expr {($cntq%2)==0}
}
#_______________________

proc ::hl_tcl::my::RemoveTags {txt from to} {
  # Removes tags in text.
  #   txt - text widget's path
  #   from - starting index
  #   to - ending index

  foreach tag {tagCOM tagCOMTK tagSTR tagVAR tagCMN tagCMN2 tagPROC tagOPT} {
    $txt tag remove $tag $from $to
  }
}
#_______________________

proc ::hl_tcl::my::HighlightCmd {txt line ln pri i} {
  # Highlights Tcl/Tk commands.
  #   txt - text widget's path
  #   line - line to be highlighted
  #   ln - line number
  #   pri - column number to highlighted from
  #   i - current position in 'line'

  variable data
  $txt tag add tagSTD "$ln.$pri" "$ln.$i +1 chars"
  if {$pri} {
    incr pri -1
    set RE $data(RE1)
  } else {
    set RE $data(RE0)
  }
  set st [string range $line $pri $i-1]
  set lcom [regexp -inline -all -indices $RE $st]
  # commands
  foreach {- - lc -} $lcom {
    lassign $lc i1 i2
    set c [string trim [string range $st $i1 $i2] "\{\}\[;\t "]
    set ik [expr {$i2-$i1+1-[string length $c]}]
    if {$c ne {}} {
      incr i1 $ik
      incr i2
      if {[lsearch -exact -sorted $data(CMD_TCL) $c]>-1} {
        $txt tag add tagCOM "$ln.$pri +$i1 char" "$ln.$pri +$i2 char"
      } elseif {[lsearch -exact -sorted $data(PROC_TCL) $c]>-1} {
        if {$c eq {namespace} &&
        ![regexp {^namespace[\s]+eval([\s]|$)+} [string range $st $i1 end]]} {
          set tag tagCOM ;# let "namespace eval" only be highlighted as proc/return
        } else {
          set tag tagPROC
        }
        $txt tag add $tag "$ln.$pri +$i1 char" "$ln.$pri +$i2 char"
      } elseif {[lsearch -exact -sorted $data(CMD_TK_EXP) $c]>-1} {
        $txt tag add tagCOMTK "$ln.$pri +$i1 char" "$ln.$pri +$i2 char"
      }
    }
  }
  # $variables:
  set dlist [list]
  set slen [expr {[string length $st]-1}]
  set cnt [CountChar $st \$ dlist no]
  foreach dl $dlist {
    if { [string index $st $dl+1] eq "\{" } {
      if {[set br2 [string first \} $st $dl+2]]!=-1} {
        $txt tag add tagVAR "$ln.$pri +$dl char" "$ln.$pri +[incr br2] char"
      }
      continue
    }
    for {set i [set dl2 $dl]} {$i<$slen} {} {
      incr i
      set ch [string index $st $i]
      if {[string is wordchar $ch] || $ch eq {:}} {
        set dl2 $i
        continue
      } elseif {$ch eq {(}} {
        if {[set br2 [string first {)} $st $i+1]]>-1} {
          set dl2 $br2
        }
      }
      break
    }
    if {$dl2>$dl} {
      $txt tag add tagVAR "$ln.$pri +$dl char" "$ln.$pri +[incr dl2] char"
    }
  }
  # -options
  set dl -1
  while {[set dl [string first - $st [incr dl]]]>-1} {
    if {[string index $st $dl-1] ni $data(S_SPACE2)} continue
    set idx1 "$ln.$pri +$dl char"
    if {[string index $st $dl+1] eq {-}} {
      incr dl ;# for --longoption
    }
    set i $dl
    set ch [string index $st $i+1]
    if {![string is alpha -strict $ch]} { ;# || ![string is ascii -strict $ch]
      continue ;# first, a Latin letter
    }
    set dl2 -1
    while {$i<$slen} {
      incr i
      set ch [string index $st $i]
      if {![string is wordchar $ch] && $ch ne {-}} break
      set dl2 $i
    }
    if {$dl2>-1} {
      $txt tag add tagOPT $idx1 "$ln.$pri +[incr dl2] char"
      set dl $dl2
    }
  }
}
#_______________________

proc ::hl_tcl::my::HighlightStr {txt p1 p2} {
  # Highlights strings.
  #   txt - text widget's path
  #   p1 - starting index of the string in 'txt'
  #   p2 - ending index of the string in 'txt'

  variable data
  set p1 [$txt index $p1]
  set p2 [$txt index $p2]
  $txt tag add tagSTR $p1 $p2
  set st [$txt get $p1 $p2]
  set lcom [regexp -inline -all -indices $data(RE5) $st]
  foreach {lc g1 g2} $lcom {
    lassign $lc i1 i2
    incr i2
    while {$i1<$i2} {
      incr i1
      if {[string first [string index $st $i1] "\[\]\$\{\}"]>-1} {
        set i12 [expr {$i1+1}]
        $txt tag add tagVAR "$p1 +$i1 char" "$p1 +$i12 char"
      }
    }
  }
}
#_______________________

proc ::hl_tcl::my::FirstQtd {lineName iName currQtd} {
  # Searches the quote characters in line.
  #   lineName - variable's name for 'line'
  #   iName - variable's name for 'i'
  #   currQtd - yes, if searching inside the quoted
  # Returns "yes" if a quote character was found.

  variable data
  upvar 1 $lineName line $iName i
  while {1} {
    if {[set i [string first \" $line $i]]==-1} {return no}
    if {[NotEscaped $line $i]} {
      if {$currQtd||$i==0} {return yes}
      set i1 [expr {$i-1}]
      set i2 [expr {$i+1}]
      if {[NotEscaped $line $i1]} {
        set c1 [string index $line $i1]  ;# check the string ends
        set c2 [string index $line $i2]
        # not needed: $c1 in $data(S_BOTH) && $c2 ni $data(S_BOTH) ||
        if {$c1 in $data(S_LEFT) && $c2 ni $data(S_RIGHT)
        || $c1 ni $data(S_LEFT) && $c2 in $data(S_RIGHT)} {
          return yes
        }
        # last reverence: for braced expression
        set i1 $i
        while {$i1>0} {
          set c1 [string index $line $i1-1]
          set c2 [string index $line $i1]
          if {$c1 in $data(S_SPACE)} {return [expr { $c2 ne "\{" }]}
          incr i1 -1
        }
        return no
      }
      return yes
    }
    incr i
  }
}
#_______________________

proc ::hl_tcl::my::HighlightComment {txt line ln k} {
  # Highlights comments.
  #   txt - text widget's path
  #   line - current line
  #   ln - line's number
  #   k - comment's starting position in line

  variable data
  set stcom [string range $line $k end]
  if {[regexp $data(RETODO) $stcom]} {
    $txt tag add tagCMN2 $ln.$k $ln.end  ;# "!" and TODO comments
  } else {
    $txt tag add tagCMN $ln.$k $ln.end
  }
}
#_______________________

proc ::hl_tcl::my::HighlightLine {txt ln prevQtd} {
  # Highlights a line in text.
  #   txt - text widget's path
  #   ln - line's number
  #   prevQtd - flag of "being quoted" from the previous line

  variable data
  if {$data(ISPLAINCOM,$txt)} {
    # plain highlighting: for the current line
    set res [{*}$data(PLAINCOM,$txt) $txt $ln $prevQtd]
    if {$res in {-1 0 1}} {
      return $res ;# to be continued in a next line with prevQtd
    }
    # if the line was highlighted okay, skip the highlighting as Tcl code
    if {$res} {return 0}
    # otherwise highlight it below as Tcl code
  }
  set line [$txt get $ln.0 $ln.end]
  if {$prevQtd==-1} {  ;# comments continued
    HighlightComment $txt $line $ln 0
    if {[string index $line end] ne "\\"} {set prevQtd 0}
    return $prevQtd
  }
  set currQtd $prevQtd  ;# current state of being quoted
  set i [set pri [set lasti 0]]
  set k -1
  while {1} {
    if {![FirstQtd line i $currQtd]} break
    set lasti $i
    if {$currQtd} {
      HighlightStr $txt $ln.$pri "$ln.$i +1 char"
      set currQtd 0
      incr lasti
      if {[AuxEnding j line lasti]} {
        set i $lasti
        set st [string range $line $i $j]
        set it 0
        if {[FirstQtd st it $currQtd]} continue  ;# there is a quote yet
        set k $j
        break
      }
    } else {
      if {[AuxEnding j line pri] && $j<$i} {
        set lasti $pri
        set k $j
        break
      }
      HighlightCmd $txt $line $ln $pri $i
      set currQtd 1
    }
    set pri $i
    incr i
  }
  if {$currQtd} {
    HighlightStr $txt $ln.$pri $ln.end
  } elseif {$k>-1 || [AuxEnding k line lasti]} {
    HighlightCmd $txt $line $ln $lasti $k
    HighlightComment $txt $line $ln $k
    if {[string index $line end] eq "\\"} {set currQtd -1}
  } else {
    HighlightCmd $txt $line $ln $lasti [string length $line]
  }
  if {!$data(MULTILINE,$txt) && $currQtd && [string index $line end] ne "\\"} {
    set currQtd 0
  }
  return $currQtd
}
#_______________________

proc ::hl_tcl::my::HighlightAll {txt} {
  # Highlights all of a text.
  #   txt - text widget's path
  # Makes a coroutine from this.
  # See also: CoroHighlightAll

  # let them work one by one:
  set coroNo [expr {[incr ::hl_tcl::my::data(CORALL)] % 10000000}]
  coroutine co_HlAll$coroNo ::hl_tcl::my::CoroHighlightAll $txt
}
#_______________________

proc ::hl_tcl::my::CoroHighlightAll {txt} {
  # Highlights all of a text as a coroutine.
  #   txt - text widget's path
  # See also: HighlightAll

  variable data
  catch {  ;# $txt may be destroyed, so catch this
    if {!$data(PLAINTEXT,$txt)} {
      set tlen [lindex [split [$txt index end] .] 0]
      RemoveTags $txt 1.0 end
      set maxl [expr {min($data(SEEN,$txt),$tlen)}]
      set maxl [expr {min($data(SEEN,$txt),$tlen)}]
      for {set currQtd [set ln [set lnseen 0]]} {$ln<$tlen} {} {
        incr ln
        set currQtd [HighlightLine $txt $ln $currQtd]
        if {[incr lnseen]>$data(SEEN,$txt)} {
          set lnseen 0
          after idle after 1 [info coroutine]
          yield
        }
      }
    }
  }
  set data(REG_TXT,$txt) {1}
}
#_______________________

proc ::hl_tcl::my::BindToEvent {w event args} {
  # Binds an event on a widget to a command.
  #   w - the widget's path
  #   event - the event
  #   args - the command

  if {[string first $args [bind $w $event]]<0} {
    bind $w $event [list + {*}$args]
  }
}

# _________________________ DYNAMIC highlighting ________________________ #

proc ::hl_tcl::my::CountQSH {txt ln} {
  # Counts quotes, slashes, hashes in a line
  #   txt - text widget's path
  #   ln - line's index

  set ln [expr {int($ln)}]
  set st [$txt get $ln.0 $ln.end]
  list [CountChar $st \"] [CountChar $st \\] [CountChar $st #]
}
#_______________________

proc ::hl_tcl::my::IsCurline {txt {flag ""}} {
  # Sets / gets "highlight a current line" flag for a text.
  #   txt - the text's path
  #   flag - the flag

  variable data
  if {$flag eq {}} {
    if {[info exists data(HL_CURLINE,$txt)]} {
      set flag $data(HL_CURLINE,$txt)
    } else {
      set flag 0
    }
    return $flag
  }
  set data(HL_CURLINE,$txt) $flag
}
#_______________________

proc ::hl_tcl::my::ShowCurrentLine {txt {doit no}} {
  # Shows the current line.
  #   txt - text widget's path
  #   doit - if yes, forces updating current line's background
  # Returns a current position of cursor.

  variable data
  set pos [$txt index insert]
  set nlines [expr {int([$txt index end])}]
  lassign [split $pos .] ln cn
  if {[catch {lassign [split $data(CURPOS,$txt) .] ln2 cn2}]} {
    set ln2 $ln
    set cn2 $cn
    set data(CURPOS,$txt) [set data(NLINES,$txt) 0]
  }
  if {[IsCurline $txt]} {
    if {$doit || int($data(CURPOS,$txt))!=$ln || $data(NLINES,$txt)!=$nlines \
    || $ln!=$ln2 || abs($cn-$cn2)>1 || $cn<2} {
      $txt tag remove tagCURLINE 1.0 end
      $txt tag add tagCURLINE [list $pos linestart] [list $pos lineend]+1displayindices
    }
  }
  set data(NLINES,$txt) $nlines
  set data(CURPOS,$txt) $pos
  return $pos
}
#_______________________

proc ::hl_tcl::my::MemPos1 {txt {donorm yes} {K ""} {s ""}} {
  # Checks and sets the cursor's width, depending on its position.
  #   txt - text widget's path
  #   donorm - if yes, forces "normal" cursor
  #   K - key (%K of bind)
  #   s - state (%s of bind)
  # This fixes an issue with text cursor: less width at 0th column.

  variable data
  if {$K eq {Home} && [string is digit -strict $s] && \
  [expr {$s & 4}]==0 && [expr {$s & 1}]==0} {
    # Ctrl-Home & Shift-Home are passed
    set p1 [$txt index insert]
    set line [$txt get "$p1 linestart" "$p1 lineend"]
    set p [expr {[string length $line]-[string length [string trimleft $line]]}]
    set p2 [expr {int($p1)}].$p
    if {$p && $p2 ne $p1} {
      after idle "::tk::TextSetCursor $txt $p2"
      return 0
    }
  }
  if {$data(INSERTWIDTH,$txt)==1} {
    if {[$txt cget -insertwidth]!=1} {$txt configure -insertwidth 1}
    return 0
  }
  set insLC [$txt index insert]
  lassign [split $insLC .] L C
  if {$data(_INSPOS_,$txt) eq {}} {
    set L2 [set C2 0]
  } else {
    lassign [split $data(_INSPOS_,$txt) .] L2 C2
  }
  if {$L!=$L2 || $C==0 || $C2==0} {
    if {$C || $donorm} {
      $txt configure -insertwidth $data(INSERTWIDTH,$txt)
    } else {
      $txt configure -insertwidth [expr {$data(INSERTWIDTH,$txt)*2-1}]
    }
  }
  return $insLC
}
#_______________________

proc ::hl_tcl::my::MemPos {txt {doit no}} {
  # Remembers the state of current line.
  #   txt - text widget's path
  #   doit - argument for ShowCurrentLine
  # See also: ShowCurrentLine

  variable data
  catch {
    set data(_INSPOS_,$txt) [MemPos1 $txt no]
    set ln [ShowCurrentLine $txt $doit]
    set data(CUR_LEN,$txt) [$txt index {end -1 char}]
    lassign [CountQSH $txt $ln] \
      data(CNT_QUOTE,$txt) data(CNT_SLASH,$txt) data(CNT_COMMENT,$txt)
    if {[$txt tag ranges tagBRACKET] ne {}}    {$txt tag remove tagBRACKET 1.0 end}
    if {[$txt tag ranges tagBRACKETERR] ne {}} {$txt tag remove tagBRACKETERR 1.0 end}
    if {[set cmd $data(CMDPOS,$txt)] ne {}} {
      # run a command after changing position (with the state as arguments)
      append cmd " $txt $data(CUR_LEN,$txt) $ln $data(CNT_QUOTE,$txt) \
        $data(CNT_SLASH,$txt) $data(CNT_COMMENT,$txt)"
      catch {after cancel $data(CMDATFER,$txt)}
      set data(CMDATFER,$txt) [after idle $cmd]
    }
  }
}
#_______________________

proc ::hl_tcl::my::RunCoroAfterIdle {txt pos1 pos2 wait args} {
  # Runs a "modified" corotine after idle.

  variable data
  if {$wait} {
    catch {
      after cancel $data(COROAFTER,$txt)
      if {$data(COROPOS1,$txt) < $pos1} {set pos1 $data(COROPOS1,$txt)}
      if {$data(COROPOS2,$txt) > $pos2} {set pos2 $data(COROPOS2,$txt)}
    }
    set data(COROPOS1,$txt) $pos1
    set data(COROPOS2,$txt) $pos2
  }
  set data(COROAFTER,$txt) [after idle "::hl_tcl::my::CoroRun $txt $pos1 $pos2 $args"]
}
#_______________________

proc ::hl_tcl::my::Modified {txt oper pos1 args} {
  # Handles modifications of text.
  #   txt - text widget's path
  # Makes a coroutine from this.
  # See also: CoroModified

  variable data
  set ar2 [lindex $args 0]
  set posins [$txt index insert]
  if {[catch {set pos1 [set pos2 [$txt index $pos1]]}]} {
    set pos1 [set pos2 $posins]
  }
  switch $oper {
    insert {
      set pos2 [expr {$pos1 + [llength [split $ar2 \n]]}]
    }
    delete {
      if {$ar2 eq {} || [catch {set pos2 [$txt index $ar2]}]} {
        set pos2 $posins
      }
    }
  }
  RunCoroAfterIdle $txt $pos1 $pos2 no {*}$args
}
#_______________________

proc ::hl_tcl::my::CoroRun {txt pos1 pos2 args} {

  variable data
  if {![info exist data(REG_TXT,$txt)] || $data(REG_TXT,$txt) eq {} || \
  ![info exist data(CUR_LEN,$txt)]} {
    # skip changes till the highlighting done
    after 10 [list ::hl_tcl::my::RunCoroAfterIdle $txt $pos1 $pos2 yes {*}$args]
    return
  }
  # let them work one by one
  set i1 [expr {int($pos1)}]
  set i2 [expr {int($pos2)}]
  set coroNo [expr {[incr data(CORMOD)] % 10000000}]
  coroutine CoModified$coroNo ::hl_tcl::my::CoroModified $txt $i1 $i2 {*}$args
}
#_______________________

proc ::hl_tcl::my::CoroModified {txt {i1 -1} {i2 -1} args} {
  # Handles modifications of text.
  #   txt - text widget's path
  #   i1 - 1st index of changes
  #   i2 - 2nd index of changes
  #   args - arguments for a command called after the modifications
  # See also: Modified

  catch {
    variable data
    # current line:
    set ln [expr {int([$txt index insert])}]
    # ending line:
    set endl [expr {int([$txt index {end -1 char}])}]
    # range of change:
    if {$i1!=-1} {
      set dl [expr {abs($i2-$i1)}]
      set ln $i1
    } else {
      set dl [expr {abs(int($data(CUR_LEN,$txt)) - $endl)}]
    }
    # begin and end of changes:
    set ln1 [set lno1 [expr {max(($ln-$dl),1)}]]
    set ln2 [set lno2 [expr {min(($ln+$dl),$endl)}]]
    lassign [CountQSH $txt $ln] cntq cnts ccmnt
    # flag "highlight to the end":
    set bf1 [expr {abs($ln-int($data(CURPOS,$txt)))>1 || $dl>1 \
    || $cntq!=$data(CNT_QUOTE,$txt) \
    || $ccmnt!=$data(CNT_COMMENT,$txt)}]
    set bf2 [expr {$cnts!=$data(CNT_SLASH,$txt)}]
    if {$bf1 && !$data(MULTILINE,$txt) || $bf2} {
      set lnt1 $ln
      set lnt2 [expr {$ln+1}]
      while {$ln2<$endl && $lnt1<$endl && $lnt2<=$endl && ( \
      [$txt get "$lnt1.end -1 char" $lnt1.end] in {\\ \"} ||
      [$txt get "$lnt2.end -1 char" $lnt2.end] in {\\ \"}) || $bf2} {
        incr lnt1 ;# next lines be handled too, if ended with "\\"
        incr lnt2
        incr ln2
        set bf2 0
      }
    }
    set tSTR [$txt tag ranges tagSTR]
    set tCMN [$txt tag ranges tagCMN]
    lappend tCMN {*}[$txt tag ranges tagCMN2]
    if {$ln1==1} {
      set currQtd 0
    } else {
      set currQtd [LineState $txt $tSTR $tCMN "$ln1.0 -1 chars"]
    }
    $txt tag add tagSTD $ln1.0 $ln2.end
    if {!$data(PLAINTEXT,$txt)} {
      set lnseen 0
      while {$ln1<=$ln2} {
        if {$ln1==$ln2} {
          set bf2 [LineState $txt $tSTR $tCMN "$ln1.end +1 chars"]
        }
        RemoveTags $txt $ln1.0 $ln1.end
        set currQtd [HighlightLine $txt $ln1 $currQtd]
        if {$ln1==$ln2 && ($bf1 || $bf2!=$currQtd) && $data(MULTILINE,$txt)} {
          set ln2 $endl  ;# run to the end
        }
        if {[incr lnseen]>$data(SEEN,$txt)} {
          set lnseen 0
          catch {after cancel $data(COROATFER,$txt)}
          set data(COROATFER,$txt) [after idle after 1 [info coroutine]]
          yield
        }
        incr ln1
      }
    }
    if {[set cmd $data(CMD,$txt)] ne {}} {
      # run a command after changes done (its arguments are txt, ln1, ln2)
      append cmd { } $txt { } $lno1 { } $lno2 { } $args
      after idle $cmd
    }
    MemPos $txt
  }
}
#_______________________

proc ::hl_tcl::my::InRange {p1 p2 l {c -1}} {
  # Checks if a text position is in a range of text positions.
  #   p1 - 1st position of range
  #   p2 - 2nd position of range
  #   l - line position to check (or 'l.c' if 'c' not set)
  #   c - column position to check

  if {$c==-1} {lassign [split $l .] l c}
  lassign [split $p1 .] l1 c1
  lassign [split $p2 .] l2 c2
  incr c2 -1 ;# text ranges are not right-inclusive
  expr { ($l>=$l1 && $l<$l2 && $c>=$c1) || ($l>$l1 && $l<=$l2 && $c<=$c2)
    || ($l==$l1 && $l1==$l2 && $c>=$c1 && $c<=$c2) || ($l>$l1 && $l<$l2) }
}
#_______________________

proc ::hl_tcl::my::SearchTag {tagpos l1} {
  # Searches a position in tag ranges.
  #   tagpos - tag position ranges
  #   l1 - the position to find
  # Returns a found range's index of -1 if not found.

  lassign [split $l1 .] l c
  set i 0
  foreach {p1 p2} $tagpos {
    if {[InRange $p1 $p2 $l $c]} {return $i}
    incr i 2
  }
  return -1
}
#_______________________

proc ::hl_tcl::my::LineState {txt tSTR tCMN l1} {
  # Gets an initial state of line.
  #   txt - text widget's path
  #   tSTR - ranges of string tags
  #   tCMN - ranges of comment tags
  #   l1 - the line's index
  # Returns: 0 if no tags for the line; 1 if the line is a string's continuation; -1 if the line is a comment's continuation.

  variable data
  set i1 [$txt index $l1]
  if {[set prev [string first -1 $l1]]>-1} {
    set i1 [$txt index "$i1 -1 chars"]
  }
  set ch [$txt get "$i1" "$i1 +1 chars"]
  if {[SearchTag $tCMN [$txt index "$i1 -1 chars"]]!=-1} {  ;# is a comment continues?
    if {$ch eq "\\"} {return -1}
  } elseif {$data(MULTILINE,$txt) || $ch eq "\\"} {         ;# is a string continues?
    set nl [lindex [split $l1 .] 0]
    if {$prev>-1} {
      # is the start of line quoted?
      # Tk tag ranges refer only to non-empty lines
      # => previous two non-empty chars' coordinates are needed
      # to analize whether they:
      # - end the range
      # - are inside of the range
      # - begin the range
      set co1 [set co2 {}]
      while {$nl>1} {
        incr nl -1
        if {[set line [$txt get $nl.0 $nl.end]] ne {}} {
          if {$co2 eq {}} {
            set co2 [$txt index "$nl.end -1 char"]
            if {[string length $line]>1} {
              set co1 [$txt index "$nl.end -2 char"]
              break
            }
          } else {
            set co1 [$txt index "$nl.end -1 char"]
            break
          }
        }
      }
      if {$co2 eq {}} {return 0}   {set f2 [expr {[SearchTag $tSTR $co2]!=-1}]}
      if {$co1 eq {}} {set f1 $f2} {set f1 [expr {[SearchTag $tSTR $co1]!=-1}]}
      set ch [$txt get $co2 "$co2 +1 chars"]
      set c [lindex [split [$txt index $co2] .] 1]
      if {![NotEscaped $line $c]} {set ch {}}
      return [expr {$ch ne {"} && $f2 || $ch eq {"} && !$f1}]
    }
    # is the end of line quoted?
    set line {}
    set nltot [lindex [split [$txt index end] .] 0]
    while {$nl<$nltot} {
      incr nl
      if {[set line [$txt get $nl.0 $nl.end]] ne {}} break
    }
    set i1 $nl.0
    set ch [$txt get $i1 "$i1 +1 chars"]
    set c [lindex [split [$txt index $i1] .] 1]
    if {![NotEscaped $line $c]} {set ch {}}
    set f1 [expr {[SearchTag $tSTR [$txt index $i1]]!=-1}]
    set f2 [expr {[SearchTag $tSTR [$txt index "$i1 +1 chars"]]!=-1}]
    return [expr {$ch ne {"} && $f1 || $ch eq {"} && !$f2}]
  }
  return 0
}

# __________ HEROIC EFFORTS to highlight the matching brackets __________ #

proc ::hl_tcl::my::MergePosList {none args} {
  # Merges lists of numbers that are not-coinciding and sorted.
  #   none - a number to be not allowed in the lists (e.g. less than minimal)
  #   args - list of the lists to be merged
  # Returns a list of pairs: index of list + item of list.

  set itot [set ilist 0]
  set lind [set lout [list]]
  foreach lst $args {
    incr ilist
    incr itot [set llen [llength $lst]]
    lappend lind [list 0 $llen]
  }
  for {set i 0} {$i<$itot} {incr i} {
    set min $none
    set ind -1
    for {set k 0} {$k<$ilist} {incr k} {
      lassign [lindex $lind $k] li llen
      if {$li < $llen} {
        set e [lindex [lindex $args $k] $li]
        if {$min == $none || $min > $e} {
          set ind $k
          set min $e
          set savli [incr li]
          set savlen $llen
        }
      }
    }
    if {$ind == -1} {return -code error {Error: probably in the input data}}
    lset lind $ind [list $savli $savlen]
    lappend lout [list $ind $min]
  }
  return $lout
}
#_______________________

proc ::hl_tcl::my::CountChar {str ch {plistName ""} {escaped yes}} {
  # Counts a character in a string.
  #   str - the string
  #   ch - the character
  #   plistName - variable name for a list of positions of *ch*
  #   escaped - true, if the character is escaped.
  # Returns a number of any occurences of character *ch* in string *str*
  # if the character is escaped, but if it is not escaped, only non-escaped
  # characters are counted.

  if {$plistName ne {}} {
    upvar 1 $plistName plist
    set plist [list]
  }
  set icnt [set begidx 0]
  while {[set idx [string first $ch $str]] >= 0} {
    set nidx $idx
    if {$escaped || ![Escaped $str $idx]} {
      incr icnt
      if {$plistName ne {}} {lappend plist [expr {$begidx+$idx}]}
    }
    incr begidx [incr idx]
    set str [string range $str $idx end]
  }
  return $icnt
}
#_______________________

proc ::hl_tcl::my::Escaped {line curpos} {
  # Checks if a character is escaped in a string.
  #   line - the string
  #   curpos - position of the character in the line
  # Returns 1 if the character is escaped in the string.

  set line [string range $line 0 $curpos-1]
  set linetrim [string trimright $line \\]
  expr {([string length $line]-[string length $linetrim])%2}
}
#_______________________

proc ::hl_tcl::my::MatchedBrackets {w inplist curpos schar dchar dir} {
  # Finds a match of characters (dchar for schar).
  #   w - text widget's path
  #   inplist - list of strings where to find a match
  #   curpos - position of schar in nl.nc form where nl=1.., nc=0..
  #   schar - source character
  #   dchar - destination character
  #   dir - search direction: 1 to the end, -1 to the beginning of list

  lassign [split $curpos .] nl nc
  if {$schar eq {"}} {
    set npos $nl.$nc
    set hlpr [$w tag prevrange tagSTR $npos]
    if {[llength $hlpr] && [$w compare "$npos +1 char" == [lindex $hlpr 1]]} {
      set dir -1  ;# <- quotes are scanned depending on their range (for tcl/c)
    } else {
      set hlpr [$w tag nextrange tagSTR $npos]
      if {![llength $hlpr] || [$w compare $npos != [lindex $hlpr 0]]} {
        # for plain texts:
        if {[$w search -exact \" "$npos +1 char" end] eq {}} {
          set dir -1
        } else {
          set lfnd [$w search -backwards -all -exact \" $npos 1.0]
          if {[llength $lfnd] % 2} {
            set dir -1
          }
        }
      }
    }
    incr nc $dir
  }
  set escaped [Escaped [lindex $inplist $nl-1] $nc]
  if {$dir==1} {set rng1 "$nc end"} else {set rng1 "0 $nc"; set nc 0}
  set retpos {}
  set scount [set dcount 0]
  incr nl -1
  set inplen [llength $inplist]
  while {$nl>=0 && $nl<$inplen} {
    set line [lindex $inplist $nl]
    set line [string range $line {*}$rng1]
    set sc [CountChar $line $schar slist $escaped]
    set dc [CountChar $line $dchar dlist $escaped]
    set plen [llength [set plist [MergePosList -1 $slist $dlist]]]
    for {set i [expr {$dir>0?0:($plen-1)}]} {$i>=0 && $i<$plen} {incr i $dir} {
      lassign [lindex $plist $i] src pos
      if {$src} {incr dcount} {incr scount}
      if {$scount <= $dcount} {
        set retpos [incr nl].[incr pos $nc]
        break
      }
    }
    if {$retpos ne {}} break
    set nc 0
    set rng1 {0 end}
    incr nl $dir
  }
  return $retpos
}
#_______________________

proc ::hl_tcl::my::HighlightBrackets {w} {
  # Highlights matching brackets if any.
  #   w - text widget's path

  variable data
  set curpos [ShowCurrentLine $w]
  set curpos2 [$w index {insert -1 chars}]
  set ch [$w get $curpos]
  set il [string first $ch $data(LBR)]
  set ir [string first $ch $data(RBR)]
  set txt [split [$w get 1.0 end] \n]
  if {$il>-1} {
    set brcpos [MatchedBrackets $w $txt $curpos \
      [string index $data(LBR) $il] [string index $data(RBR) $il] 1]
  } elseif {$ir>-1} {
    set brcpos [MatchedBrackets $w $txt $curpos \
      [string index $data(RBR) $ir] [string index $data(LBR) $ir] -1]
  } elseif {[set il [string first [$w get $curpos2] $data(LBR)]]>-1} {
    set curpos $curpos2
    set brcpos [MatchedBrackets $w $txt $curpos \
      [string index $data(LBR) $il] [string index $data(RBR) $il] 1]
  } elseif {[set ir [string first [$w get $curpos2] $data(RBR)]]>-1} {
    set curpos $curpos2
    set brcpos [MatchedBrackets $w $txt $curpos \
      [string index $data(RBR) $ir] [string index $data(LBR) $ir] -1]
  } else {
    return
  }
  if {$brcpos ne {}} {
    $w tag add tagBRACKET $brcpos
    $w tag add tagBRACKET $curpos
  } else {
    $w tag add tagBRACKETERR $curpos
  }
}

# _________________________ INTERFACE procedures ________________________ #

proc ::hl_tcl::hl_readonly {txt {ro -1} {com2 ""}} {
  # Makes the text widget be readonly or gets its 'read-only' state.
  #   txt - text widget's path
  #   ro - flag "the text widget is readonly"
  #   com2 - command to be called at viewing and after changes
  # If 'ro' argument is omitted, returns the widget's 'read-only' state.

  if {$ro==-1} {
    return [expr {[info exists ::hl_tcl::my::data(READONLY,$txt)] && $::hl_tcl::my::data(READONLY,$txt)}]
  }
  set ::hl_tcl::my::data(READONLY,$txt) $ro
  if {$com2 ne {}} {set ::hl_tcl::my::data(CMD,$txt) $com2}
  set newcom "::$txt.internal"
  if {[info commands $newcom] eq ""} {rename $txt $newcom}
  set com "[namespace current]::my::Modified $txt"
  #if {$com2 ne ""} {append com " ; $com2"}
  if {$ro} {proc ::$txt {args} " \
    if {!\[winfo exists $txt\]} {return 0} ; \
    switch -exact -- \[lindex \$args 0\] \{ \
      insert \{$com2\} \
      delete \{$com2\} \
      replace \{$com2\} \
      default \{ return \[eval $newcom \$args\] \} \
    \}"
  } else {proc ::$txt {args} " \
    if {!\[winfo exists $txt\]} {return 0} ; \
    switch -exact -- \[lindex \$args 0\] \{ \
      delete \{$com {*}\$args\} \
      insert \{$com {*}\$args\} \
      replace \{$com {*}\$args\} \
    \} ; \
    set _res_ \[eval $newcom \$args\] ; \
    return \$_res_"
  }
}
#_______________________

proc ::hl_tcl::hl_init {txt args} {
  # Initializes highlighting.
  #   txt - text widget's path
  #   args - dict of options
  # The 'args' options include:
  #   -- - means that only args' options will be initialized (defaults skipped)
  #   -dark - flag "the text widget has dark background"
  #   -readonly - flag "read-only"
  #   -optRE - flag "use of RE to highlight options"
  #   -multiline - flag "allowed multi-line strings"
  #   -cmd - command to watch editing/viewing
  #   -cmdpos - command to watch cursor positioning
  #   -colors - list of colors as set in hl_tcl::hl_colorNames
  #   -font - attributes of font
  #   -seen - lines seen at start
  #   -keywords - additional commands to highlight (as Tk ones)
  # This procedure has to be called before writing a text in the text widget.
  # See also: hl_colorNames

  if {[set setonly [expr {[lindex $args 0] eq {--}}]]} {
    set args [lrange $args 1 end]
  }
  iscurline $txt 1
  set ::hl_tcl::my::data(REG_TXT,$txt) {}  ;# disables Modified at changing the text
  set ::hl_tcl::my::data(KEYWORDS,$txt) {}
  # get default options from text's ones
  set defopts [list -insertwidth]
  foreach defopt $defopts {
    set opt [OptName $txt $defopt]
    if {![info exists ::hl_tcl::my::data($opt)]} {
      set ::hl_tcl::my::data($opt,DEFAULT) [$txt cget $defopt]
    }
  }
  foreach {opt val} {-dark 0 -readonly 0 -cmd {} -cmdpos {} -optRE 1 \
  -multiline 1 -seen 500 -plaintext no -plaincom {} -insertwidth {} -keywords {}} {
    if {[dict exists $args $opt]} {
      set val [dict get $args $opt]
    } elseif {$setonly} {
      continue  ;# only those set in args are taken into account
    }
    set ::hl_tcl::my::data([OptName $txt $opt]) $val
  }
  # reget default options from text's ones
  foreach defopt $defopts {
    set opt [OptName $txt $defopt]
    if {[info exists ::hl_tcl::my::data($opt)] && $::hl_tcl::my::data($opt) eq {}} {
      set ::hl_tcl::my::data($opt) $::hl_tcl::my::data($opt,DEFAULT)
    }
  }
  set ::hl_tcl::my::data(CMD_TK_EXP) [lsort [list \
    {*}$::hl_tcl::my::data(CMD_TK) {*}$::hl_tcl::my::data(KEYWORDS,$txt)]]
  unset ::hl_tcl::my::data(KEYWORDS,$txt)
  if {[dict exists $args -colors]} {
    set colors [dict get $args -colors]
    lassign [addingColors $::hl_tcl::my::data(DARK,$txt)] clrCURL clrCMN2
    if {[set llen [llength $colors]]==8} {
      lappend colors $clrCURL  ;# add curr.line color if omitted
    }
    if {$llen==9} {
      lappend colors $clrCMN2  ;# add #TODO color if omitted
    }
    set ::hl_tcl::my::data(COLORS,$txt) $colors
    set ::hl_tcl::my::data(SETCOLORS,$txt) 1
  } else {
    if {![info exists ::hl_tcl::my::data(COLORS,$txt)]}  {
      addingColors $::hl_tcl::my::data(DARK,$txt) $txt
    }
  }
  if {!$setonly} {
    if {[dict exists $args -font]} {
      set ::hl_tcl::my::data(FONT,$txt) [dict get $args -font]
    } else {
      set ::hl_tcl::my::data(FONT,$txt) [font actual TkFixedFont]
    }
    set ::hl_tcl::my::data(ISPLAINCOM,$txt) [expr {$::hl_tcl::my::data(PLAINCOM,$txt) ne {}}]
  }
  if {!$setonly || [dict exists $args -readonly]} {
    hl_readonly $txt $::hl_tcl::my::data(READONLY,$txt)
  }
  if {[string first ::hl_tcl:: [bind $txt]]<0} {
    my::BindToEvent $txt <FocusIn> ::hl_tcl::my::ShowCurrentLine $txt
  }
  set ::hl_tcl::my::data(_INSPOS_,$txt) {}
  my::MemPos $txt
}
#_______________________

proc ::hl_tcl::hl_text {txt} {
  # Highlights Tcl code of a text widget.
  #   txt - text widget's path

  set font0 $::hl_tcl::my::data(FONT,$txt)
  set font1 [set font2 $font0]
  $txt tag configure tagSTD -font "$font0"
  $txt tag add tagSTD 1.0 end
  dict set font1 -weight bold
  dict set font2 -slant italic
  lassign $::hl_tcl::my::data(COLORS,$txt) \
    clrCOM clrCOMTK clrSTR clrVAR clrCMN clrPROC clrOPT clrBRA clrCURL clrCMN2
  $txt tag configure tagCOM -font "$font1" -foreground $clrCOM
  $txt tag configure tagCOMTK -font "$font1" -foreground $clrCOMTK
  $txt tag configure tagSTR -font "$font0" -foreground $clrSTR
  $txt tag configure tagVAR -font "$font0" -foreground $clrVAR
  $txt tag configure tagCMN -font "$font2" -foreground $clrCMN
  $txt tag configure tagCMN2 -font "$font2" -foreground $clrCMN2 ;#red
  $txt tag configure tagPROC -font "$font1" -foreground $clrPROC
  $txt tag configure tagOPT -font "$font0" -foreground $clrOPT
  $txt tag configure tagBRACKET -font "$font0" -foreground $clrBRA
  $txt tag configure tagBRACKETERR -font "$font0" -foreground white -background red
  $txt tag configure tagCURLINE -background $clrCURL
  $txt tag raise sel
  $txt tag raise tagBRACKETERR
  catch {$txt tag raise hilited;  $txt tag raise hilited2} ;# for apave package
  my::HighlightAll $txt
  my::BindToEvent $txt <FocusIn> ::hl_tcl::my::MemPos $txt
  my::BindToEvent $txt <KeyPress> ::hl_tcl::my::MemPos1 $txt yes %K %s
  my::BindToEvent $txt <KeyRelease> ::hl_tcl::my::MemPos $txt
  my::BindToEvent $txt <ButtonRelease-1> ::hl_tcl::my::MemPos $txt
  foreach ev {Enter KeyRelease ButtonRelease-1} {
    my::BindToEvent $txt <$ev> ::hl_tcl::my::HighlightBrackets $txt
  }
  set ro $::hl_tcl::my::data(READONLY,$txt)
  set com2 $::hl_tcl::my::data(CMD,$txt)
  set txtattrs [list $txt $ro $com2]
  if {![info exists ::hl_tcl::my::data(LIST_TXT)] || \
  [set i [lsearch -index 0 -exact $::hl_tcl::my::data(LIST_TXT) $txt]]==-1} {
    lappend ::hl_tcl::my::data(LIST_TXT) $txtattrs
  } else {
    set ::hl_tcl::my::data(LIST_TXT) [lreplace $::hl_tcl::my::data(LIST_TXT) $i $i $txtattrs]
  }
  hl_readonly $txt $ro $com2
}
#_______________________

proc ::hl_tcl::hl_all {args} {
  # Updates ("rehighlights") all highlighted and existing text widgets.
  #   args - dict of options
  # See also: hl_init

  if {[info exists ::hl_tcl::my::data(LIST_TXT)]} {
    foreach wattrs $::hl_tcl::my::data(LIST_TXT) {
      lassign $wattrs txt ro com2
      if {[winfo exists $txt]} {
        if {![info exists ::hl_tcl::my::data(SETCOLORS,$txt)]} {
          unset ::hl_tcl::my::data(COLORS,$txt) ;# colors defined by DARK
        }
        # args (if set) override the appropriate settings for $txt
        hl_init $txt -- {*}$args
        hl_text $txt
      }
    }
  }
}
#_______________________

proc ::hl_tcl::hl_colorNames {} {
  # Returns a list of color names for syntax highlighting.

  list clrCOM clrCOMTK clrSTR clrVAR clrCMN clrPROC clrOPT clrBRA
}

#_______________________

proc ::hl_tcl::hl_colors {txt {dark ""} args} {
  # Gets/sets the main colors for highlighting (except for "curr.line").
  #   txt - text widget's path or {} or an index of default colors
  #   dark - flag "dark scheme"
  #   args - a list of colors to set for *txt*
  # Returns a list of colors for COM COMTK STR VAR CMN PROC OPT BRAC \
   or, if the colors aren't initialized, "standard" colors.

  if {[llength $args]} {
    set ::hl_tcl::my::data(COLORS,$txt) $args
    return
  }
  if {[info exists ::hl_tcl::my::data(COLORS,$txt)]}  {
    return $::hl_tcl::my::data(COLORS,$txt)
  }
  if {$dark eq {}} {set dark $::hl_tcl::my::data(DARK,$txt)}
  if {![string is integer -strict $txt] || $txt<0 || $txt>3} {set txt 0}
  if {$dark} {set dark 1} {set dark 0}
  return [lindex $::hl_tcl::my::data(SYNTAXCOLORS,$txt) $dark]
}
#_______________________

proc ::hl_tcl::hl_line {txt} {
  # Updates a current line's highlighting.
  #   txt - text's path

  if {!$::hl_tcl::my::data(PLAINTEXT,$txt)} {
    set ln0 [expr {int([$txt index insert])}]
    set ln2 [expr {int([$txt index end])}]
    set ln1 [expr {max (1,$ln0-1)}]
    set ln2 [expr {min ($ln2,$ln0+1)}]
    # update lines: previous, current, next
    ::hl_tcl::my::RunCoroAfterIdle $txt $ln1 $ln2 no
  }
  ::hl_tcl::my::MemPos $txt yes
  $txt configure -insertwidth $::hl_tcl::my::data(INSERTWIDTH,$txt)
}
#_______________________

proc ::hl_tcl::addingColors {{dark ""} {txt ""} {cs ""}} {
  # Sets/gets colors for a text syntax highlighting.
  #   dark - yes, if the current theme is dark
  #   txt - path to the text or {}
  #   cs - color scheme
  # If *txt* omitted, returns a list of resting colors.
  # The resting colors are:
  #   - current line's background
  #   - #TODO and #! comment's foreground

  variable my::data
  # try to get color options from current apave settings
  if {[catch {set clrCURL [lindex [::apave::obj csGet $cs] 16]}]} {
    set clrCURL {}
  }
  if {$dark eq {}} {
    if {[catch {set dark [::apave::obj csDark]}]} {
      set dark no
    }
  }
  if {$dark} {
    if {$clrCURL eq {}} {set clrCURL #29383c}
    set clrCMN2 #ff7272
  } else {
    if {$clrCURL eq {}} {set clrCURL #efe0cd}
    set clrCMN2 #ff0000
  }
  if {$txt eq {}} {
    return [list $clrCURL $clrCMN2]
  }
  set my::data(COLORS,$txt) [list {*}[hl_colors $txt] $clrCURL $clrCMN2]
}
#_______________________

proc ::hl_tcl::hl_commands {} {
  # Lists all Tcl/Tk commands registered here.

  variable my::data
  list {*}$my::data(PROC_TCL) {*}$my::data(CMD_TCL) {*}$my::data(CMD_TK)
}
#_______________________

proc ::hl_tcl::iscurline {txt {flag ""}} {
  # Sets / gets "highlight a current line" flag for a text.
  #   txt - the text's path
  #   flag - the flag

  my::IsCurline $txt $flag
}
#_______________________

proc ::hl_tcl::isdone {txt} {
  # Checks if the highlighting of the text is done.
  #   txt - text's path

  variable my::data
  expr {[info exist my::data(REG_TXT,$txt)] && $my::data(REG_TXT,$txt) ne {}}
}
#_______________________

proc ::hl_tcl::clearup {txt} {
  # Clears data related to text (esp. at deleting it).
  #   txt - text's path

  variable my::data
  foreach key [array names my::data *,$txt] {
    unset my::data($key)
  }
  foreach i [lsearch -all -exact -index 0 $my::data(LIST_TXT) $txt] {
    set my::data(LIST_TXT) [lreplace $my::data(LIST_TXT) $i $i]
  }
}
#_______________________

proc ::hl_tcl::cget {txt opt} {
  # Gets a highlighting option's value.
  #   txt - text's path
  #   opt - option's name

  variable my::data
  set opt [string toupper [string trimleft $opt -]]
  if {[info exists my::data($opt,$txt)]} {
    return $my::data($opt,$txt)
  }
  return {}
}
#_______________________

proc ::hl_tcl::configure {txt opt val} {
  # Sets a highlighting option's value.
  #   txt - text's path
  #   opt - option's name
  #   val - option's value

  variable my::data
  set opt [string toupper [string trimleft $opt -]]
  set my::data($opt,$txt) $val
}

# ________________________ Helpers _________________________ #

proc ::hl_tcl::OptName {txt opt} {
  # Gets hl_tcl option's name.
  #   txt - text's path
  #   opt - option

  return [string toupper [string range $opt 1 end]],$txt
}

# _________________________________ EOF _________________________________ #

hl_tcl.tcl