Merge branch 'worktree-agent-acb1218b465c60e42'

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 14:38:04 -04:00
commit 4405a5911e
121 files changed
+7031 -2

No files matched your search

+59 -1
View File
@@ -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 {
+38
View File
@@ -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))
}
}
}
+76
View File
@@ -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()
}
+41
View File
@@ -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)
}
}