From 104caf08dd9ee1bd4be324ff9867b629aca3a08a Mon Sep 17 00:00:00 2001 From: Dmitry Maluka Date: Tue, 20 Oct 2020 22:52:49 +0200 Subject: [PATCH] Highlighting trailing whitespaces Added option `hltrailingws` for highlighting trailing whitespaces at the end of lines. Note that it behaves in a "smart" way. It doesn't highlight newly added (transient) trailing whitespaces that naturally occur while typing text. It would be annoying to see transient highlighting every time we enter a space at the end of a line while typing. So a newly added trailing whitespace starts being highlighting only after the cursor moves to another line. Thus the highlighting serves its purpose: it draws our attention to annoying sloppy forgotten trailing whitespaces. --- internal/action/bufpane.go | 7 +++++ internal/buffer/cursor.go | 7 +++++ internal/buffer/eventhandler.go | 51 +++++++++++++++++++++++++++++++++ internal/config/settings.go | 1 + internal/display/bufwindow.go | 25 +++++++++++++++- internal/util/util.go | 23 +++++++++++++++ runtime/help/options.md | 6 ++++ 7 files changed, 119 insertions(+), 1 deletion(-) diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index dea7b906..89d174c7 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -509,6 +509,13 @@ func (h *BufPane) HandleEvent(event tcell.Event) { InfoBar.ClearGutter() } } + + cursors := h.Buf.GetCursors() + for _, c := range cursors { + if c.NewTrailingWsY != c.Y { + c.NewTrailingWsY = -1 + } + } } // Bindings returns the current bindings tree for this buffer. diff --git a/internal/buffer/cursor.go b/internal/buffer/cursor.go index 12fc5db2..bd3ae068 100644 --- a/internal/buffer/cursor.go +++ b/internal/buffer/cursor.go @@ -30,6 +30,11 @@ type Cursor struct { // to know what the original selection was OrigSelection [2]Loc + // The line number where a new trailing whitespace has been added + // or -1 if there is no new trailing whitespace at this cursor. + // This is used for checking if a trailing whitespace should be highlighted + NewTrailingWsY int + // Which cursor index is this (for multiple cursors) Num int } @@ -38,6 +43,8 @@ func NewCursor(b *Buffer, l Loc) *Cursor { c := &Cursor{ buf: b, Loc: l, + + NewTrailingWsY: -1, } c.StoreVisualX() return c diff --git a/internal/buffer/eventhandler.go b/internal/buffer/eventhandler.go index 6be34bce..66c44dba 100644 --- a/internal/buffer/eventhandler.go +++ b/internal/buffer/eventhandler.go @@ -106,6 +106,8 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) { c.Relocate() c.LastVisualX = c.GetVisualX() } + + eh.updateTrailingWs(t) } // ExecuteTextEvent runs a text event @@ -342,3 +344,52 @@ func (eh *EventHandler) RedoOneEvent() { eh.UndoStack.Push(t) } + +// updateTrailingWs updates the cursor's trailing whitespace status after a text event +func (eh *EventHandler) updateTrailingWs(t *TextEvent) { + if len(t.Deltas) != 1 { + return + } + text := t.Deltas[0].Text + start := t.Deltas[0].Start + end := t.Deltas[0].End + + c := eh.cursors[eh.active] + isEol := func(loc Loc) bool { + return loc.X == util.CharacterCount(eh.buf.LineBytes(loc.Y)) + } + if t.EventType == TextEventInsert && c.Loc == end && isEol(end) { + var addedTrailingWs bool + addedAfterWs := false + addedWsOnly := false + if start.Y == end.Y { + addedTrailingWs = util.HasTrailingWhitespace(text) + addedWsOnly = util.IsBytesWhitespace(text) + addedAfterWs = start.X > 0 && util.IsWhitespace(c.buf.RuneAt(Loc{start.X - 1, start.Y})) + } else { + lastnl := bytes.LastIndex(text, []byte{'\n'}) + addedTrailingWs = util.HasTrailingWhitespace(text[lastnl+1:]) + } + + if addedTrailingWs && !(addedAfterWs && addedWsOnly) { + c.NewTrailingWsY = c.Y + } else if !addedTrailingWs { + c.NewTrailingWsY = -1 + } + } else if t.EventType == TextEventRemove && c.Loc == start && isEol(start) { + removedAfterWs := util.HasTrailingWhitespace(eh.buf.LineBytes(start.Y)) + var removedWsOnly bool + if start.Y == end.Y { + removedWsOnly = util.IsBytesWhitespace(text) + } else { + firstnl := bytes.Index(text, []byte{'\n'}) + removedWsOnly = util.IsBytesWhitespace(text[:firstnl]) + } + + if removedAfterWs && !removedWsOnly { + c.NewTrailingWsY = c.Y + } else if !removedAfterWs { + c.NewTrailingWsY = -1 + } + } +} diff --git a/internal/config/settings.go b/internal/config/settings.go index 40e271b7..bfb1061f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -290,6 +290,7 @@ var defaultCommonSettings = map[string]interface{}{ "filetype": "unknown", "hlsearch": false, "hltaberrors": false, + "hltrailingws": false, "incsearch": true, "ignorecase": true, "indentchar": " ", diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 8d4645e0..942dd167 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -495,7 +495,11 @@ func (w *BufWindow) displayBuffer() { vloc.X = w.gutterOffset } - leadingwsEnd := len(util.GetLeadingWhitespace(b.LineBytes(bloc.Y))) + bline := b.LineBytes(bloc.Y) + blineLen := util.CharacterCount(bline) + + leadingwsEnd := len(util.GetLeadingWhitespace(bline)) + trailingwsStart := blineLen - util.CharacterCount(util.GetTrailingWhitespace(bline)) line, nColsBeforeStart, bslice, startStyle := w.getStartInfo(w.StartCol, bloc.Y) if startStyle != nil { @@ -532,6 +536,25 @@ func (w *BufWindow) displayBuffer() { } } + if b.Settings["hltrailingws"].(bool) { + if s, ok := config.Colorscheme["trailingws"]; ok { + if bloc.X >= trailingwsStart && bloc.X < blineLen { + hl := true + for _, c := range cursors { + if c.NewTrailingWsY == bloc.Y { + hl = false + break + } + } + if hl { + fg, _, _ := s.Decompose() + style = style.Background(fg) + dontOverrideBackground = true + } + } + } + } + for _, c := range cursors { if c.HasSelection() && (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) || diff --git a/internal/util/util.go b/internal/util/util.go index fb21c487..bebd949b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -16,6 +16,7 @@ import ( "strings" "time" "unicode" + "unicode/utf8" "github.com/blang/semver" runewidth "github.com/mattn/go-runewidth" @@ -363,6 +364,28 @@ func GetLeadingWhitespace(b []byte) []byte { return ws } +// GetTrailingWhitespace returns the trailing whitespace of the given byte array +func GetTrailingWhitespace(b []byte) []byte { + ws := []byte{} + for len(b) > 0 { + r, size := utf8.DecodeLastRune(b) + if IsWhitespace(r) { + ws = append([]byte(string(r)), ws...) + } else { + break + } + + b = b[:len(b)-size] + } + return ws +} + +// HasTrailingWhitespace returns true if the given byte array ends with a whitespace +func HasTrailingWhitespace(b []byte) bool { + r, _ := utf8.DecodeLastRune(b) + return IsWhitespace(r) +} + // IntOpt turns a float64 setting to an int func IntOpt(opt interface{}) int { return int(opt.(float64)) diff --git a/runtime/help/options.md b/runtime/help/options.md index 3826f27b..72075820 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -181,6 +181,12 @@ Here are the available options: default value: `false` +* `hltrailingws`: highlight trailing whitespaces at ends of lines. Note that + it doesn't highlight newly added trailing whitespaces that naturally occur + while typing text. It highlights only nasty forgotten trailing whitespaces. + + default value: `false` + * `incsearch`: enable incremental search in "Find" prompt (matching as you type). default value: `true`