mirror of
https://github.com/walles/moar.git
synced 2024-09-21 00:49:59 +03:00
Merge branch 'walles/extract-rendering'
Extracts content rendering into its own file.
This commit is contained in:
commit
531951967d
@ -3,7 +3,6 @@ package m
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
@ -100,36 +99,6 @@ func TestTokenize(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHighlightSearchHit(t *testing.T) {
|
||||
pattern, err := regexp.Compile("b")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
line := NewLine("abc")
|
||||
screenLine := createScreenLine(0, 3, line.HighlightedTokens(pattern))
|
||||
assertTokenRangesEqual(t, screenLine, []twin.Cell{
|
||||
twin.NewCell('a', twin.StyleDefault),
|
||||
twin.NewCell('b', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||
twin.NewCell('c', twin.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
func TestHighlightUtf8SearchHit(t *testing.T) {
|
||||
pattern, err := regexp.Compile("ä")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
line := NewLine("åäö")
|
||||
screenLine := createScreenLine(0, 3, line.HighlightedTokens(pattern))
|
||||
assertTokenRangesEqual(t, screenLine, []twin.Cell{
|
||||
twin.NewCell('å', twin.StyleDefault),
|
||||
twin.NewCell('ä', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||
twin.NewCell('ö', twin.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
func TestUnderline(t *testing.T) {
|
||||
tokens := cellsFromString("a\x1b[4mb\x1b[24mc")
|
||||
assert.Equal(t, len(tokens), 3)
|
||||
|
@ -12,7 +12,7 @@ func tokenize(input string) []twin.Cell {
|
||||
return line.HighlightedTokens(nil)
|
||||
}
|
||||
|
||||
func toString(cellLines [][]twin.Cell) string {
|
||||
func rowsToString(cellLines [][]twin.Cell) string {
|
||||
returnMe := ""
|
||||
for _, cellLine := range cellLines {
|
||||
lineString := ""
|
||||
@ -43,7 +43,7 @@ func assertWrap(t *testing.T, input string, width int, wrappedLines ...string) {
|
||||
}
|
||||
|
||||
t.Errorf("When wrapping <%s> at width %d:\n--Expected--\n%s\n\n--Actual--\n%s",
|
||||
input, width, toString(expected), toString(actual))
|
||||
input, width, rowsToString(expected), rowsToString(actual))
|
||||
}
|
||||
|
||||
func TestEnoughRoomNoWrapping(t *testing.T) {
|
||||
|
233
m/pager.go
233
m/pager.go
@ -117,70 +117,6 @@ func NewPager(r *Reader) *Pager {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pager) addLine(fileLineNumber *int, numberPrefixLength int, screenLineNumber int, cells []twin.Cell) {
|
||||
screenWidth, _ := p.screen.Size()
|
||||
|
||||
lineNumberString := ""
|
||||
if numberPrefixLength > 0 && fileLineNumber != nil {
|
||||
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
|
||||
}
|
||||
|
||||
p.screen.SetCell(column, screenLineNumber, twin.NewCell(digit, _numberStyle))
|
||||
}
|
||||
|
||||
screenCells := createScreenLine(p.leftColumnZeroBased, screenWidth-numberPrefixLength, cells)
|
||||
for column, token := range screenCells {
|
||||
p.screen.SetCell(column+numberPrefixLength, screenLineNumber, token)
|
||||
}
|
||||
}
|
||||
|
||||
func createScreenLine(
|
||||
stringIndexAtColumnZero int,
|
||||
screenColumnsCount int,
|
||||
cells []twin.Cell,
|
||||
) []twin.Cell {
|
||||
var returnMe []twin.Cell
|
||||
if stringIndexAtColumnZero > 0 {
|
||||
// Indicate that it's possible to scroll left
|
||||
returnMe = append(returnMe, twin.Cell{
|
||||
Rune: '<',
|
||||
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
|
||||
})
|
||||
}
|
||||
|
||||
if stringIndexAtColumnZero >= len(cells) {
|
||||
// Nothing (more) to display, never mind
|
||||
return returnMe
|
||||
}
|
||||
|
||||
for _, cell := range cells[stringIndexAtColumnZero:] {
|
||||
if len(returnMe) >= screenColumnsCount {
|
||||
// We are trying to add a character to the right of the screen.
|
||||
// Indicate that this line continues to the right.
|
||||
returnMe[len(returnMe)-1] = twin.Cell{
|
||||
Rune: '>',
|
||||
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
returnMe = append(returnMe, cell)
|
||||
}
|
||||
|
||||
return returnMe
|
||||
}
|
||||
|
||||
func (p *Pager) _AddSearchFooter() {
|
||||
_, height := p.screen.Size()
|
||||
|
||||
@ -194,95 +130,6 @@ func (p *Pager) _AddSearchFooter() {
|
||||
p.screen.SetCell(pos, height-1, twin.NewCell(' ', twin.StyleDefault.WithAttr(twin.AttrReverse)))
|
||||
}
|
||||
|
||||
func (p *Pager) _AddLines(spinner string) {
|
||||
width, height := p.screen.Size()
|
||||
wantedLineCount := height - 1
|
||||
|
||||
lines := p.reader.GetLines(p.firstLineOneBased, wantedLineCount)
|
||||
|
||||
// If we're asking for past-the-end lines, the Reader will clip for us,
|
||||
// and we should adapt to that. Otherwise if you scroll 100 lines past
|
||||
// the end, you'll then have to scroll 100 lines up again before the
|
||||
// display starts scrolling visibly.
|
||||
p.firstLineOneBased = lines.firstLineOneBased
|
||||
|
||||
// Count the length of the last line number
|
||||
//
|
||||
// Offsets figured out through trial-and-error...
|
||||
lastLineOneBased := lines.firstLineOneBased + len(lines.lines) - 1
|
||||
numberPrefixLength := len(formatNumber(uint(lastLineOneBased))) + 1
|
||||
if numberPrefixLength < 4 {
|
||||
// 4 = space for 3 digits followed by one whitespace
|
||||
//
|
||||
// https://github.com/walles/moar/issues/38
|
||||
numberPrefixLength = 4
|
||||
}
|
||||
|
||||
if !p.ShowLineNumbers {
|
||||
numberPrefixLength = 0
|
||||
}
|
||||
|
||||
screenLineNumber := 0
|
||||
screenFull := false
|
||||
for lineIndex, line := range lines.lines {
|
||||
lineNumber := p.firstLineOneBased + lineIndex
|
||||
|
||||
highlighted := line.HighlightedTokens(p.searchPattern)
|
||||
var wrapped [][]twin.Cell
|
||||
if p.WrapLongLines {
|
||||
wrapped = wrapLine(width-numberPrefixLength, highlighted)
|
||||
} else {
|
||||
// All on one line
|
||||
wrapped = [][]twin.Cell{highlighted}
|
||||
}
|
||||
|
||||
for wrapIndex, linePart := range wrapped {
|
||||
visibleLineNumber := &lineNumber
|
||||
if wrapIndex > 0 {
|
||||
visibleLineNumber = nil
|
||||
}
|
||||
p.addLine(visibleLineNumber, numberPrefixLength, screenLineNumber, linePart)
|
||||
screenLineNumber++
|
||||
|
||||
if screenLineNumber >= height-1 {
|
||||
// We have shown all the lines that can fit on the screen
|
||||
screenFull = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if screenFull {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
eofSpinner := spinner
|
||||
if eofSpinner == "" {
|
||||
// This happens when we're done
|
||||
eofSpinner = "---"
|
||||
}
|
||||
spinnerLine := cellsFromString(_EofMarkerFormat + eofSpinner)
|
||||
p.addLine(nil, 0, screenLineNumber, spinnerLine)
|
||||
|
||||
switch p.mode {
|
||||
case _Searching:
|
||||
p._AddSearchFooter()
|
||||
|
||||
case _NotFound:
|
||||
p._SetFooter("Not found: " + p.searchString)
|
||||
|
||||
case _Viewing:
|
||||
helpText := "Press ESC / q to exit, '/' to search, '?' for help"
|
||||
if p.isShowingHelp {
|
||||
helpText = "Press ESC / q to exit help, '/' to search"
|
||||
}
|
||||
p._SetFooter(lines.statusText + spinner + " " + helpText)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprint("Unsupported pager mode: ", p.mode))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pager) _SetFooter(footer string) {
|
||||
width, height := p.screen.Size()
|
||||
|
||||
@ -301,7 +148,63 @@ func (p *Pager) _SetFooter(footer string) {
|
||||
func (p *Pager) _Redraw(spinner string) {
|
||||
p.screen.Clear()
|
||||
|
||||
p._AddLines(spinner)
|
||||
width, height := p.screen.Size()
|
||||
wantedLineCount := height - 1
|
||||
|
||||
inputLines := p.reader.GetLines(p.firstLineOneBased, wantedLineCount)
|
||||
screenLines := ScreenLines{
|
||||
inputLines: inputLines,
|
||||
firstInputLineOneBased: p.firstLineOneBased,
|
||||
leftColumnZeroBased: p.leftColumnZeroBased,
|
||||
|
||||
width: width,
|
||||
height: wantedLineCount,
|
||||
|
||||
showLineNumbers: p.ShowLineNumbers,
|
||||
wrapLongLines: p.WrapLongLines,
|
||||
}
|
||||
|
||||
// If we're asking for past-the-end lines, the Reader will clip for us,
|
||||
// and we should adapt to that. Otherwise if you scroll 100 lines past
|
||||
// the end, you'll then have to scroll 100 lines up again before the
|
||||
// display starts scrolling visibly.
|
||||
p.firstLineOneBased = screenLines.firstLineOneBased()
|
||||
|
||||
var screenLineNumber int
|
||||
for lineNumber, row := range screenLines.getScreenLines(p.searchPattern) {
|
||||
screenLineNumber = lineNumber
|
||||
for column, cell := range row {
|
||||
p.screen.SetCell(column, screenLineNumber, cell)
|
||||
}
|
||||
}
|
||||
|
||||
eofSpinner := spinner
|
||||
if eofSpinner == "" {
|
||||
// This happens when we're done
|
||||
eofSpinner = "---"
|
||||
}
|
||||
spinnerLine := cellsFromString(_EofMarkerFormat + eofSpinner)
|
||||
for column, cell := range spinnerLine {
|
||||
p.screen.SetCell(column, screenLineNumber+1, cell)
|
||||
}
|
||||
|
||||
switch p.mode {
|
||||
case _Searching:
|
||||
p._AddSearchFooter()
|
||||
|
||||
case _NotFound:
|
||||
p._SetFooter("Not found: " + p.searchString)
|
||||
|
||||
case _Viewing:
|
||||
helpText := "Press ESC / q to exit, '/' to search, '?' for help"
|
||||
if p.isShowingHelp {
|
||||
helpText = "Press ESC / q to exit help, '/' to search"
|
||||
}
|
||||
p._SetFooter(inputLines.statusText + spinner + " " + helpText)
|
||||
|
||||
default:
|
||||
panic(fmt.Sprint("Unsupported pager mode: ", p.mode))
|
||||
}
|
||||
|
||||
p.screen.Show()
|
||||
}
|
||||
@ -501,6 +404,10 @@ func removeLastChar(s string) string {
|
||||
return s[:len(s)-size]
|
||||
}
|
||||
|
||||
func (p *Pager) _ScrollToEnd() {
|
||||
p.firstLineOneBased = p.reader.GetLineCount()
|
||||
}
|
||||
|
||||
func (p *Pager) _OnSearchKey(key twin.KeyCode) {
|
||||
switch key {
|
||||
case twin.KeyEscape, twin.KeyEnter:
|
||||
@ -515,12 +422,12 @@ func (p *Pager) _OnSearchKey(key twin.KeyCode) {
|
||||
p._UpdateSearchPattern()
|
||||
|
||||
case twin.KeyUp:
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased--
|
||||
p.mode = _Viewing
|
||||
|
||||
case twin.KeyDown:
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased++
|
||||
p.mode = _Viewing
|
||||
|
||||
@ -575,11 +482,11 @@ func (p *Pager) _OnKey(keyCode twin.KeyCode) {
|
||||
p.Quit()
|
||||
|
||||
case twin.KeyUp:
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased--
|
||||
|
||||
case twin.KeyDown, twin.KeyEnter:
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased++
|
||||
|
||||
case twin.KeyRight:
|
||||
@ -639,11 +546,11 @@ func (p *Pager) _OnRune(char rune) {
|
||||
}
|
||||
|
||||
case 'k', 'y':
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased--
|
||||
|
||||
case 'j', 'e':
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased++
|
||||
|
||||
case 'l':
|
||||
@ -658,7 +565,7 @@ func (p *Pager) _OnRune(char rune) {
|
||||
p.firstLineOneBased = 1
|
||||
|
||||
case '>', 'G':
|
||||
p.firstLineOneBased = p.reader.GetLineCount()
|
||||
p._ScrollToEnd()
|
||||
|
||||
case 'f', ' ':
|
||||
_, height := p.screen.Size()
|
||||
@ -772,11 +679,11 @@ func (p *Pager) StartPaging(screen twin.Screen) {
|
||||
log.Tracef("Handling mouse event %d...", event.Buttons())
|
||||
switch event.Buttons() {
|
||||
case twin.MouseWheelUp:
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased--
|
||||
|
||||
case twin.MouseWheelDown:
|
||||
// Clipping is done in _AddLines()
|
||||
// Clipping is done in _Redraw()
|
||||
p.firstLineOneBased++
|
||||
|
||||
case twin.MouseWheelLeft:
|
||||
|
@ -230,53 +230,6 @@ func TestToPattern(t *testing.T) {
|
||||
assert.Assert(t, toPattern(")g").MatchString(")g"))
|
||||
}
|
||||
|
||||
func assertTokenRangesEqual(t *testing.T, actual []twin.Cell, expected []twin.Cell) {
|
||||
if len(actual) != len(expected) {
|
||||
t.Errorf("String lengths mismatch; expected %d but got %d",
|
||||
len(expected), len(actual))
|
||||
}
|
||||
|
||||
for pos, expectedToken := range expected {
|
||||
if pos >= len(expected) || pos >= len(actual) {
|
||||
break
|
||||
}
|
||||
|
||||
actualToken := actual[pos]
|
||||
if actualToken.Rune == expectedToken.Rune && actualToken.Style == expectedToken.Style {
|
||||
// Actual == Expected, keep checking
|
||||
continue
|
||||
}
|
||||
|
||||
t.Errorf("At (0-based) position %d: Expected %v, got %v", pos, expectedToken, actualToken)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateScreenLineBase(t *testing.T) {
|
||||
line := cellsFromString("")
|
||||
screenLine := createScreenLine(0, 3, line)
|
||||
assert.Assert(t, len(screenLine) == 0)
|
||||
}
|
||||
|
||||
func TestCreateScreenLineOverflowRight(t *testing.T) {
|
||||
line := cellsFromString("012345")
|
||||
screenLine := createScreenLine(0, 3, line)
|
||||
assertTokenRangesEqual(t, screenLine, []twin.Cell{
|
||||
twin.NewCell('0', twin.StyleDefault),
|
||||
twin.NewCell('1', twin.StyleDefault),
|
||||
twin.NewCell('>', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateScreenLineUnderflowLeft(t *testing.T) {
|
||||
line := cellsFromString("012")
|
||||
screenLine := createScreenLine(1, 3, line)
|
||||
assertTokenRangesEqual(t, screenLine, []twin.Cell{
|
||||
twin.NewCell('<', twin.StyleDefault.WithAttr(twin.AttrReverse)),
|
||||
twin.NewCell('1', twin.StyleDefault),
|
||||
twin.NewCell('2', twin.StyleDefault),
|
||||
})
|
||||
}
|
||||
|
||||
func TestFindFirstLineOneBasedSimple(t *testing.T) {
|
||||
reader := NewReaderFromStream("", strings.NewReader("AB"))
|
||||
pager := NewPager(reader)
|
||||
@ -303,6 +256,16 @@ func TestFindFirstLineOneBasedAnsi(t *testing.T) {
|
||||
assert.Check(t, *hitLine == 1)
|
||||
}
|
||||
|
||||
// Converts a cell row to a plain string and removes trailing whitespace.
|
||||
func rowToString(row []twin.Cell) string {
|
||||
rowString := ""
|
||||
for _, cell := range row {
|
||||
rowString += string(cell.Rune)
|
||||
}
|
||||
|
||||
return strings.TrimRight(rowString, " ")
|
||||
}
|
||||
|
||||
func benchmarkSearch(b *testing.B, highlighted bool) {
|
||||
// Pick a go file so we get something with highlighting
|
||||
_, sourceFilename, _, ok := runtime.Caller(0)
|
||||
|
12
m/reader.go
12
m/reader.go
@ -37,8 +37,8 @@ type Reader struct {
|
||||
moreLinesAdded chan bool
|
||||
}
|
||||
|
||||
// Lines contains a number of lines from the reader, plus metadata
|
||||
type Lines struct {
|
||||
// InputLines contains a number of lines from the reader, plus metadata
|
||||
type InputLines struct {
|
||||
lines []*Line
|
||||
|
||||
// One-based line number of the first line returned
|
||||
@ -489,19 +489,19 @@ func (r *Reader) GetLine(lineNumberOneBased int) *Line {
|
||||
}
|
||||
|
||||
// GetLines gets the indicated lines from the input
|
||||
func (r *Reader) GetLines(firstLineOneBased int, wantedLineCount int) *Lines {
|
||||
func (r *Reader) GetLines(firstLineOneBased int, wantedLineCount int) *InputLines {
|
||||
r.lock.Lock()
|
||||
defer r.lock.Unlock()
|
||||
return r._GetLinesUnlocked(firstLineOneBased, wantedLineCount)
|
||||
}
|
||||
|
||||
func (r *Reader) _GetLinesUnlocked(firstLineOneBased int, wantedLineCount int) *Lines {
|
||||
func (r *Reader) _GetLinesUnlocked(firstLineOneBased int, wantedLineCount int) *InputLines {
|
||||
if firstLineOneBased < 1 {
|
||||
firstLineOneBased = 1
|
||||
}
|
||||
|
||||
if len(r.lines) == 0 {
|
||||
return &Lines{
|
||||
return &InputLines{
|
||||
lines: nil,
|
||||
|
||||
// The line number set here won't matter, we'll clip it anyway when we get it back
|
||||
@ -530,7 +530,7 @@ func (r *Reader) _GetLinesUnlocked(firstLineOneBased int, wantedLineCount int) *
|
||||
return r._GetLinesUnlocked(firstLineOneBased, wantedLineCount)
|
||||
}
|
||||
|
||||
return &Lines{
|
||||
return &InputLines{
|
||||
lines: r.lines[firstLineZeroBased : lastLineZeroBased+1],
|
||||
firstLineOneBased: firstLineOneBased,
|
||||
statusText: r._CreateStatusUnlocked(firstLineOneBased, lastLineZeroBased+1),
|
||||
|
152
m/screenLines.go
Normal file
152
m/screenLines.go
Normal file
@ -0,0 +1,152 @@
|
||||
package m
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/walles/moar/twin"
|
||||
)
|
||||
|
||||
type ScreenLines struct {
|
||||
inputLines *InputLines
|
||||
firstInputLineOneBased int
|
||||
leftColumnZeroBased int
|
||||
|
||||
width int
|
||||
height int
|
||||
|
||||
showLineNumbers bool
|
||||
wrapLongLines bool
|
||||
}
|
||||
|
||||
func (sl *ScreenLines) getScreenLines(searchPattern *regexp.Regexp) [][]twin.Cell {
|
||||
// Count the length of the last line number
|
||||
//
|
||||
// Offsets figured out through trial-and-error...
|
||||
lastLineOneBased := sl.inputLines.firstLineOneBased + len(sl.inputLines.lines) - 1
|
||||
numberPrefixLength := len(formatNumber(uint(lastLineOneBased))) + 1
|
||||
if numberPrefixLength < 4 {
|
||||
// 4 = space for 3 digits followed by one whitespace
|
||||
//
|
||||
// https://github.com/walles/moar/issues/38
|
||||
numberPrefixLength = 4
|
||||
}
|
||||
|
||||
if !sl.showLineNumbers {
|
||||
numberPrefixLength = 0
|
||||
}
|
||||
|
||||
returnLines := make([][]twin.Cell, 0, sl.height)
|
||||
screenFull := false
|
||||
for lineIndex, line := range sl.inputLines.lines {
|
||||
lineNumber := sl.firstLineOneBased() + lineIndex
|
||||
|
||||
highlighted := line.HighlightedTokens(searchPattern)
|
||||
var wrapped [][]twin.Cell
|
||||
if sl.wrapLongLines {
|
||||
wrapped = wrapLine(sl.width-numberPrefixLength, highlighted)
|
||||
} else {
|
||||
// All on one line
|
||||
wrapped = [][]twin.Cell{highlighted}
|
||||
}
|
||||
|
||||
for wrapIndex, inputLinePart := range wrapped {
|
||||
visibleLineNumber := &lineNumber
|
||||
if wrapIndex > 0 {
|
||||
visibleLineNumber = nil
|
||||
}
|
||||
|
||||
returnLines = append(returnLines,
|
||||
sl.createScreenLine(visibleLineNumber, numberPrefixLength, inputLinePart))
|
||||
|
||||
if len(returnLines) >= sl.height {
|
||||
// We have shown all the lines that can fit on the screen
|
||||
screenFull = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if screenFull {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return returnLines
|
||||
}
|
||||
|
||||
func (sl *ScreenLines) createScreenLine(lineNumberToShow *int, numberPrefixLength int, contents []twin.Cell) []twin.Cell {
|
||||
newLine := make([]twin.Cell, 0, sl.width)
|
||||
newLine = append(newLine, createLineNumberPrefix(lineNumberToShow, numberPrefixLength)...)
|
||||
|
||||
startColumn := sl.leftColumnZeroBased
|
||||
if startColumn < len(contents) {
|
||||
endColumn := sl.leftColumnZeroBased + (sl.width - numberPrefixLength)
|
||||
if endColumn > len(contents) {
|
||||
endColumn = len(contents)
|
||||
}
|
||||
newLine = append(newLine, contents[startColumn:endColumn]...)
|
||||
}
|
||||
|
||||
// Add scroll left indicator
|
||||
if sl.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] = twin.Cell{
|
||||
Rune: '<',
|
||||
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
|
||||
}
|
||||
}
|
||||
|
||||
// Add scroll right indicator
|
||||
if len(contents)+numberPrefixLength-sl.leftColumnZeroBased > sl.width {
|
||||
newLine[sl.width-1] = twin.Cell{
|
||||
Rune: '>',
|
||||
Style: twin.StyleDefault.WithAttr(twin.AttrReverse),
|
||||
}
|
||||
}
|
||||
|
||||
return newLine
|
||||
}
|
||||
|
||||
// Generate a line number prefix. Can be empty or all-whitespace depending on parameters.
|
||||
func createLineNumberPrefix(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
|
||||
}
|
||||
|
||||
func (sl *ScreenLines) firstLineOneBased() int {
|
||||
// FIXME: This is wrong when wrapping is enabled
|
||||
return sl.inputLines.firstLineOneBased
|
||||
}
|
38
m/screenLines_test.go
Normal file
38
m/screenLines_test.go
Normal file
@ -0,0 +1,38 @@
|
||||
package m
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func testCropping(t *testing.T, contents string, firstIndex int, lastIndex int, expected string) {
|
||||
screenLines := ScreenLines{width: 1 + lastIndex - firstIndex, leftColumnZeroBased: firstIndex}
|
||||
lineContents := NewLine(contents).HighlightedTokens(nil)
|
||||
screenLine := screenLines.createScreenLine(nil, 0, lineContents)
|
||||
assert.Equal(t, rowToString(screenLine), expected)
|
||||
}
|
||||
|
||||
func TestCreateScreenLine(t *testing.T) {
|
||||
testCropping(t, "abc", 0, 10, "abc")
|
||||
}
|
||||
|
||||
func TestCreateScreenLineCanScrollLeft(t *testing.T) {
|
||||
testCropping(t, "abc", 1, 10, "<c")
|
||||
}
|
||||
|
||||
func TestCreateScreenLineCanScrollRight(t *testing.T) {
|
||||
testCropping(t, "abc", 0, 1, "a>")
|
||||
}
|
||||
|
||||
func TestCreateScreenLineCanAlmostScrollRight(t *testing.T) {
|
||||
testCropping(t, "abc", 0, 2, "abc")
|
||||
}
|
||||
|
||||
func TestCreateScreenLineCanScrollBoth(t *testing.T) {
|
||||
testCropping(t, "abcde", 1, 3, "<c>")
|
||||
}
|
||||
|
||||
func TestCreateScreenLineCanAlmostScrollBoth(t *testing.T) {
|
||||
testCropping(t, "abcd", 1, 3, "<cd")
|
||||
}
|
Loading…
Reference in New Issue
Block a user