File: screenLines.go

package info (click to toggle)
moor 2.0.0-2
  • links: PTS, VCS
  • area: main
  • in suites:
  • size: 16,072 kB
  • sloc: sh: 174; ansic: 12; xml: 6; makefile: 5
file content (365 lines) | stat: -rw-r--r-- 11,691 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
package internal

import (
	"fmt"

	"github.com/walles/moor/internal/linemetadata"
	"github.com/walles/moor/internal/reader"
	"github.com/walles/moor/internal/textstyles"
	"github.com/walles/moor/twin"
)

type renderedLine struct {
	// Certain lines are available for viewing. This index is the (zero based)
	// position of this line among those.
	inputLineIndex linemetadata.Index

	// 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.StyledRune

	// Used for rendering clear-to-end-of-line control sequences:
	// https://en.wikipedia.org/wiki/ANSI_escape_code#EL
	//
	// Ref: https://github.com/walles/moor/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()
	p.longestLineLength = 0

	lastUpdatedScreenLineNumber := -1
	var renderedScreenLines [][]twin.StyledRune
	renderedScreenLines, statusText := p.renderScreenLines()
	for screenLineNumber, row := range renderedScreenLines {
		lastUpdatedScreenLineNumber = screenLineNumber
		column := 0
		for _, cell := range row {
			column += p.screen.SetCell(column, lastUpdatedScreenLineNumber, cell)
		}
	}

	// Status line code follows

	eofSpinner := spinner
	if eofSpinner == "" {
		// This happens when we're done
		eofSpinner = "---"
	}
	spinnerLine := textstyles.StyledRunesFromString(statusbarStyle, eofSpinner, nil).StyledRunes
	column := 0
	for _, cell := range spinnerLine {
		column += p.screen.SetCell(column, lastUpdatedScreenLineNumber+1, cell)
	}

	p.mode.drawFooter(statusText, spinner)

	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.StyledRune, statusText string) {
	renderedLines, statusText := p.renderLines()
	if len(renderedLines) == 0 {
		return
	}

	// Construct the screen lines to return
	screenLines := make([][]twin.StyledRune, 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.NewStyledRune(' ', 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) {
	var lineIndex linemetadata.Index
	if p.lineIndex() != nil {
		lineIndex = *p.lineIndex()
	}
	inputLines := p.Reader().GetLines(lineIndex, p.visibleHeight())
	if len(inputLines.Lines) == 0 {
		// Empty input, empty output
		return []renderedLine{}, inputLines.StatusText
	}

	lastVisibleLineNumber := inputLines.Lines[len(inputLines.Lines)-1].Number
	numberPrefixLength := p.getLineNumberPrefixLength(lastVisibleLineNumber)

	allLines := make([]renderedLine, 0)
	for _, line := range inputLines.Lines {
		rendering := p.renderLine(line, numberPrefixLength)

		var onScreenLength int
		for i := range rendering {
			trimmedLen := len(twin.TrimSpaceRight(rendering[i].cells))
			if trimmedLen > onScreenLength {
				onScreenLength = trimmedLen
			}
		}

		// We're trying to find the max length of readable characters to limit
		// the scrolling to right, so we don't go over into the vast emptiness for no reason.
		//
		// The -1 fixed an issue that seemed like an off-by-one where sometimes, when first
		// scrolling completely to the right, the first left scroll did not show the text again.
		displayLength := p.leftColumnZeroBased + onScreenLength - 1

		if displayLength >= p.longestLineLength {
			p.longestLineLength = displayLength
		}

		allLines = append(allLines, rendering...)
	}

	// 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.lineIndex() == nil {
			// Expected zero lines but got some anyway, grab the first one!
			firstVisibleIndex = index
			break
		}
		if line.inputLineIndex == *p.lineIndex() && 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:]

	wantedLineCount := p.visibleHeight()
	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 *reader.NumberedLine, numberPrefixLength int) []renderedLine {
	highlighted := line.HighlightedTokens(plainTextStyle, standoutStyle, p.searchPattern)
	var wrapped [][]twin.StyledRune
	if p.WrapLongLines {
		width, _ := p.screen.Size()
		wrapped = wrapLine(width-numberPrefixLength, highlighted.StyledRunes)
	} else {
		// All on one line
		wrapped = [][]twin.StyledRune{highlighted.StyledRunes}
	}

	rendered := make([]renderedLine, 0)
	for wrapIndex, inputLinePart := range wrapped {
		lineNumber := line.Number
		visibleLineNumber := &lineNumber
		if wrapIndex > 0 {
			visibleLineNumber = nil
		}

		decorated := p.decorateLine(visibleLineNumber, numberPrefixLength, inputLinePart)

		rendered = append(rendered, renderedLine{
			inputLineIndex: line.Index,
			wrapIndex:      wrapIndex,
			cells:          decorated,
		})
	}

	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 *linemetadata.Number, numberPrefixLength int, contents []twin.StyledRune) []twin.StyledRune {
	width, _ := p.screen.Size()
	newLine := make([]twin.StyledRune, 0, width)
	newLine = append(newLine, createLinePrefix(lineNumberToShow, numberPrefixLength)...)

	// Find the first and last fully visible runes.
	var firstVisibleRuneIndex *int
	lastVisibleRuneIndex := -1
	screenColumn := numberPrefixLength // Zero based
	lastVisibleScreenColumn := p.leftColumnZeroBased + width - 1
	cutOffRuneToTheLeft := false
	cutOffRuneToTheRight := false
	canScrollRight := false
	for i, char := range contents {
		if firstVisibleRuneIndex == nil && screenColumn >= p.leftColumnZeroBased {
			// Found the first fully visible rune. We need to point to a copy of
			// our loop variable, not the loop variable itself. Just pointing to
			// i, will make firstVisibleRuneIndex point to a new value for every
			// iteration of the loop.
			copyOfI := i
			firstVisibleRuneIndex = &copyOfI
			if i > 0 && screenColumn > p.leftColumnZeroBased && contents[i-1].Width() > 1 {
				// We had to cut a rune in half at the start
				cutOffRuneToTheLeft = true
			}
		}

		screenReached := firstVisibleRuneIndex != nil
		currentCharRightEdge := screenColumn + char.Width() - 1
		beforeRightEdge := currentCharRightEdge <= lastVisibleScreenColumn
		if screenReached {
			if beforeRightEdge {
				// This rune is fully visible
				lastVisibleRuneIndex = i
			} else {
				// We're just outside the screen on the right
				canScrollRight = true

				currentCharLeftEdge := screenColumn
				if currentCharLeftEdge <= lastVisibleScreenColumn {
					// We have to cut this rune in half
					cutOffRuneToTheRight = true
				}

				// Search done, we're off the right edge
				break
			}
		}

		screenColumn += char.Width()
	}

	// Prepend a space if we had to cut a rune in half at the start
	if cutOffRuneToTheLeft {
		newLine = append([]twin.StyledRune{twin.NewStyledRune(' ', p.ScrollLeftHint.Style)}, newLine...)
	}

	// Add the visible runes
	if firstVisibleRuneIndex != nil {
		newLine = append(newLine, contents[*firstVisibleRuneIndex:lastVisibleRuneIndex+1]...)
	}

	// Append a space if we had to cut a rune in half at the end
	if cutOffRuneToTheRight {
		newLine = append(newLine, twin.NewStyledRune(' ', p.ScrollRightHint.Style))
	}

	// Add scroll left indicator
	canScrollLeft := p.leftColumnZeroBased > 0
	if canScrollLeft && len(contents) > 0 {
		if len(newLine) == 0 {
			// Make room for the scroll left indicator
			newLine = make([]twin.StyledRune, 1)
		}

		if newLine[0].Width() > 1 {
			// Replace the first rune with two spaces so we can replace the
			// leftmost cell with a scroll left indicator. First, convert to one
			// space...
			newLine[0] = twin.NewStyledRune(' ', p.ScrollLeftHint.Style)
			// ...then prepend another space:
			newLine = append([]twin.StyledRune{twin.NewStyledRune(' ', p.ScrollLeftHint.Style)}, newLine...)

			// Prepending ref: https://stackoverflow.com/a/53737602/473672
		}

		// Set can-scroll-left marker
		newLine[0] = p.ScrollLeftHint
	}

	// Add scroll right indicator
	if canScrollRight {
		if newLine[len(newLine)-1].Width() > 1 {
			// Replace the last rune with two spaces so we can replace the
			// rightmost cell with a scroll right indicator. First, convert to one
			// space...
			newLine[len(newLine)-1] = twin.NewStyledRune(' ', p.ScrollRightHint.Style)
			// ...then append another space:
			newLine = append(newLine, twin.NewStyledRune(' ', p.ScrollRightHint.Style))
		}

		newLine[len(newLine)-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(lineNumber *linemetadata.Number, numberPrefixLength int) []twin.StyledRune {
	if numberPrefixLength == 0 {
		return []twin.StyledRune{}
	}

	lineNumberPrefix := make([]twin.StyledRune, 0, numberPrefixLength)
	if lineNumber == nil {
		for len(lineNumberPrefix) < numberPrefixLength {
			lineNumberPrefix = append(lineNumberPrefix, twin.StyledRune{Rune: ' '})
		}
		return lineNumberPrefix
	}

	lineNumberString := fmt.Sprintf("%*s ", numberPrefixLength-1, lineNumber.Format())
	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.NewStyledRune(digit, lineNumbersStyle))
	}

	return lineNumberPrefix
}