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 }