From 7b01fe4f569bcd7c4e24371aee166e9d14db36d5 Mon Sep 17 00:00:00 2001 From: Neko Box Coder Date: Sun, 13 Jul 2025 19:58:57 +0100 Subject: [PATCH 1/3] Splitting draw out to getRuneStyle in bufwindow, removing @ for wide rune in bufwindow --- internal/display/bufwindow.go | 167 +++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 74 deletions(-) diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 1ecb4323..afd650b4 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -494,7 +494,83 @@ func (w *BufWindow) displayBuffer() { } bloc.X = bslice - draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool) { + // returns the rune to be drawn, style of it and if the bg should be preserved + getRuneStyle := func(r rune, style tcell.Style, isplaceholder bool) (rune, tcell.Style, bool) { + if nColsBeforeStart > 0 || vloc.Y < 0 || isplaceholder { + return r, style, false + } + + for _, mb := range matchingBraces { + if mb.X == bloc.X && mb.Y == bloc.Y { + if b.Settings["matchbracestyle"].(string) == "highlight" { + if s, ok := config.Colorscheme["match-brace"]; ok { + return r, s, false + } else { + return r, style.Reverse(true), false + } + } else { + return r, style.Underline(true), false + } + } + } + + if r != '\t' && r != ' ' { + return r, style, false + } + + var drawrune rune + if r == '\t' { + indentrunes := []rune(b.Settings["indentchar"].(string)) + // if empty indentchar settings, use space + if len(indentrunes) == 0 { + indentrunes = []rune{' '} + } + + drawrune = indentrunes[0] + if s, ok := config.Colorscheme["indent-char"]; ok { + fg, _, _ := s.Decompose() + style = style.Foreground(fg) + } + } + + preservebg := false + if b.Settings["hltaberrors"].(bool) && bloc.X < leadingwsEnd { + if s, ok := config.Colorscheme["tab-error"]; ok { + if b.Settings["tabstospaces"].(bool) && r == '\t' { + fg, _, _ := s.Decompose() + style = style.Background(fg) + preservebg = true + } else if !b.Settings["tabstospaces"].(bool) && r == ' ' { + fg, _, _ := s.Decompose() + style = style.Background(fg) + preservebg = 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) + preservebg = true + } + } + } + } + + return drawrune, style, preservebg + } + + draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool, preservebg bool) { if nColsBeforeStart <= 0 && vloc.Y >= 0 { if highlight { if w.Buf.HighlightSearch && w.Buf.SearchMatch(bloc) { @@ -509,37 +585,8 @@ func (w *BufWindow) displayBuffer() { // syntax or hlsearch highlighting with non-default background takes precedence // 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 - } - } - } + if !preservebg && origBg != defBg { + preservebg = true } for _, c := range cursors { @@ -554,7 +601,7 @@ func (w *BufWindow) displayBuffer() { } } - if b.Settings["cursorline"].(bool) && w.active && !dontOverrideBackground && + if b.Settings["cursorline"].(bool) && w.active && !preservebg && !c.HasSelection() && c.Y == bloc.Y { if s, ok := config.Colorscheme["cursor-line"]; ok { fg, _, _ := s.Decompose() @@ -571,40 +618,12 @@ func (w *BufWindow) displayBuffer() { } } - if r == '\t' { - indentrunes := []rune(b.Settings["indentchar"].(string)) - // if empty indentchar settings, use space - if len(indentrunes) == 0 { - indentrunes = []rune{' '} - } - - r = indentrunes[0] - if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' { - fg, _, _ := s.Decompose() - style = style.Foreground(fg) - } - } - if s, ok := config.Colorscheme["color-column"]; ok { - if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !dontOverrideBackground { + if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !preservebg { fg, _, _ := s.Decompose() style = style.Background(fg) } } - - for _, mb := range matchingBraces { - if mb.X == bloc.X && mb.Y == bloc.Y { - if b.Settings["matchbracestyle"].(string) == "highlight" { - if s, ok := config.Colorscheme["match-brace"]; ok { - style = s - } else { - style = style.Reverse(true) - } - } else { - style = style.Underline(true) - } - } - } } screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, combc, style) @@ -692,7 +711,7 @@ func (w *BufWindow) displayBuffer() { // If a word (or just a wide rune) does not fit in the window if vloc.X+wordwidth > maxWidth && vloc.X > w.gutterOffset { for vloc.X < maxWidth { - draw(' ', nil, config.DefStyle, false, false) + draw(' ', nil, config.DefStyle, false, false, true) } // We either stop or we wrap to draw the word in the next line @@ -708,18 +727,17 @@ func (w *BufWindow) displayBuffer() { } for _, r := range word { - draw(r.r, r.combc, r.style, true, true) + drawrune, drawstyle, preservebg := getRuneStyle(r.r, r.style, false) + draw(drawrune, r.combc, drawstyle, true, true, preservebg) - // Draw any extra characters either spaces for tabs or @ for incomplete wide runes - if r.width > 1 { - char := ' ' - if r.r != '\t' { - char = '@' - } - - for i := 1; i < r.width; i++ { - draw(char, nil, r.style, true, false) + // Draw extra characters for tabs or wide runes + for i := 1; i < r.width; i++ { + if r.r == '\t' { + drawrune, drawstyle, preservebg = getRuneStyle('\t', r.style, false) + } else { + drawrune, drawstyle, preservebg = getRuneStyle(' ', r.style, true) } + draw(drawrune, nil, drawstyle, true, false, preservebg) } bloc.X++ } @@ -764,7 +782,8 @@ func (w *BufWindow) displayBuffer() { if vloc.X != maxWidth { // Display newline within a selection - draw(' ', nil, config.DefStyle, true, true) + drawrune, drawstyle, preservebg := getRuneStyle(' ', config.DefStyle, true) + draw(drawrune, nil, drawstyle, true, true, preservebg) } bloc.X = w.StartCol From 532c315f79ed780f424938a75e7995a3bfe13554 Mon Sep 17 00:00:00 2001 From: Neko Box Coder Date: Sun, 13 Jul 2025 21:11:43 +0100 Subject: [PATCH 2/3] Simplifying draw to be less nested --- internal/display/bufwindow.go | 111 ++++++++++++++++++---------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index afd650b4..0abced39 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -571,75 +571,80 @@ func (w *BufWindow) displayBuffer() { } draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool, preservebg bool) { - if nColsBeforeStart <= 0 && vloc.Y >= 0 { - if highlight { - if w.Buf.HighlightSearch && w.Buf.SearchMatch(bloc) { + defer func() { + if nColsBeforeStart <= 0 { + vloc.X++ + } + nColsBeforeStart-- + }() + + if nColsBeforeStart > 0 || vloc.Y < 0 { + return + } + + if highlight { + if w.Buf.HighlightSearch && w.Buf.SearchMatch(bloc) { + style = config.DefStyle.Reverse(true) + if s, ok := config.Colorscheme["hlsearch"]; ok { + style = s + } + } + + _, origBg, _ := style.Decompose() + _, defBg, _ := config.DefStyle.Decompose() + + // syntax or hlsearch highlighting with non-default background takes precedence + // over cursor-line and color-column + if !preservebg && origBg != defBg { + preservebg = true + } + + for _, c := range cursors { + if c.HasSelection() && + (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) || + bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) { + // The current character is selected style = config.DefStyle.Reverse(true) - if s, ok := config.Colorscheme["hlsearch"]; ok { + + if s, ok := config.Colorscheme["selection"]; ok { style = s } } - _, origBg, _ := style.Decompose() - _, defBg, _ := config.DefStyle.Decompose() - - // syntax or hlsearch highlighting with non-default background takes precedence - // over cursor-line and color-column - if !preservebg && origBg != defBg { - preservebg = true - } - - for _, c := range cursors { - if c.HasSelection() && - (bloc.GreaterEqual(c.CurSelection[0]) && bloc.LessThan(c.CurSelection[1]) || - bloc.LessThan(c.CurSelection[0]) && bloc.GreaterEqual(c.CurSelection[1])) { - // The current character is selected - style = config.DefStyle.Reverse(true) - - if s, ok := config.Colorscheme["selection"]; ok { - style = s - } - } - - if b.Settings["cursorline"].(bool) && w.active && !preservebg && - !c.HasSelection() && c.Y == bloc.Y { - if s, ok := config.Colorscheme["cursor-line"]; ok { - fg, _, _ := s.Decompose() - style = style.Background(fg) - } - } - } - - for _, m := range b.Messages { - if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) || - bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) { - style = style.Underline(true) - break - } - } - - if s, ok := config.Colorscheme["color-column"]; ok { - if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !preservebg { + if b.Settings["cursorline"].(bool) && w.active && !preservebg && + !c.HasSelection() && c.Y == bloc.Y { + if s, ok := config.Colorscheme["cursor-line"]; ok { fg, _, _ := s.Decompose() style = style.Background(fg) } } } - screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, combc, style) + for _, m := range b.Messages { + if bloc.GreaterEqual(m.Start) && bloc.LessThan(m.End) || + bloc.LessThan(m.End) && bloc.GreaterEqual(m.Start) { + style = style.Underline(true) + break + } + } - if showcursor { - for _, c := range cursors { - if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() { - w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0) - } + if s, ok := config.Colorscheme["color-column"]; ok { + if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !preservebg { + fg, _, _ := s.Decompose() + style = style.Background(fg) } } } - if nColsBeforeStart <= 0 { - vloc.X++ + + screen.SetContent(w.X+vloc.X, w.Y+vloc.Y, r, combc, style) + + if showcursor { + for _, c := range cursors { + if c.X == bloc.X && c.Y == bloc.Y && !c.HasSelection() { + w.showCursor(w.X+vloc.X, w.Y+vloc.Y, c.Num == 0) + } + } } - nColsBeforeStart-- } wrap := func() { From 1ef6459846305afd52f235980ded744bfc5fbfa9 Mon Sep 17 00:00:00 2001 From: Neko Box Coder Date: Sun, 13 Jul 2025 20:05:39 +0100 Subject: [PATCH 3/3] Adding showchars option --- internal/config/settings.go | 4 +- internal/display/bufwindow.go | 72 +++++++++++++++++++++++++++-------- runtime/help/options.md | 28 +++++++++++--- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/internal/config/settings.go b/internal/config/settings.go index 14e5f18b..38c3751c 100644 --- a/internal/config/settings.go +++ b/internal/config/settings.go @@ -70,7 +70,7 @@ var defaultCommonSettings = map[string]interface{}{ "hltrailingws": false, "ignorecase": true, "incsearch": true, - "indentchar": " ", + "indentchar": " ", // Deprecated "keepautoindent": false, "matchbrace": true, "matchbraceleft": true, @@ -88,6 +88,7 @@ var defaultCommonSettings = map[string]interface{}{ "scrollbar": false, "scrollmargin": float64(3), "scrollspeed": float64(2), + "showchars": "", "smartpaste": true, "softwrap": false, "splitbottom": true, @@ -210,6 +211,7 @@ func validateParsedSettings() error { } continue } + if _, ok := defaults[k]; ok { if e := verifySetting(k, v, defaults[k]); e != nil { err = e diff --git a/internal/display/bufwindow.go b/internal/display/bufwindow.go index 0abced39..e780cab0 100644 --- a/internal/display/bufwindow.go +++ b/internal/display/bufwindow.go @@ -2,6 +2,7 @@ package display import ( "strconv" + "strings" runewidth "github.com/mattn/go-runewidth" "github.com/micro-editor/tcell/v2" @@ -450,6 +451,30 @@ func (w *BufWindow) displayBuffer() { cursors := b.GetCursors() curStyle := config.DefStyle + + // Parse showchars which is in the format of key1=val1,key2=val2,... + spacechars := " " + tabchars := b.Settings["indentchar"].(string) + var indentspacechars string + var indenttabchars string + for _, entry := range strings.Split(b.Settings["showchars"].(string), ",") { + split := strings.SplitN(entry, "=", 2) + if len(split) < 2 { + continue + } + key, val := split[0], split[1] + switch key { + case "space": + spacechars = val + case "tab": + tabchars = val + case "ispace": + indentspacechars = val + case "itab": + indenttabchars = val + } + } + for ; vloc.Y < w.bufHeight; vloc.Y++ { vloc.X = 0 @@ -495,7 +520,7 @@ func (w *BufWindow) displayBuffer() { bloc.X = bslice // returns the rune to be drawn, style of it and if the bg should be preserved - getRuneStyle := func(r rune, style tcell.Style, isplaceholder bool) (rune, tcell.Style, bool) { + getRuneStyle := func(r rune, style tcell.Style, showoffset int, linex int, isplaceholder bool) (rune, tcell.Style, bool) { if nColsBeforeStart > 0 || vloc.Y < 0 || isplaceholder { return r, style, false } @@ -518,19 +543,33 @@ func (w *BufWindow) displayBuffer() { return r, style, false } - var drawrune rune - if r == '\t' { - indentrunes := []rune(b.Settings["indentchar"].(string)) - // if empty indentchar settings, use space - if len(indentrunes) == 0 { - indentrunes = []rune{' '} + var indentrunes []rune + switch r { + case '\t': + if bloc.X < leadingwsEnd && indenttabchars != "" { + indentrunes = []rune(indenttabchars) + } else { + indentrunes = []rune(tabchars) } + case ' ': + if linex%tabsize == 0 && bloc.X < leadingwsEnd && indentspacechars != "" { + indentrunes = []rune(indentspacechars) + } else { + indentrunes = []rune(spacechars) + } + } - drawrune = indentrunes[0] - if s, ok := config.Colorscheme["indent-char"]; ok { - fg, _, _ := s.Decompose() - style = style.Foreground(fg) - } + var drawrune rune + if showoffset < len(indentrunes) { + drawrune = indentrunes[showoffset] + } else { + // use space if no showchars or after we showed showchars + drawrune = ' ' + } + + if s, ok := config.Colorscheme["indent-char"]; ok { + fg, _, _ := s.Decompose() + style = style.Foreground(fg) } preservebg := false @@ -692,6 +731,7 @@ func (w *BufWindow) displayBuffer() { width := 0 + linex := totalwidth switch r { case '\t': ts := tabsize - (totalwidth % tabsize) @@ -732,15 +772,15 @@ func (w *BufWindow) displayBuffer() { } for _, r := range word { - drawrune, drawstyle, preservebg := getRuneStyle(r.r, r.style, false) + drawrune, drawstyle, preservebg := getRuneStyle(r.r, r.style, 0, linex, false) draw(drawrune, r.combc, drawstyle, true, true, preservebg) // Draw extra characters for tabs or wide runes for i := 1; i < r.width; i++ { if r.r == '\t' { - drawrune, drawstyle, preservebg = getRuneStyle('\t', r.style, false) + drawrune, drawstyle, preservebg = getRuneStyle('\t', r.style, i, linex+i, false) } else { - drawrune, drawstyle, preservebg = getRuneStyle(' ', r.style, true) + drawrune, drawstyle, preservebg = getRuneStyle(' ', r.style, i, linex+i, true) } draw(drawrune, nil, drawstyle, true, false, preservebg) } @@ -787,7 +827,7 @@ func (w *BufWindow) displayBuffer() { if vloc.X != maxWidth { // Display newline within a selection - drawrune, drawstyle, preservebg := getRuneStyle(' ', config.DefStyle, true) + drawrune, drawstyle, preservebg := getRuneStyle(' ', config.DefStyle, 0, totalwidth, true) draw(drawrune, nil, drawstyle, true, true, preservebg) } diff --git a/runtime/help/options.md b/runtime/help/options.md index e16d2259..4b21e018 100644 --- a/runtime/help/options.md +++ b/runtime/help/options.md @@ -203,12 +203,8 @@ Here are the available options: default value: `true` -* `indentchar`: sets the indentation character. This will not be inserted into - files; it is only a visual indicator that whitespace is present. If set to a - printing character, it functions as a subset of the "show invisibles" - setting available in many other text editors. The color of this character is - determined by the `indent-char` field in the current theme rather than the - default text color. +* `indentchar`: sets the character to be shown to display tab characters. + This option is **deprecated**, use the `tab` key in `showchars` option instead. default value: ` ` (space) @@ -386,6 +382,25 @@ Here are the available options: default value: `2` +* `showchars`: sets what characters to be shown to display various invisible + characters in the file. The characters shown will not be inserted into files. + This option is specified in the form of `key1=value1,key2=value2,...`. + + Here are the list of keys: + - `space`: space characters + - `tab`: tab characters. If set, overrides the `indentchar` option. + - `ispace`: space characters at indent position before the first visible + character in a line. If this is not set, `space` will be shown + instead. + - `itab`: tab characters before the first visible character in a line. + If this is not set, `tab` will be shown instead. + An example of this option value could be `tab=>,space=.,itab=|>,ispace=|` + + The color of the shown character is determined by the `indent-char` + field in the current theme rather than the default text color. + + default value: `` + * `smartpaste`: add leading whitespace when pasting multiple lines. This will attempt to preserve the current indentation level when pasting an unindented block. @@ -577,6 +592,7 @@ so that you can see what the formatting should look like. "scrollbarchar": "|", "scrollmargin": 3, "scrollspeed": 2, + "showchars": "", "smartpaste": true, "softwrap": false, "splitbottom": true,