builtin-programs/editor/editor-utils.folk

Claim the editor utils library is [library create editorUtilsLib {
    proc applyTextViewport {originalText x y width height} {
        set lines [split $originalText \n]
        set lines [lrange $lines $y [expr {($height - 1) + $y}]]
        set lines [lmap line $lines {
            set line [string range $line $x [expr {($width - 1) + $x}]]
        }]
        return [join $lines \n]
    }

    proc cursorToXy {code cursor} {
        set codeBeforeCursor [string range $code 0 [- $cursor 1]]
        set linesBeforeCursor [split $codeBeforeCursor "\n"]
        set lineCountBeforeCursor [llength $linesBeforeCursor]

        set cursorX [string length [lindex $linesBeforeCursor end]]
        set cursorY [max [- $lineCountBeforeCursor 1] 0]

        return [list $cursorX $cursorY]
    }

    proc xyToCursor {code cursorX cursorY} {
        if { $cursorX < 0 } { set cursorX 0 }
        if { $cursorY < 0 } { set cursorY 0 }

        set lines [split $code "\n"]
        set maxCursorY [max 0 [- [llength $lines] 1]]
        set cursorY [min $cursorY $maxCursorY]

        set relevantLines [lrange $lines 0 [- $cursorY 1]]
        set relevantLineLen [string length [lindex $lines $cursorY]]
        set joined [join $relevantLines "\n"]
        # make sure cursorX < line length
        set cursorX [min $cursorX $relevantLineLen]
        set cursor [+ [string length $joined] $cursorX]
        # don't forget to add the length of \n at the beginning
        if {$cursorY > 0} { incr cursor }

        return $cursor
    }

    proc insertText {code cursor newText} {
        set before [string range $code 0 [- $cursor 1]]
        set after [string range $code $cursor end]
        set joined [join [list $before $newText $after] ""]

        incr cursor
        lassign [cursorToXy $code $cursor] cursorX cursorY
        set maxCursorX $cursorX

        return [list $joined $cursor $maxCursorX]
    }

    proc deleteText {code cursor count} {
        set before [string range $code 0 [- $cursor 1]]
        set after [string range $code [+ $cursor $count] end]
        set joined [join [list $before $after] ""]

        return $joined
    }

    proc deleteToBeginning {code cursor} {
        lassign [cursorToXy $code $cursor] x y

        set lines [split $code "\n"]
        set line [lindex $lines $y]
        set newLine [string range $line $x end]
        lset lines $y $newLine
        return [join $lines "\n"]
    }

    proc getLine {code cursor} {
        lassign [cursorToXy $code $cursor] x y

        set lines [split $code "\n"]
        set line [lindex $lines $y]
    }

    proc getLineLength {code cursor} {
        set line [getLine $code $cursor]
        set ll [string length $line]
        return $ll
    }

    # returns {newCursor newMaxCursorX}
    proc handleNavigation {key code cursor maxCursorX} {
        switch $key {
            Left {
                set cursor [- $cursor 1]
                set cursor [max $cursor 0]

                lassign [cursorToXy $code $cursor] cursorX cursorY
                set maxCursorX $cursorX
            }
            Right {
                set cursor [+ $cursor 1]
                set codeLength [string length $code]
                set cursor [min $cursor $codeLength]

                lassign [cursorToXy $code $cursor] cursorX cursorY
                set maxCursorX $cursorX
            }
            Up {
                lassign [cursorToXy $code $cursor] cursorX cursorY
                set cursorX $maxCursorX
                set cursorY [- $cursorY 1]
                set cursor [xyToCursor $code $cursorX $cursorY]
            }
            Down {
                lassign [cursorToXy $code $cursor] cursorX cursorY
                set cursorX $maxCursorX
                set cursorY [+ $cursorY 1]
                set cursor [xyToCursor $code $cursorX $cursorY]
            }
            Control_a {
                lassign [cursorToXy $code $cursor] cursorX cursorY
                set newX 0
                set maxCursorX $newX
                set cursor [xyToCursor $code $newX $cursorY]
            }
            Control_e {
                lassign [cursorToXy $code $cursor] cursorX cursorY
                set newX [getLineLength $code $cursor]
                set maxCursorX $newX
                set cursor [xyToCursor $code $newX $cursorY]
            }
        }

        return [list $cursor $maxCursorX]
    }

    # returns {newCode newCursor newMaxCursorX}
    proc handleRemovalAndReturn {key code cursor maxCursorX} {
        switch $key {
            Delete {
                if { $cursor != 0 } {
                set cursor [- $cursor 1]
                set code [deleteText $code $cursor 1]
                set maxCursorX [lindex [cursorToXy $code $cursor] 0]
                }
            }
            Remove {
                set code [deleteText $code $cursor 1]
            }
            Control_u {
                # delete from cursor back to 0 and move cursor to 0
                lassign [cursorToXy $code $cursor] cursorX cursorY
                set code [deleteToBeginning $code $cursor]
                set newX 0
                set cursor [xyToCursor $code $newX $cursorY]
            }
            Return {
                # figure out how many spaces there are before the current line
                regexp {^(\s*)} [getLine $code $cursor] -> spacing
                set spacingLen [string length $spacing]
                lassign [insertText $code $cursor "\n$spacing"] code

                set maxCursorX $spacingLen
                set cursor [+ $cursor [+ 1 $spacingLen]]
            }
        }

        return [list $code $cursor $maxCursorX]
    }

    proc getSelectedText {code selAnchor cursor} {
        set start [min $selAnchor $cursor]
        set end [max $selAnchor $cursor]
        return [string range $code $start [- $end 1]]
    }

    proc replaceRange {code rangeStart rangeEnd newText} {
        if {$rangeStart > 0} {
            set before [string range $code 0 [- $rangeStart 1]]
        } else {
            set before ""
        }
        set after [string range $code $rangeEnd end]
        set code "${before}${newText}${after}"
        set cursor [+ $rangeStart [string length $newText]]
        lassign [cursorToXy $code $cursor] cursorX cursorY
        set maxCursorX $cursorX
        return [list $code $cursor $maxCursorX]
    }

    proc lineNumberView {ystart linecount} {
        set yend [expr {$ystart + $linecount}]
        set numbers [list]
        for {set i [expr {$ystart + 1}]} {$i <= $yend} {incr i} {
            lappend numbers $i
        }
        join $numbers "\n"
    }

    # For rendering:

    proc getAdvance {em} {
        # From NeomatrixCode.csv
        return $(0.5859375 * $em)
    }

    proc widthAndHeight {resolvedGeom} {
        set tagSize [dict get $resolvedGeom tagSize]
        set left [dict get $resolvedGeom left]
        set right [dict get $resolvedGeom right]
        set top [dict get $resolvedGeom top]
        set bottom [dict get $resolvedGeom bottom]

        set width $($left + $tagSize + $right)
        set height $($top + $tagSize + $bottom)

        return [list $width $height]
    }

    # given program and the editor options, figure out how many characters can
    # fit in this editor
    proc editorSizeInCharacters {margin resolvedGeom options} {
        set textScale [dict get $options scale]
        set advance [getAdvance $textScale]

        lassign [widthAndHeight $resolvedGeom] width height
        set width $($width - [lindex $margin 3] - $advance*2.5 - [lindex $margin 1])
        set height $($height - [lindex $margin 0] - [lindex $margin 2])

        set widthInCharacters $(int($width / $advance))
        set heightInCharacters $(int($height / $textScale))

        return [list $widthInCharacters $heightInCharacters]
    }
}]