diff --git a/README.md b/README.md index 6d4a8579..3be5f9b3 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ You can also check out the website for Micro at https://micro-editor.github.io. - Small and simple. - Easily configurable. - Macros. +- Smart highlighting of trailing whitespace and tab vs space errors. - Common editor features such as undo/redo, line numbers, Unicode support, soft wrapping, … ## Installation diff --git a/internal/action/bufpane.go b/internal/action/bufpane.go index dea7b906..2baf6739 100644 --- a/internal/action/bufpane.go +++ b/internal/action/bufpane.go @@ -509,6 +509,14 @@ func (h *BufPane) HandleEvent(event tcell.Event) { InfoBar.ClearGutter() } } + + cursors := h.Buf.GetCursors() + for _, c := range cursors { + if c.NewTrailingWsY != c.Y && (!c.HasSelection() || + (c.NewTrailingWsY != c.CurSelection[0].Y && c.NewTrailingWsY != c.CurSelection[1].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..53f64025 100644 --- a/internal/buffer/eventhandler.go +++ b/internal/buffer/eventhandler.go @@ -106,6 +106,10 @@ func (eh *EventHandler) DoTextEvent(t *TextEvent, useUndo bool) { c.Relocate() c.LastVisualX = c.GetVisualX() } + + if useUndo { + eh.updateTrailingWs(t) + } } // ExecuteTextEvent runs a text event @@ -290,6 +294,7 @@ func (eh *EventHandler) UndoOneEvent() { if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) { t.C = *eh.cursors[teCursor.Num] eh.cursors[teCursor.Num].Goto(teCursor) + eh.cursors[teCursor.Num].NewTrailingWsY = teCursor.NewTrailingWsY } else { teCursor.Num = -1 } @@ -333,6 +338,7 @@ func (eh *EventHandler) RedoOneEvent() { if teCursor.Num >= 0 && teCursor.Num < len(eh.cursors) { t.C = *eh.cursors[teCursor.Num] eh.cursors[teCursor.Num].Goto(teCursor) + eh.cursors[teCursor.Num].NewTrailingWsY = teCursor.NewTrailingWsY } else { teCursor.Num = -1 } @@ -342,3 +348,58 @@ 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 + } + } else if c.NewTrailingWsY != -1 && start.Y != end.Y && c.Loc.GreaterThan(start) && + ((t.EventType == TextEventInsert && c.Y == c.NewTrailingWsY+(end.Y-start.Y)) || + (t.EventType == TextEventRemove && c.Y == c.NewTrailingWsY-(end.Y-start.Y))) { + // The cursor still has its new trailingws + // but its line number was shifted by insert or remove of lines above + c.NewTrailingWsY = c.Y + } +} diff --git a/internal/config/settings.go b/internal/config/settings.go index 3b9bfaef..bfb1061f 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -289,6 +289,8 @@ var defaultCommonSettings = map[string]interface{}{ "fileformat": defaultFileFormat(), "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 6e67f845..942dd167 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -495,6 +495,12 @@ func (w *BufWindow) displayBuffer() { vloc.X = w.gutterOffset } + 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 { curStyle = *startStyle @@ -518,6 +524,37 @@ func (w *BufWindow) displayBuffer() { // over cursor-line and color-column dontOverrideBackground := origBg != defBg + if b.Settings["hltaberrors"].(bool) { + if s, ok := config.Colorscheme["tab-error"]; ok { + isTab := (r == '\t') || (r == ' ' && !showcursor) + if (b.Settings["tabstospaces"].(bool) && isTab) || + (!b.Settings["tabstospaces"].(bool) && bloc.X < leadingwsEnd && r == ' ' && !isTab) { + fg, _, _ := s.Decompose() + style = style.Background(fg) + dontOverrideBackground = true + } + } + } + + 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/colorschemes/atom-dark.micro b/runtime/colorschemes/atom-dark.micro index d7f8eff6..0f462995 100644 --- a/runtime/colorschemes/atom-dark.micro +++ b/runtime/colorschemes/atom-dark.micro @@ -29,3 +29,5 @@ color-link color-column "#2D2F31" #color-link type.extended "default" #Plain brackets color-link match-brace "#1D1F21,#62B1FE" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/bubblegum.micro b/runtime/colorschemes/bubblegum.micro index 4c039d4d..dcc2276d 100644 --- a/runtime/colorschemes/bubblegum.micro +++ b/runtime/colorschemes/bubblegum.micro @@ -27,3 +27,5 @@ color-link color-column "254" color-link type.extended "241,231" color-link symbol.brackets "241,231" color-link match-brace "231,28" +color-link tab-error "210" +color-link trailingws "210" diff --git a/runtime/colorschemes/cmc-16.micro b/runtime/colorschemes/cmc-16.micro index 09ad6eaf..0a50096c 100644 --- a/runtime/colorschemes/cmc-16.micro +++ b/runtime/colorschemes/cmc-16.micro @@ -43,3 +43,5 @@ color-link color-column "cyan" color-link underlined.url "underline blue, white" color-link divider "blue" color-link match-brace "black,cyan" +color-link tab-error "brightred" +color-link trailingws "brightred" diff --git a/runtime/colorschemes/cmc-tc.micro b/runtime/colorschemes/cmc-tc.micro index f142210e..b0502c6f 100644 --- a/runtime/colorschemes/cmc-tc.micro +++ b/runtime/colorschemes/cmc-tc.micro @@ -39,3 +39,5 @@ color-link constant.bool "bold #55ffff" color-link constant.bool.true "bold #85ff85" color-link constant.bool.false "bold #ff8585" color-link match-brace "#1e2124,#55ffff" +color-link tab-error "#d75f5f" +color-link trailingws "#d75f5f" diff --git a/runtime/colorschemes/darcula.micro b/runtime/colorschemes/darcula.micro index 560ab585..7103e842 100644 --- a/runtime/colorschemes/darcula.micro +++ b/runtime/colorschemes/darcula.micro @@ -30,3 +30,5 @@ color-link type.extended "default" #color-link symbol.brackets "default" color-link symbol.tag "#AE81FF,#242424" color-link match-brace "#242424,#7A9EC2" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/default.micro b/runtime/colorschemes/default.micro index 1c3b5eab..9ae5bfd6 100644 --- a/runtime/colorschemes/default.micro +++ b/runtime/colorschemes/default.micro @@ -30,3 +30,5 @@ color-link type.extended "default" #color-link symbol.brackets "default" color-link symbol.tag "#AE81FF,#282828" color-link match-brace "#282828,#AE81FF" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/dracula-tc.micro b/runtime/colorschemes/dracula-tc.micro index b242eb6c..af0509c7 100644 --- a/runtime/colorschemes/dracula-tc.micro +++ b/runtime/colorschemes/dracula-tc.micro @@ -44,3 +44,6 @@ color-link color-column "#44475A" color-link type.extended "default" color-link match-brace "#282A36,#FF79C6" + +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/dukedark-tc.micro b/runtime/colorschemes/dukedark-tc.micro index 54afff60..52ec8476 100644 --- a/runtime/colorschemes/dukedark-tc.micro +++ b/runtime/colorschemes/dukedark-tc.micro @@ -34,3 +34,5 @@ color-link type.keyword "bold #5aaae6,#001e28" color-link type.extended "#ffffff,#001e28" color-link underlined "#608b4e,#001e28" color-link match-brace "#001e28,#5aaae6" +color-link tab-error "#d75f5f" +color-link trailingws "#d75f5f" diff --git a/runtime/colorschemes/dukelight-tc.micro b/runtime/colorschemes/dukelight-tc.micro index c381f2b1..c694ffb6 100644 --- a/runtime/colorschemes/dukelight-tc.micro +++ b/runtime/colorschemes/dukelight-tc.micro @@ -34,3 +34,5 @@ color-link type.keyword "bold #780050,#f0f0f0" color-link type.extended "#000000,#f0f0f0" color-link underlined "#3f7f5f,#f0f0f0" color-link match-brace "#f0f0f0,#780050" +color-link tab-error "#ff8787" +color-link trailingws "#ff8787" diff --git a/runtime/colorschemes/dukeubuntu-tc.micro b/runtime/colorschemes/dukeubuntu-tc.micro index 7c9f7afb..b34cc2c4 100644 --- a/runtime/colorschemes/dukeubuntu-tc.micro +++ b/runtime/colorschemes/dukeubuntu-tc.micro @@ -34,3 +34,5 @@ color-link type.keyword "bold #5aaae6,#2d0023" color-link type.extended "#ffffff,#2d0023" color-link underlined "#886484,#2d0023" color-link match-brace "#2d0023,#5aaae6" +color-link tab-error "#d75f5f" +color-link trailingws "#d75f5f" diff --git a/runtime/colorschemes/geany.micro b/runtime/colorschemes/geany.micro index 7333a2a2..43ced31a 100644 --- a/runtime/colorschemes/geany.micro +++ b/runtime/colorschemes/geany.micro @@ -25,3 +25,5 @@ color-link diff-deleted "red" color-link gutter-error ",red" color-link gutter-warning "red" color-link match-brace "black,cyan" +color-link tab-error "brightred" +color-link trailingws "brightred" diff --git a/runtime/colorschemes/gotham.micro b/runtime/colorschemes/gotham.micro index 600822b6..d067a81c 100644 --- a/runtime/colorschemes/gotham.micro +++ b/runtime/colorschemes/gotham.micro @@ -25,3 +25,5 @@ color-link cursor-line "#091F2E" color-link color-column "#11151C" color-link symbol "#99D1CE,#0C1014" color-link match-brace "#0C1014,#D26937" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/gruvbox-tc.micro b/runtime/colorschemes/gruvbox-tc.micro index e6301e67..65d72b15 100644 --- a/runtime/colorschemes/gruvbox-tc.micro +++ b/runtime/colorschemes/gruvbox-tc.micro @@ -25,3 +25,5 @@ color-link color-column "#79740e" color-link statusline "#ebdbb2,#665c54" color-link tabbar "#ebdbb2,#665c54" color-link match-brace "#282828,#d3869b" +color-link tab-error "#d75f5f" +color-link trailingws "#d75f5f" diff --git a/runtime/colorschemes/gruvbox.micro b/runtime/colorschemes/gruvbox.micro index a59e99ef..46cc09cf 100644 --- a/runtime/colorschemes/gruvbox.micro +++ b/runtime/colorschemes/gruvbox.micro @@ -22,3 +22,5 @@ color-link color-column "237" color-link statusline "223,237" color-link tabbar "223,237" color-link match-brace "235,72" +color-link tab-error "167" +color-link trailingws "167" diff --git a/runtime/colorschemes/material-tc.micro b/runtime/colorschemes/material-tc.micro index 561cf81c..5a7f9c89 100644 --- a/runtime/colorschemes/material-tc.micro +++ b/runtime/colorschemes/material-tc.micro @@ -31,3 +31,5 @@ color-link todo "bold #C792EA,#263238" color-link type "#FFCB6B,#263238" color-link underlined "underline #EEFFFF,#263238" color-link match-brace "#263238,#C792EA" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/monokai-dark.micro b/runtime/colorschemes/monokai-dark.micro index 51174309..3a1e89c4 100644 --- a/runtime/colorschemes/monokai-dark.micro +++ b/runtime/colorschemes/monokai-dark.micro @@ -24,3 +24,5 @@ color-link gutter-warning "#E6DB74" color-link cursor-line "#323232" color-link color-column "#323232" color-link match-brace "#1D0000,#AE81FF" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/monokai.micro b/runtime/colorschemes/monokai.micro index e33cf830..13c44b74 100644 --- a/runtime/colorschemes/monokai.micro +++ b/runtime/colorschemes/monokai.micro @@ -30,3 +30,5 @@ color-link type.extended "default" #color-link symbol.brackets "default" color-link symbol.tag "#AE81FF,#282828" color-link match-brace "#282828,#AE81FF" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/one-dark.micro b/runtime/colorschemes/one-dark.micro index b6c96954..ed994321 100644 --- a/runtime/colorschemes/one-dark.micro +++ b/runtime/colorschemes/one-dark.micro @@ -35,3 +35,5 @@ color-link type "#66D9EF" color-link type.keyword "#C678DD" color-link underlined "#8996A8" color-link match-brace "#21252C,#C678DD" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/railscast.micro b/runtime/colorschemes/railscast.micro index 61934b94..01c0055d 100644 --- a/runtime/colorschemes/railscast.micro +++ b/runtime/colorschemes/railscast.micro @@ -28,6 +28,8 @@ color-link tabbar "bold #b1b1b1,#232323" color-link cursor-line "#353535" color-link color-column "#353535" color-link space "underline #e6e1dc,#2b2b2b" +color-link tab-error "#d75f5f" +color-link trailingws "#d75f5f" #the Python syntax definition are wrong. This is not how you should do decorators! color-link brightgreen "#edb753,#2b2b2b" diff --git a/runtime/colorschemes/simple.micro b/runtime/colorschemes/simple.micro index 74c71e02..707c04cb 100644 --- a/runtime/colorschemes/simple.micro +++ b/runtime/colorschemes/simple.micro @@ -29,3 +29,5 @@ color-link symbol.brackets "default" #Color shebangs the comment color color-link preproc.shebang "comment" color-link match-brace ",magenta" +color-link tab-error "brightred" +color-link trailingws "brightred" diff --git a/runtime/colorschemes/solarized-tc.micro b/runtime/colorschemes/solarized-tc.micro index d68a024a..8eae0391 100644 --- a/runtime/colorschemes/solarized-tc.micro +++ b/runtime/colorschemes/solarized-tc.micro @@ -27,3 +27,5 @@ color-link color-column "#003541" color-link type.extended "#839496,#002833" color-link symbol.brackets "#839496,#002833" color-link match-brace "#002833,#268BD2" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/solarized.micro b/runtime/colorschemes/solarized.micro index 745b46ea..4d3923ad 100644 --- a/runtime/colorschemes/solarized.micro +++ b/runtime/colorschemes/solarized.micro @@ -26,3 +26,5 @@ color-link color-column "black" color-link type.extended "default" color-link symbol.brackets "default" color-link match-brace ",blue" +color-link tab-error "brightred" +color-link trailingws "brightred" diff --git a/runtime/colorschemes/sunny-day.micro b/runtime/colorschemes/sunny-day.micro index 82e4b8f4..c4161bce 100644 --- a/runtime/colorschemes/sunny-day.micro +++ b/runtime/colorschemes/sunny-day.micro @@ -25,3 +25,5 @@ color-link cursor-line "229" #color-link color-column "196" color-link current-line-number "246" color-line match-brace "230,22" +color-link tab-error "210" +color-link trailingws "210" diff --git a/runtime/colorschemes/twilight.micro b/runtime/colorschemes/twilight.micro index 224fb7fd..8135bb80 100644 --- a/runtime/colorschemes/twilight.micro +++ b/runtime/colorschemes/twilight.micro @@ -36,3 +36,5 @@ color-link type "#F9EE98" color-link type.keyword "#CDA869" color-link underlined "#8996A8" color-link match-brace "#141414,#E0C589" +color-link tab-error "#D75F5F" +color-link trailingws "#D75F5F" diff --git a/runtime/colorschemes/zenburn.micro b/runtime/colorschemes/zenburn.micro index e4f91175..acbd83fb 100644 --- a/runtime/colorschemes/zenburn.micro +++ b/runtime/colorschemes/zenburn.micro @@ -26,3 +26,5 @@ color-link cursor-line "238" color-link color-column "238" color-link current-line-number "188,237" color-link match-brace "237,223" +color-link tab-error "167" +color-link trailingws "167" diff --git a/runtime/help/options.md b/runtime/help/options.md index a492bc44..72075820 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -174,6 +174,19 @@ Here are the available options: default value: `false` +* `hltaberrors`: highlight tabs when spaces are expected, and spaces when tabs + are expected. More precisely: if `tabstospaces` option is on, highlight + all tab characters; if `tabstospaces` is off, highlight space characters + in the initial indent part of the line. + + 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` diff --git a/runtime/plugins/autoclose/autoclose.lua b/runtime/plugins/autoclose/autoclose.lua index 531b7601..f1fc2fad 100644 --- a/runtime/plugins/autoclose/autoclose.lua +++ b/runtime/plugins/autoclose/autoclose.lua @@ -50,11 +50,11 @@ function preInsertNewline(bp) for i = 1, #autoNewlinePairs do if curRune == charAt(autoNewlinePairs[i], 1) then if nextRune == charAt(autoNewlinePairs[i], 2) then - bp:InsertNewline() - bp:InsertTab() bp.Buf:Insert(-bp.Cursor.Loc, "\n" .. ws) bp:StartOfLine() bp:CursorLeft() + bp:InsertNewline() + bp:InsertTab() return false end end