Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion internal/config/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ var defaultCommonSettings = map[string]interface{}{
"hltrailingws": false,
"ignorecase": true,
"incsearch": true,
"indentchar": " ",
"indentchar": " ", // Deprecated
"keepautoindent": false,
"matchbrace": true,
"matchbraceleft": true,
Expand All @@ -88,6 +88,7 @@ var defaultCommonSettings = map[string]interface{}{
"scrollbar": false,
"scrollmargin": float64(3),
"scrollspeed": float64(2),
"showchars": "",
"smartpaste": true,
"softwrap": false,
"splitbottom": true,
Expand Down Expand Up @@ -210,6 +211,7 @@ func validateParsedSettings() error {
}
continue
}

if _, ok := defaults[k]; ok {
if e := verifySetting(k, v, defaults[k]); e != nil {
err = e
Expand Down
284 changes: 174 additions & 110 deletions internal/display/bufwindow.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package display

import (
"strconv"
"strings"

runewidth "github.com/mattn/go-runewidth"
"github.com/micro-editor/tcell/v2"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -494,133 +519,171 @@ func (w *BufWindow) displayBuffer() {
}
bloc.X = bslice

draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool) {
if nColsBeforeStart <= 0 && vloc.Y >= 0 {
if highlight {
if w.Buf.HighlightSearch && w.Buf.SearchMatch(bloc) {
style = config.DefStyle.Reverse(true)
if s, ok := config.Colorscheme["hlsearch"]; ok {
style = s
// returns the rune to be drawn, style of it and if the bg should be preserved

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the point of this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The point of the comment is to describe the return values, otherwise you have no idea what it returns unless you read the rest of the impl/call sites.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what we do - look at the call sites to see how the function is actually used and what for. If we are not interested in the call sites, why would we be interested in the function itself?

If the problem is just that we don't see the names of the return values here in the function declaration, we could use named return values? Or the problem is that we don't want to use them since the line would become enormously long (which it already is)?

Anyway, I'm ok with keeping it as is.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But again, why are you doing that in this commit "Simplifying draw to be less nested" which is supposed to be just about simplifying draw() to be less nested? This comment should added in the first commit, where getRuneStyle() itself is added?

And same about renaming bgoverridable and returnrune - why do that in this commit, and why do that at all? If we agree they should be preservebg and drawrune, we should name them preservebg and drawrune from the beginning, in the first commit?

I genuinely don't get it.

@Neko-Box-Coder Neko-Box-Coder Jul 20, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's fine, just merged to the wrong commit, have moved it to the first one. Should be good now?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, now looks good.

Except that, the commit message of the 2nd commit includes an outdated sentence?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oops, missed that. Have removed it.

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
}

for _, mb := range matchingBraces {
Comment thread
dmaluka marked this conversation as resolved.
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
}
}
}

_, origBg, _ := style.Decompose()
_, defBg, _ := config.DefStyle.Decompose()

// 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
Comment thread
dmaluka marked this conversation as resolved.
}
}
if r != '\t' && r != ' ' {
return r, style, false
}

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)
}
}

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
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)
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)
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)
return drawrune, style, preservebg
}

if s, ok := config.Colorscheme["selection"]; ok {
style = s
}
}
draw := func(r rune, combc []rune, style tcell.Style, highlight bool, showcursor bool, preservebg bool) {
defer func() {
Comment thread
dmaluka marked this conversation as resolved.
if nColsBeforeStart <= 0 {
vloc.X++
}
nColsBeforeStart--
}()

if b.Settings["cursorline"].(bool) && w.active && !dontOverrideBackground &&
!c.HasSelection() && c.Y == bloc.Y {
if s, ok := config.Colorscheme["cursor-line"]; ok {
fg, _, _ := s.Decompose()
style = style.Background(fg)
}
}
}
if nColsBeforeStart > 0 || vloc.Y < 0 {
return
}

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 highlight {
if w.Buf.HighlightSearch && w.Buf.SearchMatch(bloc) {
style = config.DefStyle.Reverse(true)
if s, ok := config.Colorscheme["hlsearch"]; ok {
style = s
}
}

if r == '\t' {
indentrunes := []rune(b.Settings["indentchar"].(string))
// if empty indentchar settings, use space
if len(indentrunes) == 0 {
indentrunes = []rune{' '}
}
_, origBg, _ := style.Decompose()
_, defBg, _ := config.DefStyle.Decompose()

r = indentrunes[0]
if s, ok := config.Colorscheme["indent-char"]; ok && r != ' ' {
fg, _, _ := s.Decompose()
style = style.Foreground(fg)
// 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 s, ok := config.Colorscheme["color-column"]; ok {
if colorcolumn != 0 && vloc.X-w.gutterOffset+w.StartCol == colorcolumn && !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()
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)
}
}
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
}
}

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)
}
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() {
Expand Down Expand Up @@ -668,6 +731,7 @@ func (w *BufWindow) displayBuffer() {

width := 0

linex := totalwidth

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visualx? (like in VLocFromLoc() for example)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't use visiualx because it's quite easy to get mixed up with vloc.X, which is different.
vloc.X resets on wrap but this doesn't, this is x (draw/visual?) position in a line.

switch r {
case '\t':
ts := tabsize - (totalwidth % tabsize)
Expand All @@ -692,7 +756,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
Expand All @@ -708,18 +772,17 @@ func (w *BufWindow) displayBuffer() {
}

for _, r := range word {
draw(r.r, r.combc, r.style, true, true)

// 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)
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, i, linex+i, false)
} else {
drawrune, drawstyle, preservebg = getRuneStyle(' ', r.style, i, linex+i, true)
}
draw(drawrune, nil, drawstyle, true, false, preservebg)
}
bloc.X++
}
Expand Down Expand Up @@ -764,7 +827,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, 0, totalwidth, true)
draw(drawrune, nil, drawstyle, true, true, preservebg)
}

bloc.X = w.StartCol
Expand Down
Loading
Loading