package cview

import (
	
	
	
	
	

	
	
	
	
)

var (
	// TabSize is the number of spaces with which a tab character will be replaced.
	TabSize = 4
)

var (
	openColorRegex  = regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`)
	openRegionRegex = regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`)
)

// textViewIndex contains information about each line displayed in the text
// view.
type textViewIndex struct {
	Line            int    // The index into the "buffer" variable.
	Pos             int    // The index into the "buffer" line ([]byte position).
	NextPos         int    // The (byte) index of the next character in this buffer line.
	Width           int    // The screen width of this line.
	ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset).
	BackgroundColor string // The starting background color ("" = don't change, "-" = reset).
	Attributes      string // The starting attributes ("" = don't change, "-" = reset).
	Region          []byte // The starting region ID.
}

// textViewRegion contains information about a region.
type textViewRegion struct {
	// The region ID.
	ID []byte

	// The starting and end screen position of the region as determined the last
	// time Draw() was called. A negative value indicates out-of-rect positions.
	FromX, FromY, ToX, ToY int
}

// TextView is a box which displays text. It implements the io.Writer interface
// so you can stream text to it. This does not trigger a redraw automatically
// but if a handler is installed via SetChangedFunc(), you can cause it to be
// redrawn. (See SetChangedFunc() for more details.)
//
// # Navigation
//
// If the text view is scrollable (the default), text is kept in a buffer which
// may be larger than the screen and can be navigated similarly to Vim:
//
//   - h, left arrow: Move left.
//   - l, right arrow: Move right.
//   - j, down arrow: Move down.
//   - k, up arrow: Move up.
//   - g, home: Move to the top.
//   - G, end: Move to the bottom.
//   - Ctrl-F, page down: Move down by one page.
//   - Ctrl-B, page up: Move up by one page.
//
// If the text is not scrollable, any text above the top visible line is
// discarded.
//
// Use SetInputCapture() to override or modify keyboard input.
//
// # Colors
//
// If dynamic colors are enabled via SetDynamicColors(), text color can be
// changed dynamically by embedding color strings in square brackets. This works
// the same way as anywhere else. Please see the package documentation for more
// information.
//
// # Regions and Highlights
//
// If regions are enabled via SetRegions(), you can define text regions within
// the text and assign region IDs to them. Text regions start with region tags.
// Region tags are square brackets that contain a region ID in double quotes,
// for example:
//
//	We define a ["rg"]region[""] here.
//
// A text region ends with the next region tag. Tags with no region ID ([""])
// don't start new regions. They can therefore be used to mark the end of a
// region. Region IDs must satisfy the following regular expression:
//
//	[a-zA-Z0-9_,;: \-\.]+
//
// Regions can be highlighted by calling the Highlight() function with one or
// more region IDs. This can be used to display search results, for example.
//
// The ScrollToHighlight() function can be used to jump to the currently
// highlighted region once when the text view is drawn the next time.
type TextView struct {
	*Box

	// The text buffer.
	buffer [][]byte

	// The last bytes that have been received but are not part of the buffer yet.
	recentBytes []byte

	// The last width and height of the text view.
	lastWidth, lastHeight int

	// The processed line index. This is nil if the buffer has changed and needs
	// to be re-indexed.
	index []*textViewIndex

	// The width of the text view buffer index.
	indexWidth int

	// If set to true, the buffer will be reindexed each time it is modified.
	reindex bool

	// The horizontal text alignment, one of AlignLeft, AlignCenter, or AlignRight.
	align int

	// The vertical text alignment, one of AlignTop, AlignMiddle, or AlignBottom.
	valign VerticalAlignment

	// Information about visible regions as of the last call to Draw().
	regionInfos []*textViewRegion

	// Indices into the "index" slice which correspond to the first line of the
	// first highlight and the last line of the last highlight. This is calculated
	// during re-indexing. Set to -1 if there is no current highlight.
	fromHighlight, toHighlight int

	// The screen space column of the highlight in its first line. Set to -1 if
	// there is no current highlight.
	posHighlight int

	// A set of region IDs that are currently highlighted.
	highlights map[string]struct{}

	// The screen width of the longest line in the index (not the buffer).
	longestLine int

	// The index of the first line shown in the text view.
	lineOffset int

	// The maximum number of newlines the text view will hold (0 = unlimited).
	maxLines int

	// If set to true, the text view will always remain at the end of the content.
	trackEnd bool

	// The number of characters to be skipped on each line (not in wrap mode).
	columnOffset int

	// The height of the content the last time the text view was drawn.
	pageSize int

	// If set to true, the text view will keep a buffer of text which can be
	// navigated when the text is longer than what fits into the box.
	scrollable bool

	// Visibility of the scroll bar.
	scrollBarVisibility ScrollBarVisibility

	// The scroll bar color.
	scrollBarColor tcell.Color

	// If set to true, lines that are longer than the available width are wrapped
	// onto the next line. If set to false, any characters beyond the available
	// width are discarded.
	wrap bool

	// The maximum line width when wrapping (0 = use TextView width).
	wrapWidth int

	// If set to true and if wrap is also true, lines are split at spaces or
	// after punctuation characters.
	wordWrap bool

	// The (starting) color of the text.
	textColor tcell.Color

	// The foreground color of highlighted text.
	highlightForeground tcell.Color

	// The background color of highlighted text.
	highlightBackground tcell.Color

	// If set to true, the text color can be changed dynamically by piping color
	// strings in square brackets to the text view.
	dynamicColors bool

	// If set to true, region tags can be used to define regions.
	regions bool

	// A temporary flag which, when true, will automatically bring the current
	// highlight(s) into the visible screen.
	scrollToHighlights bool

	// If true, setting new highlights will be a XOR instead of an overwrite
	// operation.
	toggleHighlights bool

	// An optional function which is called when the content of the text view has
	// changed.
	changed func()

	// An optional function which is called when the user presses one of the
	// following keys: Escape, Enter, Tab, Backtab.
	done func(tcell.Key)

	// An optional function which is called when one or more regions were
	// highlighted.
	highlighted func(added, removed, remaining []string)

	sync.RWMutex
	clicked func(regionId string)
}

// NewTextView returns a new text view.
func () *TextView {
	return &TextView{
		Box:                 NewBox(),
		highlights:          make(map[string]struct{}),
		lineOffset:          -1,
		reindex:             true,
		scrollable:          true,
		scrollBarVisibility: ScrollBarAuto,
		scrollBarColor:      Styles.ScrollBarColor,
		align:               AlignLeft,
		valign:              AlignTop,
		wrap:                true,
		textColor:           Styles.PrimaryTextColor,
		highlightForeground: Styles.PrimitiveBackgroundColor,
		highlightBackground: Styles.PrimaryTextColor,
	}
}

// SetScrollable sets the flag that decides whether or not the text view is
// scrollable. If true, text is kept in a buffer and can be navigated. If false,
// the last line will always be visible.
func ( *TextView) ( bool) {
	.Lock()
	defer .Unlock()

	.scrollable = 
	if ! {
		.trackEnd = true
	}
}

// SetScrollBarVisibility specifies the display of the scroll bar.
func ( *TextView) ( ScrollBarVisibility) {
	.Lock()
	defer .Unlock()

	.scrollBarVisibility = 
}

// SetScrollBarColor sets the color of the scroll bar.
func ( *TextView) ( tcell.Color) {
	.Lock()
	defer .Unlock()

	.scrollBarColor = 
}

// SetWrap sets the flag that, if true, leads to lines that are longer than the
// available width being wrapped onto the next line. If false, any characters
// beyond the available width are not displayed.
func ( *TextView) ( bool) {
	.Lock()
	defer .Unlock()

	if .wrap !=  {
		.index = nil
	}
	.wrap = 
}

// SetWordWrap sets the flag that, if true and if the "wrap" flag is also true
// (see SetWrap()), wraps the line at spaces or after punctuation marks. Note
// that trailing spaces will not be printed.
//
// This flag is ignored if the "wrap" flag is false.
func ( *TextView) ( bool) {
	.Lock()
	defer .Unlock()

	if .wordWrap !=  {
		.index = nil
	}
	.wordWrap = 
}

// SetTextAlign sets the horizontal alignment of the text. This must be either
// AlignLeft, AlignCenter, or AlignRight.
func ( *TextView) ( int) {
	.Lock()
	defer .Unlock()

	if .align !=  {
		.index = nil
	}
	.align = 
}

// SetVerticalAlign sets the vertical alignment of the text. This must be
// either AlignTop, AlignMiddle, or AlignBottom.
func ( *TextView) ( VerticalAlignment) {
	.Lock()
	defer .Unlock()

	if .valign !=  {
		.index = nil
	}
	.valign = 
}

// SetTextColor sets the initial color of the text (which can be changed
// dynamically by sending color strings in square brackets to the text view if
// dynamic colors are enabled).
func ( *TextView) ( tcell.Color) {
	.Lock()
	defer .Unlock()

	.textColor = 
}

// SetHighlightForegroundColor sets the foreground color of highlighted text.
func ( *TextView) ( tcell.Color) {
	.Lock()
	defer .Unlock()

	.highlightForeground = 
}

// SetHighlightBackgroundColor sets the foreground color of highlighted text.
func ( *TextView) ( tcell.Color) {
	.Lock()
	defer .Unlock()

	.highlightBackground = 
}

// SetBytes sets the text of this text view to the provided byte slice.
// Previously contained text will be removed.
func ( *TextView) ( []byte) {
	.Lock()
	defer .Unlock()

	.clear()
	.write()
}

// SetText sets the text of this text view to the provided string. Previously
// contained text will be removed.
func ( *TextView) ( string) {
	.SetBytes([]byte())
}

// GetBytes returns the current text of this text view. If "stripTags" is set
// to true, any region/color tags are stripped from the text.
func ( *TextView) ( bool) []byte {
	.RLock()
	defer .RUnlock()

	if ! {
		if len(.recentBytes) > 0 {
			return bytes.Join(append(.buffer, .recentBytes), []byte("\n"))
		}
		return bytes.Join(.buffer, []byte("\n"))
	}

	 := bytes.Join(.buffer, []byte("\n"))
	return StripTags(, .dynamicColors, .regions)
}

// GetText returns the current text of this text view. If "stripTags" is set
// to true, any region/color tags are stripped from the text.
func ( *TextView) ( bool) string {
	return string(.GetBytes())
}

// GetBufferSize returns the number of lines and the length of the longest line
// in the text buffer. The screen size of the widget is available via GetRect.
func ( *TextView) () ( int,  int) {
	.RLock()
	defer .RUnlock()

	return len(.buffer), .longestLine
}

// SetDynamicColors sets the flag that allows the text color to be changed
// dynamically. See class description for details.
func ( *TextView) ( bool) {
	.Lock()
	defer .Unlock()

	if .dynamicColors !=  {
		.index = nil
	}
	.dynamicColors = 
}

// SetRegions sets the flag that allows to define regions in the text. See class
// description for details.
func ( *TextView) ( bool) {
	.Lock()
	defer .Unlock()

	if .regions !=  {
		.index = nil
	}
	.regions = 
}

// SetChangedFunc sets a handler function which is called when the text of the
// text view has changed. This is useful when text is written to this io.Writer
// in a separate goroutine. Doing so does not automatically cause the screen to
// be refreshed so you may want to use the "changed" handler to redraw the
// screen.
//
// Note that to avoid race conditions or deadlocks, there are a few rules you
// should follow:
//
//   - You can call Application.Draw() from this handler.
//   - You can call TextView.HasFocus() from this handler.
//   - During the execution of this handler, access to any other variables from
//     this primitive or any other primitive should be queued using
//     Application.QueueUpdate().
//
// See package description for details on dealing with concurrency.
func ( *TextView) ( func()) {
	.Lock()
	defer .Unlock()

	.changed = 
}

// SetDoneFunc sets a handler which is called when the user presses on the
// following keys: Escape, Enter, Tab, Backtab. The key is passed to the
// handler.
func ( *TextView) ( func( tcell.Key)) {
	.Lock()
	defer .Unlock()

	.done = 
}

// SetHighlightedFunc sets a handler which is called when the list of currently
// highlighted regions change. It receives a list of region IDs which were newly
// highlighted, those that are not highlighted anymore, and those that remain
// highlighted.
//
// Note that because regions are only determined during drawing, this function
// can only fire for regions that have existed during the last call to Draw().
func ( *TextView) ( func(, ,  []string)) {
	.highlighted = 
}

// SetClickedFunc Handler to run when a region is clicked.
func ( *TextView) ( func( string)) {
	.clicked = 
}

func ( *TextView) () {
	if .maxLines <= 0 {
		return
	}

	 := len(.buffer)
	if  > .maxLines {
		.buffer = .buffer[-.maxLines:]
	}
}

// SetMaxLines sets the maximum number of newlines the text view will hold
// before discarding older data from the buffer.
func ( *TextView) ( int) {
	.maxLines = 
	.clipBuffer()
}

// ScrollTo scrolls to the specified row and column (both starting with 0).
func ( *TextView) (,  int) {
	.Lock()
	defer .Unlock()

	if !.scrollable {
		return
	}
	.lineOffset = 
	.columnOffset = 
	.trackEnd = false
}

// ScrollToBeginning scrolls to the top left corner of the text if the text view
// is scrollable.
func ( *TextView) () {
	.Lock()
	defer .Unlock()

	if !.scrollable {
		return
	}
	.trackEnd = false
	.lineOffset = 0
	.columnOffset = 0
}

// ScrollToEnd scrolls to the bottom left corner of the text if the text view
// is scrollable. Adding new rows to the end of the text view will cause it to
// scroll with the new data.
func ( *TextView) () {
	.Lock()
	defer .Unlock()

	if !.scrollable {
		return
	}
	.trackEnd = true
	.columnOffset = 0
}

// GetScrollOffset returns the number of rows and columns that are skipped at
// the top left corner when the text view has been scrolled.
func ( *TextView) () (,  int) {
	.RLock()
	defer .RUnlock()

	return .lineOffset, .columnOffset
}

// Clear removes all text from the buffer.
func ( *TextView) () {
	.Lock()
	defer .Unlock()

	.clear()
}

func ( *TextView) () {
	.buffer = nil
	.recentBytes = nil
	if .reindex {
		.index = nil
	}
}

// Highlight specifies which regions should be highlighted. If highlight
// toggling is set to true (see SetToggleHighlights()), the highlight of the
// provided regions is toggled (highlighted regions are un-highlighted and vice
// versa). If toggling is set to false, the provided regions are highlighted and
// all other regions will not be highlighted (you may also provide nil to turn
// off all highlights).
//
// For more information on regions, see class description. Empty region strings
// are ignored.
//
// Text in highlighted regions will be drawn inverted, i.e. with their
// background and foreground colors swapped.
func ( *TextView) ( ...string) {
	.Lock()

	// Toggle highlights.
	if .toggleHighlights {
		var  []string
	:
		for  := range .highlights {
			for ,  := range  {
				if  ==  {
					continue 
				}
			}
			 = append(, )
		}
		for ,  := range  {
			if ,  := .highlights[]; ! {
				 = append(, )
			}
		}
		 = 
	} // Now we have a list of region IDs that end up being highlighted.

	// Determine added and removed regions.
	var , ,  []string
	if .highlighted != nil {
		for ,  := range  {
			if ,  := .highlights[];  {
				 = append(, )
				delete(.highlights, )
			} else {
				 = append(, )
			}
		}
		for  := range .highlights {
			 = append(, )
		}
	}

	// Make new selection.
	.highlights = make(map[string]struct{})
	for ,  := range  {
		if  == "" {
			continue
		}
		.highlights[] = struct{}{}
	}
	.index = nil

	// Notify.
	if .highlighted != nil && (len() > 0 || len() > 0) {
		.Unlock()
		.highlighted(, , )
	} else {
		.Unlock()
	}
}

// GetHighlights returns the IDs of all currently highlighted regions.
func ( *TextView) () ( []string) {
	.RLock()
	defer .RUnlock()

	for  := range .highlights {
		 = append(, )
	}
	return
}

// SetToggleHighlights sets a flag to determine how regions are highlighted.
// When set to true, the Highlight() function  will toggle the
// provided/selected regions. When set to false, Highlight()
// will simply highlight the provided regions.
func ( *TextView) ( bool) {
	.toggleHighlights = 
}

// ScrollToHighlight will cause the visible area to be scrolled so that the
// highlighted regions appear in the visible area of the text view. This
// repositioning happens the next time the text view is drawn. It happens only
// once so you will need to call this function repeatedly to always keep
// highlighted regions in view.
//
// Nothing happens if there are no highlighted regions or if the text view is
// not scrollable.
func ( *TextView) () {
	.Lock()
	defer .Unlock()

	if len(.highlights) == 0 || !.scrollable || !.regions {
		return
	}
	.index = nil
	.scrollToHighlights = true
	.trackEnd = false
}

// GetRegionText returns the text of the region with the given ID. If dynamic
// colors are enabled, color tags are stripped from the text. Newlines are
// always returned as '\n' runes.
//
// If the region does not exist or if regions are turned off, an empty string
// is returned.
func ( *TextView) ( string) string {
	.RLock()
	defer .RUnlock()

	if !.regions || len() == 0 {
		return ""
	}

	var (
		          bytes.Buffer
		 string
	)

	for ,  := range .buffer {
		// Find all color tags in this line.
		var  [][]int
		if .dynamicColors {
			 = colorPattern.FindAllIndex(, -1)
		}

		// Find all regions in this line.
		var (
			 [][]int
			       [][][]byte
		)
		if .regions {
			 = regionPattern.FindAllIndex(, -1)
			 = regionPattern.FindAllSubmatch(, -1)
		}

		// Analyze this line.
		var ,  int
		for ,  := range  {
			// Skip any color tags.
			if  < len() &&  >= [][0] &&  < [][1] {
				if  == [][1]-1 {
					++
					if  == len() {
						continue
					}
				}
				if [][1]-[][0] > 2 {
					continue
				}
			}

			// Skip any regions.
			if  < len() &&  >= [][0] &&  < [][1] {
				if  == [][1]-1 {
					if  ==  {
						// This is the end of the requested region. We're done.
						return .String()
					}
					 = string([][1])
					++
				}
				continue
			}

			// Add this rune.
			if  ==  {
				.WriteByte()
			}
		}

		// Add newline.
		if  ==  {
			.WriteRune('\n')
		}
	}

	return escapePattern.ReplaceAllString(.String(), `[$1$2]`)
}

// Focus is called when this primitive receives focus.
func ( *TextView) ( func( Primitive)) {
	.Lock()
	defer .Unlock()

	// Implemented here with locking because this is used by layout primitives.
	.hasFocus = true
}

// HasFocus returns whether or not this primitive has focus.
func ( *TextView) () bool {
	.RLock()
	defer .RUnlock()

	// Implemented here with locking because this may be used in the "changed"
	// callback.
	return .hasFocus
}

// Write lets us implement the io.Writer interface. Tab characters will be
// replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted
// as a new line.
func ( *TextView) ( []byte) ( int,  error) {
	.Lock()
	 := .changed
	if  != nil {
		// Notify at the end.
		defer ()
	}
	defer .Unlock()

	return .write()
}

func ( *TextView) ( []byte) ( int,  error) {
	// Copy data over.
	 := append(.recentBytes, ...)
	.recentBytes = nil

	// If we have a trailing invalid UTF-8 byte, we'll wait.
	if ,  := utf8.DecodeLastRune();  == utf8.RuneError {
		.recentBytes = 
		return len(), nil
	}

	// If we have a trailing open dynamic color, exclude it.
	if .dynamicColors {
		 := openColorRegex.FindIndex()
		if  != nil {
			.recentBytes = [[0]:]
			 = [:[0]]
		}
	}

	// If we have a trailing open region, exclude it.
	if .regions {
		 := openRegionRegex.FindIndex()
		if  != nil {
			.recentBytes = [[0]:]
			 = [:[0]]
		}
	}

	// Transform the new bytes into strings.
	 = bytes.Replace(, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1)
	for ,  := range bytes.Split(, []byte("\n")) {
		if  == 0 {
			if len(.buffer) == 0 {
				.buffer = [][]byte{}
			} else {
				.buffer[len(.buffer)-1] = append(.buffer[len(.buffer)-1], ...)
			}
		} else {
			.buffer = append(.buffer, )
		}
	}

	.clipBuffer()

	// Reset the index.
	if .reindex {
		.index = nil
	}

	return len(), nil
}

// SetWrapWidth set the maximum width of lines when wrapping is enabled.
// When set to 0 the width of the TextView is used.
func ( *TextView) ( int) {
	.Lock()
	defer .Unlock()

	.wrapWidth = 
}

// SetReindexBuffer set a flag controlling whether the buffer is reindexed when
// it is modified. This improves the performance of TextViews whose contents
// always have line-breaks in the same location. This must be called after the
// buffer has been indexed.
func ( *TextView) ( bool) {
	.Lock()
	defer .Unlock()

	.reindex = 

	if  {
		.index = nil
	}
}

// reindexBuffer re-indexes the buffer such that we can use it to easily draw
// the buffer onto the screen. Each line in the index will contain a pointer
// into the buffer from which on we will print text. It will also contain the
// color with which the line starts.
func ( *TextView) ( int) {
	if .index != nil && (!.wrap ||  == .indexWidth) {
		return // Nothing has changed. We can still use the current index.
	}
	.index = nil
	.indexWidth = 
	.fromHighlight, .toHighlight, .posHighlight = -1, -1, -1

	// If there's no space, there's no index.
	if  < 1 {
		return
	}

	if .wrapWidth > 0 && .wrapWidth <  {
		 = .wrapWidth
	}

	// Initial states.
	var  []byte
	var (
		                                  bool
		, ,  string
	)

	// Go through each line in the buffer.
	for ,  := range .buffer {
		, , , , , ,  := decomposeText(, .dynamicColors, .regions)

		// Split the line if required.
		var  []string
		 := string()
		if .wrap && len() > 0 {
			for len() > 0 {
				 := runewidth.Truncate(, , "")
				if len() == 0 {
					// We'll extract at least one grapheme cluster.
					 := uniseg.NewGraphemes()
					.Next()
					,  := .Positions()
					 = [:]
				}
				if .wordWrap && len() < len() {
					// Add any spaces from the next line.
					if  := spacePattern.FindStringIndex([len():]);  != nil && [0] == 0 {
						 = [:len()+[1]]
					}

					// Can we split before the mandatory end?
					 := boundaryPattern.FindAllStringIndex(, -1)
					if len() > 0 {
						// Yes. Let's split there.
						 = [:[len()-1][1]]
					}
				}
				 = append(, )
				 = [len():]
			}
		} else {
			// No need to split the line.
			 = []string{}
		}

		// Create index from split lines.
		var , , ,  int
		for ,  := range  {
			 := &textViewIndex{
				Line:            ,
				Pos:             ,
				ForegroundColor: ,
				BackgroundColor: ,
				Attributes:      ,
				Region:          ,
			}

			// Shift original position with tags.
			 := len()
			 := 
			 := 
			 := 0
			for {
				// Which tag comes next?
				 := make([][3]int, 0, 3)
				if  < len() {
					 = append(, [3]int{[][0], [][1], 0}) // 0 = color tag.
				}
				if  < len() {
					 = append(, [3]int{[][0], [][1], 1}) // 1 = region tag.
				}
				if  < len() {
					 = append(, [3]int{[][0], [][1], 2}) // 2 = escape tag.
				}
				 := -1
				 := -1
				for ,  := range  {
					if  < 0 || [0] <  {
						 = [0]
						 = 
					}
				}

				// Is the next tag in range?
				if  < 0 ||  > + {
					break // No. We're done with this line.
				}

				// Advance.
				 := [][0] -  - 
				 = [][1]
				 :=  - [][0]
				if [][2] == 2 {
					 = 1
				}
				 += 
				 =  - ( -  - )

				// Process the tag.
				switch [][2] {
				case 0:
					// Process color tags.
					, ,  = styleFromTag(, , , [])
					++
				case 1:
					// Process region tags.
					 = [][1]
					_,  = .highlights[string()]

					// Update highlight range.
					if  {
						 := len(.index)
						if .fromHighlight < 0 {
							.fromHighlight, .toHighlight = , 
							.posHighlight = runewidth.StringWidth([:])
						} else if  > .toHighlight {
							.toHighlight = 
						}
					}

					++
				case 2:
					// Process escape tags.
					++
				}
			}

			// Advance to next line.
			 +=  + 

			// Append this line.
			.NextPos = 
			.Width = runewidth.StringWidth()
			.index = append(.index, )
		}

		// Word-wrapped lines may have trailing whitespace. Remove it.
		if .wrap && .wordWrap {
			for ,  := range .index {
				 := .buffer[.Line][.Pos:.NextPos]
				 := bytes.TrimRightFunc(, unicode.IsSpace)
				if len() != len() {
					 := .NextPos
					.NextPos -= len() - len()
					.Width -= runewidth.StringWidth(string(.buffer[.Line][.NextPos:]))
				}
			}
		}
	}

	// Calculate longest line.
	.longestLine = 0
	for ,  := range .index {
		if .Width > .longestLine {
			.longestLine = .Width
		}
	}
}

// Draw draws this primitive onto the screen.
func ( *TextView) ( tcell.Screen) {
	if !.GetVisible() {
		return
	}

	.Box.Draw()

	.Lock()
	defer .Unlock()

	// Get the available size.
	, , ,  := .GetInnerRect()
	if  == 0 {
		return
	}
	.pageSize = 

	if .index == nil ||  != .lastWidth ||  != .lastHeight {
		.reindexBuffer()
	}
	.lastWidth, .lastHeight = , 

	 := .scrollBarVisibility == ScrollBarAlways || (.scrollBarVisibility == ScrollBarAuto && len(.index) > )
	if  {
		-- // Subtract space for scroll bar.
	}

	.reindexBuffer()
	if .regions {
		.regionInfos = nil
	}

	// Draw scroll bar last.
	defer func() {
		if ! {
			return
		}

		 := len(.index)
		 := int(float64(len(.index)) * (float64(.lineOffset) / float64(len(.index)-)))

		// Render cursor at the bottom when tracking end
		if .trackEnd &&  <=  {
			 =  + 1
			 = 
		}

		for  := 0;  < ; ++ {
			RenderScrollBar(, .scrollBarVisibility, +, +, , , , , .hasFocus, .scrollBarColor)
		}
	}()

	// If we don't have an index, there's nothing to draw.
	if .index == nil {
		return
	}

	// Move to highlighted regions.
	if .regions && .scrollToHighlights && .fromHighlight >= 0 {
		// Do we fit the entire height?
		if .toHighlight-.fromHighlight+1 <  {
			// Yes, let's center the highlights.
			.lineOffset = (.fromHighlight + .toHighlight - ) / 2
		} else {
			// No, let's move to the start of the highlights.
			.lineOffset = .fromHighlight
		}

		// If the highlight is too far to the right, move it to the middle.
		if .posHighlight-.columnOffset > 3*/4 {
			.columnOffset = .posHighlight - /2
		}

		// If the highlight is offscreen on the left, move it onscreen.
		if .posHighlight-.columnOffset < 0 {
			.columnOffset = .posHighlight - /4
		}
	}
	.scrollToHighlights = false

	// Adjust line offset.
	if .lineOffset+ > len(.index) {
		.trackEnd = true
	}
	if .trackEnd {
		.lineOffset = len(.index) - 
	}
	if .lineOffset < 0 {
		.lineOffset = 0
	}

	// Adjust column offset.
	if .align == AlignLeft {
		if .columnOffset+ > .longestLine {
			.columnOffset = .longestLine - 
		}
		if .columnOffset < 0 {
			.columnOffset = 0
		}
	} else if .align == AlignRight {
		if .columnOffset- < -.longestLine {
			.columnOffset =  - .longestLine
		}
		if .columnOffset > 0 {
			.columnOffset = 0
		}
	} else { // AlignCenter.
		 := (.longestLine - ) / 2
		if  > 0 {
			if .columnOffset >  {
				.columnOffset = 
			}
			if .columnOffset < - {
				.columnOffset = -
			}
		} else {
			.columnOffset = 0
		}
	}

	// Calculate offset to apply vertical alignment
	 := 0
	if len(.index) <  {
		if .valign == AlignMiddle {
			 = ( - len(.index)) / 2
		} else if .valign == AlignBottom {
			 =  - len(.index)
		}
	}

	// Draw the buffer.
	 := tcell.StyleDefault.Foreground(.textColor).Background(.backgroundColor)
	for  := .lineOffset;  < len(.index); ++ {
		// Are we done?
		if -.lineOffset >=  {
			break
		}

		// Get the text for this line.
		 := .index[]
		 := .buffer[.Line][.Pos:.NextPos]
		 := .ForegroundColor
		 := .BackgroundColor
		 := .Attributes
		 := .Region
		if .regions {
			if len(.regionInfos) > 0 && !bytes.Equal(.regionInfos[len(.regionInfos)-1].ID, ) {
				// End last region.
				.regionInfos[len(.regionInfos)-1].ToX = 
				.regionInfos[len(.regionInfos)-1].ToY =  +  - .lineOffset
			}
			if len() > 0 && (len(.regionInfos) == 0 || !bytes.Equal(.regionInfos[len(.regionInfos)-1].ID, )) {
				// Start a new region.
				.regionInfos = append(.regionInfos, &textViewRegion{
					ID:    ,
					FromX: ,
					FromY:  +  - .lineOffset,
					ToX:   -1,
					ToY:   -1,
				})
			}
		}

		// Process tags.
		, , , , , ,  := decomposeText(, .dynamicColors, .regions)

		// Calculate the position of the line.
		var ,  int
		if .align == AlignLeft {
			 = -.columnOffset
		} else if .align == AlignRight {
			 =  - .Width - .columnOffset
		} else { // AlignCenter.
			 = (-.Width)/2 - .columnOffset
		}
		if  < 0 {
			 = -
			 = 0
		}

		 :=  +  - .lineOffset + 

		// Print the line.
		if  >= 0 {
			var , , , ,  int
			iterateString(string(), func( rune,  []rune, , , ,  int) bool {
				// Process tags.
				for {
					if  < len() && + >= [][0] && + < [][1] {
						// Get the color.
						, ,  = styleFromTag(, , , [])
						 += [][1] - [][0]
						++
					} else if  < len() && + >= [][0] && + < [][1] {
						// Get the region.
						if len() > 0 && len(.regionInfos) > 0 && bytes.Equal(.regionInfos[len(.regionInfos)-1].ID, ) {
							// End last region.
							.regionInfos[len(.regionInfos)-1].ToX =  + 
							.regionInfos[len(.regionInfos)-1].ToY =  +  - .lineOffset
						}
						 = [][1]
						if len() > 0 {
							// Start new region.
							.regionInfos = append(.regionInfos, &textViewRegion{
								ID:    ,
								FromX:  + ,
								FromY:  +  - .lineOffset,
								ToX:   -1,
								ToY:   -1,
							})
						}
						 += [][1] - [][0]
						++
					} else {
						break
					}
				}

				// Skip the second-to-last character of an escape tag.
				if  < len() && + == [][1]-2 {
					++
					++
				}

				// Mix the existing style with the new style.
				, , ,  := .GetContent(+, )
				, ,  := .Decompose()
				 := overlayStyle(, , , , )

				// Do we highlight this character?
				var  bool
				if len() > 0 {
					if ,  := .highlights[string()];  {
						 = true
					}
				}
				if  {
					 := .highlightForeground
					 := .highlightBackground
					if  == tcell.ColorDefault {
						 = Styles.PrimaryTextColor
						if  == tcell.ColorDefault {
							 = tcell.ColorWhite.TrueColor()
						}
					}
					if  == tcell.ColorDefault {
						, ,  := .RGB()
						 := colorful.Color{R: float64() / 255, G: float64() / 255, B: float64() / 255}
						, ,  := .Hcl()
						if  < .5 {
							 = tcell.ColorWhite.TrueColor()
						} else {
							 = tcell.ColorBlack.TrueColor()
						}
					}
					 = .Foreground().Background()
				}

				// Skip to the right.
				if !.wrap &&  <  {
					 += 
					return false
				}

				// Stop at the right border.
				if + >  {
					return true
				}

				// Draw the character.
				for  :=  - 1;  >= 0; -- {
					if  == 0 {
						.SetContent(++, , , , )
					} else {
						.SetContent(++, , ' ', nil, )
					}
				}

				// Advance.
				 += 
				return false
			})
		}
	}

	// If this view is not scrollable, we'll purge the buffer of lines that have
	// scrolled out of view.
	if !.scrollable && .lineOffset > 0 {
		if .lineOffset >= len(.index) {
			.buffer = nil
		} else {
			.buffer = .buffer[.index[.lineOffset].Line:]
		}
		.index = nil
		.lineOffset = 0
	}
}

// InputHandler returns the handler for this primitive.
func ( *TextView) () func( *tcell.EventKey,  func( Primitive)) {
	return .WrapInputHandler(func( *tcell.EventKey,  func( Primitive)) {
		 := .Key()

		if HitShortcut(, Keys.Cancel, Keys.Select, Keys.Select2, Keys.MovePreviousField, Keys.MoveNextField) {
			if .done != nil {
				.done()
			}
			return
		}

		.Lock()
		defer .Unlock()

		if !.scrollable {
			return
		}

		if HitShortcut(, Keys.MoveFirst, Keys.MoveFirst2) {
			.trackEnd = false
			.lineOffset = 0
			.columnOffset = 0
		} else if HitShortcut(, Keys.MoveLast, Keys.MoveLast2) {
			.trackEnd = true
			.columnOffset = 0
		} else if HitShortcut(, Keys.MoveUp, Keys.MoveUp2) {
			.trackEnd = false
			.lineOffset--
		} else if HitShortcut(, Keys.MoveDown, Keys.MoveDown2) {
			.lineOffset++
		} else if HitShortcut(, Keys.MoveLeft, Keys.MoveLeft2) {
			.columnOffset--
		} else if HitShortcut(, Keys.MoveRight, Keys.MoveRight2) {
			.columnOffset++
		} else if HitShortcut(, Keys.MovePreviousPage) {
			.trackEnd = false
			.lineOffset -= .pageSize
		} else if HitShortcut(, Keys.MoveNextPage) {
			.lineOffset += .pageSize
		}
	})
}

// MouseHandler returns the mouse handler for this primitive.
func ( *TextView) () func( MouseAction,  *tcell.EventMouse,  func( Primitive)) ( bool,  Primitive) {
	return .WrapMouseHandler(func( MouseAction,  *tcell.EventMouse,  func( Primitive)) ( bool,  Primitive) {
		,  := .Position()
		if !.InRect(, ) {
			return false, nil
		}

		switch  {
		case MouseLeftClick:
			if .regions {
				// Find a region to highlight.
				for ,  := range .regionInfos {
					if  == .FromY &&  < .FromX ||
						 == .ToY &&  >= .ToX ||
						.FromY >= 0 &&  < .FromY ||
						.ToY >= 0 &&  > .ToY {
						continue
					}
					if .clicked != nil {
						.clicked(string(.ID))
					}
					break
				}
			}
			 = true
			()
		case MouseScrollUp:
			if .scrollable {
				.trackEnd = false
				.lineOffset--
				 = true
			}
		case MouseScrollDown:
			if .scrollable {
				.lineOffset++
				 = true
			}
		}

		return
	})
}