package editor import ( "strings" "testing" "github.com/gdamore/tcell/v2" "codeberg.org/turbo-editors/turbo-core/theme" ) // markAt returns the character in the gutter's separator column on a row, and // the style it is drawn in. func markAt(t *testing.T, view *View, screen tcell.SimulationScreen, row int) (rune, tcell.Style) { t.Helper() cells, width, _ := screen.GetContents() cell := cells[row*width+view.gutterWidth()-1] if len(cell.Runes) == 0 { return ' ', cell.Style } return cell.Runes[0], cell.Style } func TestAMarkIsDrawnBesideItsLine(t *testing.T) { view, screen, p := newTestView(t, "one\ntwo\nthree\n", 40, 8) view.SetMarks(map[int]Severity{1: MarkError}) draw(t, view, screen, p) if got, _ := markAt(t, view, screen, 1); got != '\u00d7' { t.Errorf("the gutter shows %q beside line 2, want the error mark", got) } if got, _ := markAt(t, view, screen, 0); got != ' ' { t.Errorf("line 1 shows %q and has nothing wrong with it", got) } } func TestAMarkDoesNotMoveTheText(t *testing.T) { // The whole reason the separator column is used: the text's left edge, the // cursor's screen column and the column a click lands on must all stay put. view, screen, p := newTestView(t, "package main\n", 40, 8) before := view.gutterWidth() view.SetMarks(map[int]Severity{0: MarkError}) lines := draw(t, view, screen, p) if got := view.gutterWidth(); got != before { t.Errorf("the gutter went from %d to %d columns", before, got) } if got := []rune(lines[0])[before]; got != 'p' { t.Errorf("the text starts with %q at column %d, want it where it was", got, before) } } func TestEachSeverityHasItsOwnGlyphAndColour(t *testing.T) { // The three theme keys exist in every shipped theme and were drawn nowhere // until now. A severity painted in the wrong one is invisible as a bug. th, err := theme.Load("turbo-classic", "") if err != nil { t.Fatalf("Load() error = %v", err) } seen := map[rune]Severity{} for _, severity := range []Severity{MarkError, MarkWarning, MarkInformation, MarkHint} { glyph, hasGlyph := markGlyphs[severity] key, hasStyle := markStyles[severity] if !hasGlyph || !hasStyle { t.Fatalf("severity %v has glyph=%v style=%v", severity, hasGlyph, hasStyle) } if other, clash := seen[glyph]; clash && severity != MarkHint { t.Errorf("severities %v and %v draw the same glyph %q", other, severity, glyph) } seen[glyph] = severity if fg, _, _ := th.Style(key).Decompose(); fg == 0 { t.Errorf("severity %v resolves to no colour through %q", severity, key) } } if markStyles[MarkError] == markStyles[MarkWarning] { t.Error("an error and a warning are drawn in the same colour") } } func TestAMarkIsDrawnInItsSeveritysColour(t *testing.T) { view, screen, p := newTestView(t, "one\ntwo\n", 40, 8) view.SetMarks(map[int]Severity{0: MarkError, 1: MarkWarning}) draw(t, view, screen, p) _, errorStyle := markAt(t, view, screen, 0) _, warningStyle := markAt(t, view, screen, 1) if errorStyle == warningStyle { t.Error("the error and the warning were drawn in the same style") } th := testTheme(t) wantFg, _, _ := th.Style(theme.KeyDiagnosticError).Decompose() if gotFg, _, _ := errorStyle.Decompose(); gotFg != wantFg { t.Errorf("the error mark is %v, want the theme's diagnostic.error %v", gotFg, wantFg) } } func TestHidingTheLineNumbersHidesTheMarks(t *testing.T) { // The consequence of using the separator column, stated where somebody // changing it will read it: there is no separator column without numbers. view, screen, p := newTestView(t, "one\n", 40, 8) view.SetMarks(map[int]Severity{0: MarkError}) view.SetLineNumbers(false) lines := draw(t, view, screen, p) if strings.ContainsRune(strings.Join(lines, ""), '\u00d7') { t.Errorf("a mark was drawn with the gutter hidden:\n%s", strings.Join(lines, "\n")) } } func TestClearingTheMarksClearsTheGutter(t *testing.T) { view, screen, p := newTestView(t, "one\n", 40, 8) view.SetMarks(map[int]Severity{0: MarkError}) draw(t, view, screen, p) view.SetMarks(nil) draw(t, view, screen, p) if got, _ := markAt(t, view, screen, 0); got != ' ' { t.Errorf("the gutter still shows %q after the marks were cleared", got) } } func TestAZeroSeverityIsNotAMark(t *testing.T) { // MarkNone is the zero value, so a map built by hand can hold one without // meaning to say anything. view, screen, p := newTestView(t, "one\n", 40, 8) view.SetMarks(map[int]Severity{0: MarkNone}) // Asserted on markOf rather than on the screen: a severity with no glyph // draws NUL, which a simulated screen shows as a blank — so the drawing // looks right whether or not the guard is there. if severity, marked := view.markOf(0); marked { t.Errorf("markOf(0) = %v, true; a line marked with nothing has no mark", severity) } draw(t, view, screen, p) if got, _ := markAt(t, view, screen, 0); got != ' ' { t.Errorf("the gutter shows %q for a line marked with nothing", got) } }