Merge branch 'worktree-agent-acb1218b465c60e42'
This commit is contained in:
121 files changed
+7031
-2
No files matched your search
+59
-1
@@ -1,6 +1,9 @@
|
||||
package internal
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// DiffOp says a line is kept, removed or added.
|
||||
type DiffOp uint8
|
||||
@@ -119,6 +122,61 @@ func myers(out []DiffLine, a, b []string, ao, bo int) []DiffLine {
|
||||
return out
|
||||
}
|
||||
|
||||
// WordSpan is a run of a line that a word diff kept, removed or added.
|
||||
type WordSpan struct {
|
||||
Op DiffOp
|
||||
Text string
|
||||
}
|
||||
|
||||
// WordDiff compares two lines by words: from holds kept and removed spans, to kept and added.
|
||||
func WordDiff(a, b string) (from, to []WordSpan) {
|
||||
add := func(spans []WordSpan, op DiffOp, s string) []WordSpan {
|
||||
if n := len(spans); n > 0 && spans[n-1].Op == op {
|
||||
spans[n-1].Text += s
|
||||
return spans
|
||||
}
|
||||
return append(spans, WordSpan{op, s})
|
||||
}
|
||||
for _, d := range LineDiff(wordTokens(a), wordTokens(b)) {
|
||||
switch d.Op {
|
||||
case DiffEqual:
|
||||
from, to = add(from, DiffEqual, d.Text), add(to, DiffEqual, d.Text)
|
||||
case DiffDelete:
|
||||
from = add(from, DiffDelete, d.Text)
|
||||
case DiffInsert:
|
||||
to = add(to, DiffInsert, d.Text)
|
||||
}
|
||||
}
|
||||
return from, to
|
||||
}
|
||||
|
||||
// wordTokens splits s into words, runs of spaces and single other characters.
|
||||
func wordTokens(s string) []string {
|
||||
var out []string
|
||||
kind := func(r rune) int {
|
||||
switch {
|
||||
case r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r):
|
||||
return 1
|
||||
case unicode.IsSpace(r):
|
||||
return 2
|
||||
}
|
||||
return 0
|
||||
}
|
||||
start, prev := 0, -1
|
||||
for i, r := range s {
|
||||
k := kind(r)
|
||||
if i > start && (k != prev || k == 0) {
|
||||
out = append(out, s[start:i])
|
||||
start = i
|
||||
}
|
||||
prev = k
|
||||
}
|
||||
if start < len(s) {
|
||||
out = append(out, s[start:])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DiffStats counts the added and removed lines of a diff.
|
||||
func DiffStats(d []DiffLine) (added, removed int) {
|
||||
for _, l := range d {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWordTokens(t *testing.T) {
|
||||
got := wordTokens("let x_1 = foo(bar); ok")
|
||||
want := []string{"let", " ", "x_1", " ", "=", " ", "foo", "(", "bar", ")", ";", " ", "ok"}
|
||||
if !slices.Equal(got, want) {
|
||||
t.Fatalf("tokens %q", got)
|
||||
}
|
||||
if wordTokens("") != nil {
|
||||
t.Fatal("empty line has tokens")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWordDiff(t *testing.T) {
|
||||
from, to := WordDiff("return a + b", "return a - total")
|
||||
wantFrom := []WordSpan{{DiffEqual, "return a "}, {DiffDelete, "+"}, {DiffEqual, " "}, {DiffDelete, "b"}}
|
||||
wantTo := []WordSpan{{DiffEqual, "return a "}, {DiffInsert, "-"}, {DiffEqual, " "}, {DiffInsert, "total"}}
|
||||
if !slices.Equal(from, wantFrom) || !slices.Equal(to, wantTo) {
|
||||
t.Fatalf("from %+v to %+v", from, to)
|
||||
}
|
||||
join := func(s []WordSpan) (out string) {
|
||||
for _, w := range s {
|
||||
out += w.Text
|
||||
}
|
||||
return
|
||||
}
|
||||
for _, p := range [][2]string{{"", "new text"}, {"old text", ""}, {"same", "same"}, {"中文 词语", "中文 句子"}} {
|
||||
f, to := WordDiff(p[0], p[1])
|
||||
if join(f) != p[0] || join(to) != p[1] {
|
||||
t.Errorf("%q→%q rebuilt %q→%q", p[0], p[1], join(f), join(to))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// CodeSpans colors a code line; inBlock carries block comments.
|
||||
func CodeSpans(k theme.Tokens, language, line string, inBlock bool) ([]ui.Span, bool) {
|
||||
toks, open := Highlight(language, line, inBlock)
|
||||
spans := make([]ui.Span, 0, len(toks))
|
||||
for _, t := range toks {
|
||||
s := ui.Span{Text: t.Text, Color: k.Text}
|
||||
switch t.Kind {
|
||||
case TokKeyword:
|
||||
s.Color, s.Weight = k.AccentText, 600
|
||||
case TokString:
|
||||
s.Color = k.Success
|
||||
case TokNumber:
|
||||
s.Color = k.Info
|
||||
case TokComment:
|
||||
s.Color, s.Italic = k.TextMuted, true
|
||||
case TokType:
|
||||
s.Color = k.Warning
|
||||
case TokPunct:
|
||||
s.Color = k.TextMuted
|
||||
}
|
||||
spans = append(spans, s)
|
||||
}
|
||||
return spans, open
|
||||
}
|
||||
|
||||
// ansiLight and ansiDark are 16 colors readable on Surface.
|
||||
var (
|
||||
ansiLight = [16]string{"#211C1D", "#A12635", "#326247", "#805714", "#355E80", "#7A3B78", "#2F6B6B", "#6A5B55",
|
||||
"#6A5B55", "#B0303F", "#3B7352", "#8E6417", "#3F6E94", "#8A4787", "#367A7A", "#211C1D"}
|
||||
ansiDark = [16]string{"#A89A95", "#F0A1AA", "#94C6A4", "#DFC080", "#9DBDDA", "#D9A6D6", "#8FCFCF", "#F2EBDD",
|
||||
"#BCAEAA", "#F5B7BE", "#ADD6B9", "#E9CF99", "#B5CDE4", "#E4BDE2", "#A9DCDC", "#FFFFFF"}
|
||||
ansiCube = [6]uint8{0, 95, 135, 175, 215, 255}
|
||||
)
|
||||
|
||||
// AnsiResolve resolves a color; ok is false for default.
|
||||
func AnsiResolve(dark bool, col AnsiColor) (ui.Color, bool) {
|
||||
switch col.Kind {
|
||||
case AnsiRGB:
|
||||
return ui.RGB(col.R, col.G, col.B), true
|
||||
case AnsiIndexed:
|
||||
i := col.Index
|
||||
switch {
|
||||
case i < 16 && dark:
|
||||
return ui.Hex(ansiDark[i]), true
|
||||
case i < 16:
|
||||
return ui.Hex(ansiLight[i]), true
|
||||
case i < 232:
|
||||
i -= 16
|
||||
return ui.RGB(ansiCube[i/36], ansiCube[i/6%6], ansiCube[i%6]), true
|
||||
default:
|
||||
g := 8 + 10*(i-232)
|
||||
return ui.RGB(g, g, g), true
|
||||
}
|
||||
}
|
||||
return ui.Color{}, false
|
||||
}
|
||||
|
||||
// StripAnsi drops SGR escapes from s, keeping its text.
|
||||
func StripAnsi(s string) string {
|
||||
spans, _ := ParseAnsi(s, AnsiStyle{})
|
||||
var b strings.Builder
|
||||
for _, sp := range spans {
|
||||
b.WriteString(sp.Text)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
func TestAnsiPaletteReadable(t *testing.T) {
|
||||
for _, k := range []theme.Tokens{theme.Light(), theme.Dark()} {
|
||||
for i := range 16 {
|
||||
col, ok := AnsiResolve(k.Dark, AnsiColor{Kind: AnsiIndexed, Index: uint8(i)})
|
||||
if !ok {
|
||||
t.Fatalf("index %d unresolved", i)
|
||||
}
|
||||
if r := theme.Contrast(col, k.Surface); r < 4.5 {
|
||||
t.Errorf("dark=%v color %d contrast %.2f", k.Dark, i, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
if c, _ := AnsiResolve(false, AnsiColor{Kind: AnsiIndexed, Index: 196}); c.R != 255 || c.G != 0 || c.B != 0 {
|
||||
t.Errorf("cube 196 = %v", c)
|
||||
}
|
||||
if c, _ := AnsiResolve(false, AnsiColor{Kind: AnsiIndexed, Index: 244}); c.R != 128 {
|
||||
t.Errorf("gray 244 = %v", c)
|
||||
}
|
||||
if _, ok := AnsiResolve(true, AnsiColor{}); ok {
|
||||
t.Error("default color resolved")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodeSpansColorKeywords(t *testing.T) {
|
||||
k := theme.Light()
|
||||
spans, _ := CodeSpans(k, "go", "func main() // hi", false)
|
||||
if spans[0].Text != "func" || spans[0].Color != k.AccentText {
|
||||
t.Fatalf("keyword span %+v", spans[0])
|
||||
}
|
||||
if last := spans[len(spans)-1]; last.Color != k.TextMuted || !last.Italic {
|
||||
t.Fatalf("comment span %+v", last)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user