Skip to content

Commit 2c991e7

Browse files
committed
Go port: agentiloop CLI (flags, REPL, TUI, markdown, syntax highlighting, sessions, remembered launch, slash commands) with tests
1 parent db5e964 commit 2c991e7

13 files changed

Lines changed: 3656 additions & 8 deletions

File tree

‎cmd/agentiloop/highlight.go‎

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package main
2+
3+
// Syntax highlighting (chroma) → styled spans. Used for fenced code blocks in
4+
// markdown and for read_file previews in the TUI.
5+
6+
import (
7+
"path/filepath"
8+
"strings"
9+
"unicode"
10+
11+
"github.com/alecthomas/chroma/v2"
12+
"github.com/alecthomas/chroma/v2/lexers"
13+
"github.com/gdamore/tcell/v2"
14+
)
15+
16+
// Xcode Dark palette (same hex values as AgentColorSyntax's CodeBlockTheme).
17+
// Keywords are bold like the Agent app.
18+
const (
19+
xcPlain = 0xDFDFE0
20+
xcComment = 0x6C9C5A
21+
xcKeyword = 0xFF7AB2
22+
xcString = 0xFC6A5D
23+
xcNumber = 0xD9C97C
24+
xcType = 0xD0A8FF
25+
xcFunction = 0x67B7A4
26+
xcBuiltin = 0xB281EB
27+
xcPreproc = 0xFFA14F
28+
xcAttribute = 0xFD8F3F
29+
xcProperty = 0x4EB0CC
30+
)
31+
32+
// importWords are keywords that pull in other code; Xcode colors them like the preprocessor.
33+
var importWords = map[string]bool{"import": true, "@import": true, "#import": true, "#include": true, "using": true}
34+
35+
// selfWords are the language's "this object" keywords, colored like keywords.
36+
var selfWords = map[string]bool{"self": true, "Self": true, "this": true, "super": true}
37+
38+
// tokenStyle maps a chroma token (type + text) onto the palette. Chroma's token
39+
// classes are coarser than TextMate scopes, so a few words are special-cased to
40+
// match Xcode: imports are orange, self/this are keywords, capitalized builtins are types.
41+
func tokenStyle(t chroma.TokenType, value string) tcell.Style {
42+
c := func(h int32, bold bool) tcell.Style { return fg(rgb(h)).Bold(bold) }
43+
v := strings.TrimSpace(value)
44+
switch {
45+
case importWords[v] && (t.InCategory(chroma.Keyword) || t.InCategory(chroma.Comment)):
46+
return c(xcPreproc, true)
47+
case selfWords[v] && (t.InCategory(chroma.Keyword) || t.InCategory(chroma.Name)):
48+
return c(xcKeyword, true)
49+
case t == chroma.NameBuiltin && v != "" && unicode.IsUpper([]rune(v)[0]):
50+
return c(xcType, false)
51+
case t == chroma.CommentPreproc || t == chroma.CommentPreprocFile || t == chroma.KeywordNamespace:
52+
return c(xcPreproc, true)
53+
case t.InCategory(chroma.Comment):
54+
return c(xcComment, false)
55+
case t == chroma.KeywordType:
56+
return c(xcType, false)
57+
case t == chroma.KeywordConstant:
58+
return c(xcNumber, false)
59+
case t.InCategory(chroma.Keyword):
60+
return c(xcKeyword, true)
61+
case t.InCategory(chroma.LiteralString):
62+
return c(xcString, false)
63+
case t.InCategory(chroma.LiteralNumber) || t == chroma.NameConstant:
64+
return c(xcNumber, false)
65+
case t == chroma.NameBuiltinPseudo:
66+
return c(xcKeyword, true)
67+
case t == chroma.NameClass || t == chroma.NameNamespace || t == chroma.NameException:
68+
return c(xcType, false)
69+
case t == chroma.NameFunction || t == chroma.NameFunctionMagic:
70+
return c(xcFunction, false)
71+
case t == chroma.NameBuiltin:
72+
return c(xcBuiltin, false)
73+
case t == chroma.NameDecorator || t == chroma.NameAttribute:
74+
return c(xcAttribute, false)
75+
case t == chroma.NameTag || t == chroma.NameProperty:
76+
return c(xcProperty, false)
77+
}
78+
return c(xcPlain, false)
79+
}
80+
81+
// lexerFor resolves a fence language ("rust", "py", "bash") or a file extension.
82+
func lexerFor(hint string) chroma.Lexer {
83+
h := strings.TrimSpace(hint)
84+
if h == "" {
85+
return nil
86+
}
87+
if l := lexers.Get(h); l != nil {
88+
return l
89+
}
90+
return lexers.Match("x." + strings.TrimPrefix(h, "."))
91+
}
92+
93+
// highlight returns one span list per line, no line terminators. nil when the
94+
// language is unknown.
95+
func highlight(code, hint string) []Line {
96+
lexer := lexerFor(hint)
97+
if lexer == nil {
98+
return nil
99+
}
100+
lexer = chroma.Coalesce(lexer)
101+
code = strings.ReplaceAll(code, "\t", " ")
102+
it, err := lexer.Tokenise(nil, code)
103+
if err != nil {
104+
return nil
105+
}
106+
out := []Line{nil}
107+
for _, tok := range it.Tokens() {
108+
st := tokenStyle(tok.Type, tok.Value)
109+
parts := strings.Split(tok.Value, "\n")
110+
for i, p := range parts {
111+
if i > 0 {
112+
out = append(out, nil)
113+
}
114+
if p = strings.TrimSuffix(p, "\r"); p != "" {
115+
out[len(out)-1] = append(out[len(out)-1], styled(p, st))
116+
}
117+
}
118+
}
119+
// A trailing newline doesn't start another line.
120+
if strings.HasSuffix(code, "\n") && len(out) > 1 && len(out[len(out)-1]) == 0 {
121+
out = out[:len(out)-1]
122+
}
123+
return out
124+
}
125+
126+
// highlightNumbered highlights read_file-style lines (" 1│code") by path's
127+
// extension, keeping the gutter. nil if the extension is unknown or a line has no gutter.
128+
func highlightNumbered(lines []string, path string) []Line {
129+
ext := strings.TrimPrefix(filepath.Ext(path), ".")
130+
if ext == "" {
131+
return nil
132+
}
133+
gutters := make([]string, len(lines))
134+
bodies := make([]string, len(lines))
135+
for i, l := range lines {
136+
g, b, ok := strings.Cut(l, "│")
137+
if !ok {
138+
return nil
139+
}
140+
gutters[i], bodies[i] = g+"│", b
141+
}
142+
code := highlight(strings.Join(bodies, "\n"), ext)
143+
if code == nil {
144+
return nil
145+
}
146+
out := make([]Line, len(gutters))
147+
for i, g := range gutters {
148+
line := Line{styled(g, fg(tcell.ColorGray))}
149+
if i < len(code) {
150+
line = append(line, code[i]...)
151+
}
152+
out[i] = line
153+
}
154+
return out
155+
}

0 commit comments

Comments
 (0)