00001 # TKE - Advanced Programmer's Editor
00002 # Copyright (C) 2014-2019 Trevor Williams (phase1geo@gmail.com)
00003 #
00004 # This program is free software; you can redistribute it and/or modify
00005 # it under the terms of the GNU General Public License as published by
00006 # the Free Software Foundation; either version 2 of the License, or
00007 # (at your option) any later version.
00008 #
00009 # This program is distributed in the hope that it will be useful,
00010 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00011 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
00012 # GNU General Public License for more details.
00013 #
00014 # You should have received a copy of the GNU General Public License along
00015 # with this program; if not, write to the Free Software Foundation, Inc.,
00016 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
00017
00018 ######################################################################
00019 # Name: folding.tcl
00020 # Author: Trevor Williams (phase1geo@gmail.com)
00021 # Date: 02/06/2016
00022 # Brief: Contains namespace handling code folding.
00023 ######################################################################
00024
00025 namespace eval folding {
00026
00027 array set enable {}
00028
00029 ######################################################################
00030 # Returns true if the given text widget has code folding enabled.
00031 proc get_enable {txt} {
00032
00033 variable enable
00034
00035 return $enable($txt)
00036
00037 }
00038
00039 ######################################################################
00040 # Returns the current value of fold enable
00041 proc get_vim_foldenable {txt} {
00042
00043 if {[catch { $txt gutter hide folding } rc] || ($rc == 1)} {
00044 return 0
00045 }
00046
00047 return 1
00048
00049 }
00050
00051 ######################################################################
00052 # Returns the indentation method based on the values of enable and the
00053 # current indentation mode.
00054 proc get_method {txt} {
00055
00056 variable enable
00057
00058 if {[info exists enable($txt)] && $enable($txt)} {
00059 switch [indent::get_indent_mode $txt] {
00060 "OFF" { return "manual" }
00061 "IND" { return "indent" }
00062 "IND+" { return [expr {[indent::is_auto_indent_available $txt] ? "syntax" : "indent"}] }
00063 default { return "none" }
00064 }
00065 } else {
00066 return "none"
00067 }
00068
00069 }
00070
00071 ######################################################################
00072 # Returns true if the given position contains a fold point.
00073 proc fold_state {txt line} {
00074
00075 if {[set state [$txt gutter get folding $line]] ne ""} {
00076 return $state
00077 }
00078
00079 return "none"
00080
00081 }
00082
00083 ######################################################################
00084 # Returns a value of true if at least one of the specified folding marker
00085 # exists; otherwise, returns true.
00086 proc fold_state_exists {txt state} {
00087
00088 return [expr [llength [$txt gutter get folding $state]] > 0]
00089
00090 }
00091
00092 ######################################################################
00093 # Adds the bindings necessary for code folding to work.
00094 proc initialize {txt} {
00095
00096 # Set the fold enable
00097 set_fold_enable $txt [preferences::get View/EnableCodeFolding]
00098
00099 }
00100
00101 ######################################################################
00102 # Called whenever the text widget is destroyed.
00103 proc handle_destroy_txt {txt} {
00104
00105 variable enable
00106
00107 unset -nocomplain enable($txt)
00108
00109 }
00110
00111 ######################################################################
00112 # Set the fold enable to the given type.
00113 proc set_fold_enable {txt value} {
00114
00115 variable enable
00116
00117 if {[set enable($txt) $value]} {
00118 enable_folding $txt
00119 add_folds $txt 1.0 end
00120 } else {
00121 disable_folding $txt
00122 }
00123
00124 }
00125
00126 ######################################################################
00127 # Sets the value of the Vim foldenable indicator to the given boolean
00128 # value. Updates the UI state accordingly.
00129 proc set_vim_foldenable {txt value} {
00130
00131 if {$value == [$txt gutter hide folding]} {
00132 if {$value} {
00133 restore_folds $txt
00134 $txt gutter hide folding 0
00135 } else {
00136 $txt tag remove __folded 1.0 end
00137 $txt gutter hide folding 1
00138 }
00139 }
00140
00141 }
00142
00143 ######################################################################
00144 # Disables code folding in the given text widget.
00145 proc disable_folding {txt} {
00146
00147 # Remove all folded text
00148 $txt tag remove __folded 1.0 end
00149
00150 # Remove the gutter
00151 $txt gutter destroy folding
00152
00153 }
00154
00155 ######################################################################
00156 # Enables code folding in the current text widget.
00157 proc enable_folding {txt} {
00158
00159 # Add the folding gutter
00160 $txt gutter create folding \
00161 open [list -symbol \u25be -onclick [list folding::close_fold 1] -onshiftclick [list folding::close_fold 0]] \
00162 close [list -symbol \u25b8 -onclick [list folding::open_fold 1] -onshiftclick [list folding::open_fold 0]] \
00163 eopen [list -symbol \u25be -onclick [list folding::close_fold 1] -onshiftclick [list folding::close_fold 0]] \
00164 eclose [list -symbol \u25b8 -onclick [list folding::open_fold 1] -onshiftclick [list folding::open_fold 0]] \
00165 end [list -symbol \u221f]
00166
00167 # Restart the folding
00168 restart $txt
00169
00170 # Update the closed marker color
00171 update_closed $txt
00172
00173 }
00174
00175 ######################################################################
00176 # Restarts the folder after the text widget has had its tags cleared.
00177 proc restart {txt} {
00178
00179 $txt.t tag configure __folded -elide 1
00180 $txt.t tag place __folded invisible
00181
00182 }
00183
00184 ######################################################################
00185 # Update the closed marker colors.
00186 proc update_closed {txt} {
00187
00188 if {[lsearch [$txt gutter names] folding] != -1} {
00189
00190 array set theme [theme::get_syntax_colors]
00191
00192 # Update the folding color
00193 $txt gutter configure folding close -fg $theme(closed_fold)
00194 $txt gutter configure folding eclose -fg $theme(closed_fold)
00195
00196 }
00197
00198 }
00199
00200 ######################################################################
00201 # Adds any found folds to the gutter
00202 proc add_folds {txt startpos endpos} {
00203
00204 set method [get_method $txt]
00205
00206 # If we are doing manual code folding, don't go any further
00207 if {$method eq "manual"} {
00208 return
00209
00210 # Get the starting and ending line
00211 } elseif {$method eq "indent"} {
00212 set startpos 1.0
00213 if {[set range [$txt syntax prevrange prewhite "$startpos lineend"]] ne ""} {
00214 set startpos [lindex $range 0]
00215 }
00216 }
00217
00218 set startline [lindex [split [$txt index $startpos] .] 0]
00219 set endline [lindex [split [$txt index $endpos] .] 0]
00220 set lines(open) [list]
00221 set lines(end) [list]
00222 set lines(eopen) [list]
00223
00224 # Clear the folding gutter in
00225 $txt gutter clear folding $startline $endline
00226
00227 # Add the folding indicators
00228 for {set i $startline} {$i <= $endline} {incr i} {
00229 lappend lines([check_fold $txt $i]) $i
00230 }
00231
00232 $txt gutter set folding open $lines(open) end $lines(end) eopen $lines(eopen)
00233
00234 }
00235
00236 ######################################################################
00237 # Returns true if a fold point has been detected at the given index.
00238 proc check_fold {txt line} {
00239
00240 set indent_cnt 0
00241 set unindent_cnt 0
00242
00243 switch [get_method $txt] {
00244 syntax {
00245 set indent_cnt [indent::get_tag_count $txt.t indent $line.0 $line.end]
00246 set unindent_cnt [indent::get_tag_count $txt.t unindent $line.0 $line.end]
00247 }
00248 indent {
00249 if {[$txt syntax contains prewhite $line.0]} {
00250 set prev 0
00251 set curr 0
00252 set next 0
00253 catch { set prev [$txt count -chars {*}[$txt syntax prevrange prewhite $line.0]] }
00254 catch { set curr [$txt count -chars {*}[$txt syntax nextrange prewhite $line.0]] }
00255 catch { set next [$txt count -chars {*}[$txt syntax nextrange prewhite $line.0+1c]] }
00256 set indent_cnt [expr $curr < $next]
00257 set unindent_cnt [expr $curr < $prev]
00258 if {$indent_cnt && $unindent_cnt} {
00259 return "eopen"
00260 }
00261 }
00262 }
00263 }
00264
00265 return [expr {($indent_cnt > $unindent_cnt) ? "open" : ($indent_cnt < $unindent_cnt) ? "end" : ""}]
00266
00267 }
00268
00269 ######################################################################
00270 # Returns the gutter information in sorted order.
00271 proc get_gutter_info {txt} {
00272
00273 set data [list]
00274
00275 foreach tag [list open close eopen eclose end] {
00276 foreach tline [$txt gutter get folding $tag] {
00277 lappend data [list $tline $tag]
00278 }
00279 }
00280
00281 return [lsort -integer -index 0 $data]
00282
00283 }
00284
00285 ######################################################################
00286 # Returns the starting and ending positions of the range to fold.
00287 proc get_fold_range {txt line depth} {
00288
00289 set count 0
00290 set aboves [list]
00291 set belows [list]
00292 set closed [list]
00293
00294 if {[get_method $txt] eq "indent"} {
00295
00296 set start_chars [$txt count -chars {*}[$txt syntax nextrange prewhite $line.0]]
00297 set next_line $line.0
00298 set final [lindex [split [$txt index end] .] 0].0
00299 set all_chars [list]
00300
00301 while {[set range [$txt syntax nextrange prewhite $next_line]] ne ""} {
00302 set chars [$txt count -chars {*}$range]
00303 set tline [lindex [split [lindex $range 0] .] 0]
00304 set state [fold_state $txt $tline]
00305 if {($state eq "close") || ($state eq "eclose")} {
00306 lappend closed $tline
00307 }
00308 if {($chars > $start_chars) || ($all_chars eq [list])} {
00309 if {($state ne "none") && ($state ne "end")} {
00310 lappend all_chars [list $tline $chars]
00311 }
00312 } else {
00313 set final $tline.0
00314 break
00315 }
00316 set next_line [lindex $range 1]
00317 }
00318
00319 set last $start_chars
00320 foreach {tline chars} [concat {*}[lsort -integer -index 1 $all_chars]] {
00321 incr count [expr $chars != $last]
00322 if {$count < $depth} {
00323 lappend belows $tline
00324 } else {
00325 lappend aboves $tline
00326 }
00327 set last $chars
00328 }
00329
00330 return [list [expr $line + 1].0 $final $belows $aboves $closed]
00331
00332 } else {
00333
00334 array set inc [list end -1 open 1 close 1 eopen -1 eclose -1]
00335
00336 set index [lsearch -index 0 [set data [get_gutter_info $txt]] $line]
00337
00338 foreach {tline tag} [concat {*}[lrange $data $index end]] {
00339 if {$tag ne "end"} {
00340 if {$count < $depth} {
00341 lappend belows $tline
00342 } else {
00343 lappend aboves $tline
00344 }
00345 if {($tag eq "close") || ($tag eq "eclose")} {
00346 lappend closed $tline
00347 }
00348 }
00349 if {[incr count $inc($tag)] == 0} {
00350 return [list [expr $line + 1].0 $tline.0 $belows $aboves $closed]
00351 } elseif {$count < 0} {
00352 set count 0
00353 } elseif {($tag eq "eopen") || ($tag eq "eclose")} {
00354 incr count
00355 }
00356 }
00357
00358 return [list [expr $line + 1].0 [lindex [split [$txt index end] .] 0].0 $belows $aboves $closed]
00359
00360 }
00361
00362 }
00363
00364 ######################################################################
00365 # Returns the line number of the highest level folding marker that contains
00366 # the given line.
00367 proc show_line {txt line} {
00368
00369 if {![get_vim_foldenable $txt]} {
00370 return
00371 }
00372
00373 array set counts [list open -1 close -1 eopen 0 eclose 0 end 1]
00374
00375 # Find our current position
00376 set data [lsort -integer -index 0 [list [list $line current] {*}[get_gutter_info $txt]]]
00377 set index [lsearch $data [list $line current]]
00378 set count 1
00379
00380 for {set i [expr $index - 1]} {$i >= 0} {incr i -1} {
00381 lassign [lindex $data $i] line tag
00382 if {[incr count $counts($tag)] == 0} {
00383 open_fold 1 $txt $line
00384 if {[lsearch [$txt tag names $line.0] __folded] == -1} {
00385 return
00386 }
00387 set count 1
00388 }
00389 }
00390
00391 }
00392
00393 ######################################################################
00394 # Restores the eliding based on the stored values within the given text
00395 # widget.
00396 proc restore_folds {txt} {
00397
00398 foreach line [$txt gutter get folding close] {
00399 lassign [get_fold_range $txt $line 1] startpos endpos
00400 $txt tag add __folded $startpos $endpos
00401 }
00402
00403 }
00404
00405 ######################################################################
00406 # Toggles the fold for the given line.
00407 proc toggle_fold {txt line {depth 1}} {
00408
00409 # If foldenable is 0, return immediately
00410 if {![get_vim_foldenable $txt]} {
00411 return
00412 }
00413
00414 switch [$txt gutter get folding $line] {
00415 open -
00416 eopen { close_fold $depth $txt $line }
00417 close -
00418 eclose { open_fold $depth $txt $line }
00419 }
00420
00421 }
00422
00423 ######################################################################
00424 # Toggles all folds.
00425 proc toggle_all_folds {txt} {
00426
00427 # If foldenable is 0, return immediately
00428 if {![get_vim_foldenable $txt]} {
00429 return
00430 }
00431
00432 if {[$txt gutter get folding open] ne [list]} {
00433 close_all_folds $txt
00434 } else {
00435 open_all_folds $txt
00436 }
00437
00438 }
00439
00440 ######################################################################
00441 # Close the selected range.
00442 proc close_range {txt startpos endpos} {
00443
00444 # If foldenable is 0, return immediately
00445 if {![get_vim_foldenable $txt]} {
00446 return
00447 }
00448
00449 if {[get_method $txt] eq "manual"} {
00450
00451 lassign [split [$txt index $startpos] .] start_line start_col
00452 lassign [split [$txt index $endpos] .] end_line end_col
00453
00454 $txt tag add __folded [expr $start_line + 1].0 [expr $end_line + 1].0
00455 $txt gutter set folding close $start_line
00456 $txt gutter set folding end [expr $end_line + 1]
00457
00458 }
00459
00460 }
00461
00462 ######################################################################
00463 # Close the selected text.
00464 proc close_selected {txt} {
00465
00466 set retval 0
00467
00468 # If foldenable is 0, return immediately
00469 if {![get_vim_foldenable $txt]} {
00470 return $retval
00471 }
00472
00473 if {[get_method $txt] eq "manual"} {
00474
00475 foreach {endpos startpos} [lreverse [$txt tag ranges sel]] {
00476 close_range $txt $startpos $endpos-1c
00477 set retval 1
00478 }
00479
00480 # Clear the selection
00481 $txt tag remove sel 1.0 end
00482
00483 }
00484
00485 return $retval
00486
00487 }
00488
00489 ######################################################################
00490 # Attempts to delete the closed fold marker (if it exists). This operation
00491 # is only valid in manual mode.
00492 proc delete_fold {txt line} {
00493
00494 # If foldenable is 0, return immediately
00495 if {![get_vim_foldenable $txt]} {
00496 return $retval
00497 }
00498
00499 if {[get_method $txt] eq "manual"} {
00500
00501 # Get the current line state
00502 set state [fold_state $txt $line]
00503
00504 # Open the fold if it is closed
00505 if {$state eq "close"} {
00506 open_fold 1 $txt $line
00507 }
00508
00509 # Remove the start/end markers for the current fold
00510 if {($state eq "close") || ($state eq "open")} {
00511 lassign [get_fold_range $txt $line 1] startpos endpos
00512 $txt gutter clear folding $line
00513 $txt gutter clear folding [lindex [split $endpos .] 0]
00514 return $endpos
00515 }
00516
00517 }
00518
00519 }
00520
00521 ######################################################################
00522 # Delete all folds between the first and last lines of the current
00523 # open/close fold.
00524 proc delete_folds {txt line} {
00525
00526 # If foldenable is 0, return immediately
00527 if {![get_vim_foldenable $txt]} {
00528 return $retval
00529 }
00530
00531 if {[get_method $txt] eq "manual"} {
00532
00533 # Get the current line state
00534 set state [fold_state $txt $line]
00535
00536 # Open the fold recursively if it is closed
00537 if {$state eq "close"} {
00538 open_fold 0 $txt $line
00539 }
00540
00541 # If the line is closed or opened, continue with the recursive deletion
00542 if {($state eq "close") || ($state eq "open")} {
00543 lassign [get_fold_range $txt $line 1] startpos endpos
00544 $txt gutter clear folding $line [lindex [split $endpos .] 0]
00545 }
00546
00547 }
00548
00549 }
00550
00551 ######################################################################
00552 # Deletes all fold markers found in the given range.
00553 proc delete_folds_in_range {txt startline endline} {
00554
00555 # If foldenable is 0, return immediately
00556 if {![get_vim_foldenable $txt]} {
00557 return $retval
00558 }
00559
00560 if {[get_method $txt] eq "manual"} {
00561
00562 # Get all of the open/close folds
00563 set lines [lsort -integer [list $startline {*}[$txt gutter get folding open] {*}[$txt gutter get folding close]]]
00564 set lineslen [llength $lines]
00565 set index [expr [lsearch $lines $startline] + 1]
00566
00567 while {($index < $lineslen) && ([lindex $lines $index] <= $endline)} {
00568 delete_fold $txt [lindex $lines $index]
00569 incr index
00570 }
00571
00572 }
00573
00574 }
00575
00576 ######################################################################
00577 # Deletes all fold markers. This operation is only valid in manual
00578 # mode.
00579 proc delete_all_folds {txt} {
00580
00581 # If foldenable is 0, return immediately
00582 if {![get_vim_foldenable $txt]} {
00583 return $retval
00584 }
00585
00586 if {[get_method $txt] eq "manual"} {
00587
00588 # Remove all folded text
00589 $txt tag remove __folded 1.0 end
00590
00591 # Clear all of fold indicators in the gutter
00592 $txt gutter clear folding 1 [lindex [split [$txt index end] .] 0]
00593
00594 }
00595
00596 }
00597
00598 ######################################################################
00599 # Closes a fold, hiding the contents.
00600 proc close_fold {depth txt line} {
00601
00602 # If foldenable is 0, return immediately
00603 if {![get_vim_foldenable $txt]} {
00604 return $retval
00605 }
00606
00607 array set map {
00608 open close
00609 close close
00610 eopen eclose
00611 eclose eclose
00612 }
00613
00614 # Get the fold range
00615 lassign [get_fold_range $txt $line [expr ($depth == 0) ? 100000 : $depth]] startpos endpos belows
00616
00617 # Replace the open/eopen symbol with the close/eclose symbol
00618 foreach line $belows {
00619 set type [$txt gutter get folding $line]
00620 $txt gutter clear folding $line
00621 $txt gutter set folding $map($type) $line
00622 }
00623
00624 # Hide the text
00625 $txt tag add __folded $startpos $endpos
00626
00627 return $endpos
00628
00629 }
00630
00631 ######################################################################
00632 # Close all folds in the given range.
00633 proc close_folds_in_range {txt startline endline depth} {
00634
00635 # If foldenable is 0, return immediately
00636 if {![get_vim_foldenable $txt]} {
00637 return $retval
00638 }
00639
00640 # Get all of the open folds
00641 set open_lines [$txt gutter get folding open]
00642
00643 while {$startline <= $endline} {
00644 set lines [lsort -integer [list $startline {*}$open_lines]]
00645 set index [expr [lsearch $lines $startline] + 1]
00646 set startline [lindex [split [close_fold $depth $txt [lindex $lines $index]] .] 0]
00647 }
00648
00649 }
00650
00651 ######################################################################
00652 # Closes all open folds.
00653 proc close_all_folds {txt} {
00654
00655 # If foldenable is 0, return immediately
00656 if {![get_vim_foldenable $txt]} {
00657 return $retval
00658 }
00659
00660 array set inc [list end -1 open 1 close 1 eopen 0 eclose 0]
00661
00662 # Get ordered gutter list
00663 set ranges [list]
00664 set count 0
00665 set oline ""
00666 foreach {tline tag} [concat {*}[get_gutter_info $txt]] {
00667 if {($count == 0) && (($tag eq "open") || ($tag eq "eopen"))} {
00668 set oline [expr $tline + 1]
00669 }
00670 if {[incr count $inc($tag)] == 0} {
00671 if {$oline ne ""} {
00672 lappend ranges $oline.0 $tline.0
00673 set oline ""
00674 }
00675 } elseif {$count < 0} {
00676 set count 0
00677 }
00678 }
00679
00680 if {[llength $ranges] > 0} {
00681
00682 # Close the folds
00683 $txt gutter set folding close [$txt gutter get folding open]
00684 $txt gutter set folding eclose [$txt gutter get folding eopen]
00685
00686 # Adds folds
00687 $txt tag add __folded {*}$ranges
00688
00689 }
00690
00691 }
00692
00693 ######################################################################
00694 # Opens a fold, showing the contents.
00695 proc open_fold {depth txt line} {
00696
00697 # If foldenable is 0, return immediately
00698 if {![get_vim_foldenable $txt]} {
00699 return $retval
00700 }
00701
00702 array set map {
00703 close open
00704 open open
00705 eclose eopen
00706 eopen eopen
00707 }
00708
00709 # Get the fold range
00710 lassign [get_fold_range $txt $line [expr ($depth == 0) ? 100000 : $depth]] startpos endpos belows aboves closed
00711
00712 foreach tline [concat $belows $aboves] {
00713 set type [$txt gutter get folding $line]
00714 $txt gutter clear folding $tline
00715 $txt gutter set folding $map($type) $tline
00716 }
00717
00718 # Remove the folded tag
00719 $txt tag remove __folded $startpos $endpos
00720
00721 # Close all of the previous folds
00722 if {$depth > 0} {
00723 foreach tline [::struct::set intersect $aboves $closed] {
00724 close_fold 1 $txt $tline
00725 }
00726 }
00727
00728 return $endpos
00729
00730 }
00731
00732 ######################################################################
00733 # Open all folds in the given range.
00734 proc open_folds_in_range {txt startline endline depth} {
00735
00736 # If foldenable is 0, return immediately
00737 if {![get_vim_foldenable $txt]} {
00738 return $retval
00739 }
00740
00741 # Get all of the closed folds
00742 set close_lines [$txt gutter get folding close]
00743
00744 while {$startline <= $endline} {
00745 set lines [lsort -integer [list $startline {*}$close_lines]]
00746 set index [expr [lsearch $lines $startline] + 1]
00747 set startline [lindex [split [open_fold $depth $txt [lindex $lines $index]] .] 0]
00748 }
00749
00750 }
00751
00752 ######################################################################
00753 # Opens all closed folds.
00754 proc open_all_folds {txt} {
00755
00756 # If foldenable is 0, return immediately
00757 if {![get_vim_foldenable $txt]} {
00758 return $retval
00759 }
00760
00761 $txt tag remove __folded 1.0 end
00762 $txt gutter set folding open [$txt gutter get folding close]
00763 $txt gutter set folding eopen [$txt gutter get folding eclose]
00764
00765 }
00766
00767 ######################################################################
00768 # Jumps to the next or previous folding.
00769 proc jump_to {txt dir {num 1}} {
00770
00771 # If foldenable is 0, return immediately
00772 if {![get_vim_foldenable $txt]} {
00773 return $retval
00774 }
00775
00776 # Get a sorted list of open/close tags and locate our current position
00777 set data [set line [lindex [split [$txt index insert] .] 0]]
00778 foreach tag [list close eclose] {
00779 lappend data {*}[$txt gutter get folding $tag]
00780 }
00781
00782 # Find the index of the close symbols and set the cursor on the line
00783 if {[set index [lsearch [set data [lsort -unique -integer -index 0 $data]] $line]] != -1} {
00784 if {$dir eq "next"} {
00785 if {[incr index $num] == [llength $data]} {
00786 return
00787 }
00788 } else {
00789 if {[incr index -$num] < 0} {
00790 return
00791 }
00792 }
00793 ::tk::TextSetCursor $txt [lindex $data $index].0
00794 vim::adjust_insert $txt.t
00795 }
00796
00797 }
00798
00799 ######################################################################
00800 # Returns true if the specified index exists within a fold.
00801 proc is_folded {txt index} {
00802
00803 return [expr [lsearch -exact [$txt tag names $index] __folded] != -1]
00804
00805 }
00806
00807 }