1
1
mirror of https://github.com/walles/moar.git synced 2024-11-30 12:42:26 +03:00
moar/m/screenLines.go
2022-10-28 06:59:45 +02:00

306 lines
8.6 KiB
Go

package m
import (
"fmt"
"github.com/walles/moar/twin"
)
type renderedLine struct {
inputLineOneBased int
// If an input line has been wrapped into two, the part on the second line
// will have a wrapIndex of 1.
wrapIndex int
cells []twin.Cell
// Used for rendering clear-to-end-of-line control sequences:
// https://en.wikipedia.org/wiki/ANSI_escape_code#EL
//
// Ref: https://github.com/walles/moar/issues/106
trailer twin.Style
}
// Refresh the whole pager display, both contents lines and the status line at
// the bottom
func (p *Pager) redraw(spinner string) {
p.screen.Clear()
lastUpdatedScreenLineNumber := -1
var renderedScreenLines [][]twin.Cell
renderedScreenLines, statusText := p.renderScreenLines()
for lineNumber, row := range renderedScreenLines {
lastUpdatedScreenLineNumber = lineNumber
for column, cell := range row {
p.screen.SetCell(column, lastUpdatedScreenLineNumber, cell)
}
}
// Status line code follows
eofSpinner := spinner
if eofSpinner == "" {
// This happens when we're done
eofSpinner = "---"
}
spinnerLine := cellsFromString(_EofMarkerFormat + eofSpinner).Cells
for column, cell := range spinnerLine {
p.screen.SetCell(column, lastUpdatedScreenLineNumber+1, cell)
}
switch p.mode {
case _Searching:
p.addSearchFooter()
case _NotFound:
p.setFooter("Not found: " + p.searchString)
case _GotoLine:
p.addGotoLineFooter()
case _Viewing:
helpText := "Press ESC / q to exit, '/' to search, '?' for help"
if p.isShowingHelp {
helpText = "Press ESC / q to exit help, '/' to search"
}
if p.ShowStatusBar {
p.setFooter(statusText + spinner + " " + helpText)
}
default:
panic(fmt.Sprint("Unsupported pager mode: ", p.mode))
}
p.screen.Show()
}
// Render screen lines into an array of lines consisting of Cells.
//
// At most height - 1 lines will be returned, leaving room for one status line.
//
// The lines returned by this method are decorated with horizontal scroll
// markers and line numbers and are ready to be output to the screen.
func (p *Pager) renderScreenLines() (lines [][]twin.Cell, statusText string) {
renderedLines, statusText := p.renderLines()
if len(renderedLines) == 0 {
return
}
// Construct the screen lines to return
screenLines := make([][]twin.Cell, 0, len(renderedLines))
for _, renderedLine := range renderedLines {
screenLines = append(screenLines, renderedLine.cells)
if renderedLine.trailer == twin.StyleDefault {
continue
}
// Fill up with the trailer
screenWidth, _ := p.screen.Size()
for len(screenLines[len(screenLines)-1]) < screenWidth {
screenLines[len(screenLines)-1] =
append(screenLines[len(screenLines)-1], twin.NewCell(' ', renderedLine.trailer))
}
}
return screenLines, statusText
}
// Render all lines that should go on the screen.
//
// Returns both the lines and a suitable status text.
//
// The returned lines are display ready, meaning that they come with horizontal
// scroll markers and line numbers as necessary.
//
// The maximum number of lines returned by this method is limited by the screen
// height. If the status line is visible, you'll get at most one less than the
// screen height from this method.
func (p *Pager) renderLines() ([]renderedLine, string) {
_, height := p.screen.Size()
wantedLineCount := height - 1
if !p.ShowStatusBar {
wantedLineCount = height
}
inputLines := p.reader.GetLines(p.lineNumberOneBased(), wantedLineCount)
if inputLines.lines == nil {
// Empty input, empty output
return []renderedLine{}, inputLines.statusText
}
allLines := make([]renderedLine, 0)
for lineIndex, line := range inputLines.lines {
lineNumber := inputLines.firstLineOneBased + lineIndex
allLines = append(allLines, p.renderLine(line, lineNumber)...)
}
// Find which index in allLines the user wants to see at the top of the
// screen
firstVisibleIndex := -1 // Not found
for index, line := range allLines {
if p.lineNumberOneBased() == 0 {
// Expected zero lines but got some anyway, grab the first one!
firstVisibleIndex = index
break
}
if line.inputLineOneBased == p.lineNumberOneBased() && line.wrapIndex == p.deltaScreenLines() {
firstVisibleIndex = index
break
}
}
if firstVisibleIndex == -1 {
panic(fmt.Errorf("scrollPosition %#v not found in allLines size %d",
p.scrollPosition, len(allLines)))
}
// Drop the lines that should go above the screen
allLines = allLines[firstVisibleIndex:]
if len(allLines) < wantedLineCount {
// Screen has enough room for everything, return everything
return allLines, inputLines.statusText
}
return allLines[0:wantedLineCount], inputLines.statusText
}
// Render one input line into one or more screen lines.
//
// The returned line is display ready, meaning that it comes with horizontal
// scroll markers and line number as necessary.
//
// lineNumber and numberPrefixLength are required for knowing how much to
// indent, and to (optionally) render the line number.
func (p *Pager) renderLine(line *Line, lineNumber int) []renderedLine {
highlighted := line.HighlightedTokens(p.searchPattern)
var wrapped [][]twin.Cell
if p.WrapLongLines {
width, _ := p.screen.Size()
wrapped = wrapLine(width-p.numberPrefixLength(), highlighted.Cells)
} else {
// All on one line
wrapped = [][]twin.Cell{highlighted.Cells}
}
rendered := make([]renderedLine, 0)
for wrapIndex, inputLinePart := range wrapped {
visibleLineNumber := &lineNumber
if wrapIndex > 0 {
visibleLineNumber = nil
}
rendered = append(rendered, renderedLine{
inputLineOneBased: lineNumber,
wrapIndex: wrapIndex,
cells: p.decorateLine(visibleLineNumber, inputLinePart),
})
}
if highlighted.Trailer != twin.StyleDefault {
// In the presence of wrapping, add the trailer to the last of the wrap
// lines only. This matches what both iTerm and the macOS Terminal does.
rendered[len(rendered)-1].trailer = highlighted.Trailer
}
return rendered
}
// Take a rendered line and decorate as needed:
// * Line number, or leading whitespace for wrapped lines
// * Scroll left indicator
// * Scroll right indicator
func (p *Pager) decorateLine(lineNumberToShow *int, contents []twin.Cell) []twin.Cell {
width, _ := p.screen.Size()
newLine := make([]twin.Cell, 0, width)
numberPrefixLength := p.numberPrefixLength()
newLine = append(newLine, createLinePrefix(lineNumberToShow, numberPrefixLength)...)
startColumn := p.leftColumnZeroBased
if startColumn < len(contents) {
endColumn := p.leftColumnZeroBased + (width - numberPrefixLength)
if endColumn > len(contents) {
endColumn = len(contents)
}
newLine = append(newLine, contents[startColumn:endColumn]...)
}
// Add scroll left indicator
if p.leftColumnZeroBased > 0 && len(contents) > 0 {
if len(newLine) == 0 {
// Don't panic on short lines, this new Cell will be
// overwritten with '<' right after this if statement
newLine = append(newLine, twin.Cell{})
}
// Add can-scroll-left marker
newLine[0] = p.ScrollLeftHint
}
// Add scroll right indicator
if len(contents)+numberPrefixLength-p.leftColumnZeroBased > width {
newLine[width-1] = p.ScrollRightHint
}
return newLine
}
// Generate a line number prefix of the given length.
//
// Can be empty or all-whitespace depending on parameters.
func createLinePrefix(fileLineNumber *int, numberPrefixLength int) []twin.Cell {
if numberPrefixLength == 0 {
return []twin.Cell{}
}
lineNumberPrefix := make([]twin.Cell, 0, numberPrefixLength)
if fileLineNumber == nil {
for len(lineNumberPrefix) < numberPrefixLength {
lineNumberPrefix = append(lineNumberPrefix, twin.Cell{Rune: ' '})
}
return lineNumberPrefix
}
lineNumberString := formatNumber(uint(*fileLineNumber))
lineNumberString = fmt.Sprintf("%*s ", numberPrefixLength-1, lineNumberString)
if len(lineNumberString) > numberPrefixLength {
panic(fmt.Errorf(
"lineNumberString <%s> longer than numberPrefixLength %d",
lineNumberString, numberPrefixLength))
}
for column, digit := range lineNumberString {
if column >= numberPrefixLength {
break
}
lineNumberPrefix = append(lineNumberPrefix, twin.NewCell(digit, _numberStyle))
}
return lineNumberPrefix
}
// Is the given position visible on screen?
func (p *Pager) isVisible(scrollPosition scrollPosition) bool {
if scrollPosition.lineNumberOneBased(p) < p.lineNumberOneBased() {
// It's above the screen, not visible
return false
}
lastVisiblePosition := p.getLastVisiblePosition()
if scrollPosition.lineNumberOneBased(p) > lastVisiblePosition.lineNumberOneBased(p) {
// Line number too high, not visible
return false
}
if scrollPosition.deltaScreenLines(p) > lastVisiblePosition.deltaScreenLines(p) {
// Sub-line-number too high, not visible
return false
}
return true
}