@ 261101.7
current
package main

import (
	"reflect"
	"strings"
	"testing"
)

func scopesOf(name, code string) []scope {
	return findScopes(splitLines(tokenise(codeLexer(name), code)), langName(codeLexer(name)) == "python")
}

// A block that spans lines is a scope, and brackets or a string on one
// line around something are a leaf; brackets inside a string or comment,
// or around nothing, are neither. A box reaches past each box inside it, and a call
// whose argument is a function draws one box.
func TestScopesGo(t *testing.T) {
	src := "func f() {\n" + // 0
		"\tif x { y() }\n" + // 1
		"\tfor {\n" + // 2
		"\t\ts := \"{\" // }\n" + // 3
		"\t\tgo(func() {\n" + // 4
		"\t\t}())\n" + // 5
		"\t}\n" + // 6
		"}\n" // 7
	got := scopesOf("a.go", src)
	want := []scope{
		{first: 0, last: 7, left: 0, right: 30, depth: 1},
		{first: 1, last: 1, left: 13, right: 20, depth: 2, leaf: 1},
		{first: 2, last: 6, left: 8, right: 29, depth: 2},
		{first: 3, last: 3, left: 21, right: 24, depth: 3, leaf: 1},
		{first: 4, last: 5, left: 16, right: 27, depth: 3},
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("scopes\n%+v, want\n%+v", got, want)
	}
}

// In Python a line ending with a colon opens the lines indented under it,
// blank lines included, up to the last of them.
func TestScopesPython(t *testing.T) {
	src := "class A:\n" + // 0
		"    def f(self):\n" + // 1
		"        x = {'a': 1}\n" + // 2
		"\n" + // 3
		"        return x\n" + // 4
		"y = 1\n" // 5
	var got []scope
	for _, s := range scopesOf("a.py", src) {
		if s.leaf == 0 {
			got = append(got, s)
		}
	}
	if len(got) != 2 || got[0].first != 0 || got[0].last != 4 || got[1].first != 1 || got[1].last != 4 || got[1].left != 4 {
		t.Errorf("scopes %+v, want class 0-4 and def 1-4 at 4", got)
	}
}

// Leaves nest within a line, each a level below the one around it, and
// sit below the blocks around their row.
func TestScopeLeaves(t *testing.T) {
	got := scopesOf("a.go", "x := f(g(a), [2]int{1, 2})\n")
	want := []scope{
		{first: 0, last: 0, left: 6, right: 26, depth: 1, leaf: 1},
		{first: 0, last: 0, left: 8, right: 11, depth: 2, leaf: 2},
		{first: 0, last: 0, left: 13, right: 16, depth: 2, leaf: 2},
		{first: 0, last: 0, left: 19, right: 25, depth: 2, leaf: 2},
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("leaves\n%+v, want\n%+v", got, want)
	}
}

// A string is a leaf with its quotes and escapes, inside the brackets
// around it; an empty one is not, and one that spans lines is a block.
func TestScopeStrings(t *testing.T) {
	got := scopesOf("a.go", "f(\"a\\n\", \"\")\nx := `one\ntwo`\n")
	want := []scope{
		{first: 0, last: 0, left: 1, right: 12, depth: 1, leaf: 1},
		{first: 0, last: 0, left: 2, right: 7, depth: 2, leaf: 2},
		{first: 1, last: 2, left: 0, right: 9, depth: 1},
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("strings\n%+v, want\n%+v", got, want)
	}
}

// A box starts at the least indent among its rows, so a raw string's
// lines at the margin stay inside it and the boxes around it.
func TestScopeLeftmostRow(t *testing.T) {
	src := "func f() {\n" + // 0
		"\tx := `a\n" + // 1
		"b`\n" + // 2
		"}\n" // 3
	got := scopesOf("a.go", src)
	want := []scope{
		{first: 0, last: 3, left: 0, right: 16, depth: 1},
		{first: 1, last: 2, left: 0, right: 15, depth: 2},
	}
	if !reflect.DeepEqual(got, want) {
		t.Errorf("scopes\n%+v, want\n%+v", got, want)
	}
}

// A code page draws each scope as an empty box in its first line, after
// the line number; a flat page and fences draw none.
func TestScopeBoxes(t *testing.T) {
	src := "func f() {\n\treturn\n}\n"
	var b strings.Builder
	highlight(&b, codeLexer("a.go"), src, true, true)
	out := b.String()
	if !strings.Contains(out, `<pre class="sourceCode numberSource go numberLines scopes">`) ||
		!strings.Contains(out, `<span id="L1"><a href="#L1"></a><i class="scope shade" style="--c:0;--w:14;--r:3;--d:1"></i><span class="kw">func</span>`) {
		t.Errorf("no box in the first line: %s", out)
	}
	b.Reset()
	highlight(&b, codeLexer("a.go"), src, true, false)
	if strings.Contains(b.String(), "scope") {
		t.Errorf("flat page has scopes: %s", b.String())
	}
}