1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
// Fenced code blocks: what an agent writes in Markdown, and the one place a
// scanner can be pointed at with confidence.
package acp
import (
"strings"
"codeberg.org/turbo-editors/turbo-core/syntax"
)
// Block is a run of an entry's text that is either prose or code.
//
// An agent answers in Markdown, so a fenced block is a real thing in what it
// sends and the one place a scanner can be pointed at with confidence.
type Block struct {
// Language is what the fence named, or LanguageNone for prose and for a
// fence that named nothing.
Language syntax.Language
// Code says this block came out of a fence.
Code bool
// Text is the block's own text, without the fence markers.
Text string
}
// SplitBlocks separates fenced code from the prose around it.
//
// Only the *fence* decides. A scanner is never guessed at from the shape of
// the text: a highlighter that is wrong is worse than one that is quiet, which
// is the rule every scanner in this library already follows.
//
// blocks := acp.SplitBlocks("here:\n```go\nx := 1\n```\n")
// blocks[1].Language // "go"
func SplitBlocks(text string) []Block {
var blocks []Block
var current []string
inFence, language := false, syntax.LanguageNone
flush := func(code bool, lang syntax.Language) {
if len(current) == 0 {
return
}
blocks = append(blocks, Block{Language: lang, Code: code, Text: strings.Join(current, "\n")})
current = nil
}
for _, line := range strings.Split(text, "\n") {
if fence, named := fenceOf(line); fence {
if inFence {
flush(true, language)
inFence, language = false, syntax.LanguageNone
continue
}
flush(false, syntax.LanguageNone)
inFence, language = true, named
continue
}
current = append(current, line)
}
// A fence nobody closed is still code: an agent cut off mid-answer leaves
// one, and drawing the rest as prose would change colour halfway down a
// function for no reason a reader can see.
flush(inFence, language)
return blocks
}
// fenceOf reports whether a line opens or closes a fence, and which language
// it named.
func fenceOf(line string) (bool, syntax.Language) {
trimmed := strings.TrimSpace(line)
if !strings.HasPrefix(trimmed, "```") {
return false, syntax.LanguageNone
}
name := strings.ToLower(strings.TrimSpace(strings.TrimPrefix(trimmed, "```")))
if name == "" {
return true, syntax.LanguageNone
}
return true, languageNamed(name)
}
// languageNamed returns the registered language a fence's tag means, or
// LanguageNone when no scanner claims it.
//
// The tag is matched against what the editor actually knows rather than
// against a list kept here, so an editor that registers Rust colours a ```rust
// block with no change to this file. The few aliases below are the ones a
// model reaches for by habit.
func languageNamed(name string) syntax.Language {
if alias, ok := fenceAliases[name]; ok {
name = alias
}
wanted := syntax.Language(name)
for _, known := range syntax.Registered() {
if known == wanted {
return known
}
}
return syntax.LanguageNone
}
// fenceAliases maps the tags models write onto the names languages register
// themselves under.
var fenceAliases = map[string]string{
"golang": "go",
"sh": "bash",
"shell": "bash",
"zsh": "bash",
"console": "bash",
"js": "javascript",
"mjs": "javascript",
"yml": "yaml",
"md": "markdown",
"docker": "dockerfile",
"containerfile": "dockerfile",
}
|