Skip to content

Commit f82ee62

Browse files
committed
refactor(tui): restructure code highlighting with better separation of concerns
- Extract `parseRange` function to handle range parsing logic - Split `renderWithStyle` into `renderHighlightedCode` and `renderPlainCode` functions - Add `formatLineNumber` and `getLineNumberWidth` helper functions for line number formatting - Extract `parseCodeBlockInfo` and `renderMarkdownSection` functions from main processing loop - Improve error handling with fallback to plain rendering when syntax highlighting fails - Update tests to reflect new function names and signatures - Simplify regex pattern construction in `processMarkdownWithHighlighting`
1 parent 45a25e1 commit f82ee62

File tree

2 files changed

+187
-137
lines changed

2 files changed

+187
-137
lines changed

internal/tui/highlight.go

Lines changed: 172 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,17 @@ func parseHighlightSyntax(syntax string) []LineRange {
3333
}
3434

3535
syntax = strings.Trim(syntax, "{}")
36-
37-
var ranges []LineRange
3836
parts := strings.Split(syntax, ",")
37+
var ranges []LineRange
3938

4039
for _, part := range parts {
4140
part = strings.TrimSpace(part)
4241
if strings.Contains(part, "-") {
43-
// Range like "1-3"
44-
rangeParts := strings.Split(part, "-")
45-
if len(rangeParts) == 2 {
46-
start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
47-
end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
48-
if err1 == nil && err2 == nil && start <= end {
49-
ranges = append(ranges, LineRange{Start: start, End: end})
50-
}
42+
if r := parseRange(part); r != nil {
43+
ranges = append(ranges, *r)
5144
}
5245
} else {
53-
// Single line like "5"
54-
line, err := strconv.Atoi(part)
55-
if err == nil {
46+
if line, err := strconv.Atoi(part); err == nil {
5647
ranges = append(ranges, LineRange{Start: line, End: line})
5748
}
5849
}
@@ -61,7 +52,26 @@ func parseHighlightSyntax(syntax string) []LineRange {
6152
return ranges
6253
}
6354

55+
func parseRange(part string) *LineRange {
56+
rangeParts := strings.Split(part, "-")
57+
if len(rangeParts) != 2 {
58+
return nil
59+
}
60+
61+
start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
62+
end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
63+
64+
if err1 == nil && err2 == nil && start <= end {
65+
return &LineRange{Start: start, End: end}
66+
}
67+
return nil
68+
}
69+
6470
func shouldHighlightLine(lineNum int, ranges []LineRange) bool {
71+
if len(ranges) == 0 {
72+
return true // highlight all lines if no ranges specified
73+
}
74+
6575
for _, r := range ranges {
6676
if lineNum >= r.Start && lineNum <= r.End {
6777
return true
@@ -70,102 +80,185 @@ func shouldHighlightLine(lineNum int, ranges []LineRange) bool {
7080
return false
7181
}
7282

73-
func renderCustomCodeBlock(content string, info CodeHighlightInfo, themeName string) string {
74-
lexer := lexers.Get(info.Language)
75-
if lexer == nil {
76-
lexer = lexers.Fallback
83+
func formatLineNumber(lineNum, width int) string {
84+
style := lipgloss.NewStyle().
85+
Foreground(lipgloss.Color("240")).
86+
PaddingRight(1)
87+
88+
lineNumStr := strconv.Itoa(lineNum)
89+
paddedLineNum := fmt.Sprintf("%*s", width-1, lineNumStr)
90+
return style.Render(paddedLineNum)
91+
}
92+
93+
func getLineNumberWidth(startLine, totalLines int) int {
94+
if totalLines == 0 {
95+
return 0
7796
}
78-
lexer = chroma.Coalesce(lexer)
97+
maxLineNum := startLine + totalLines - 1
98+
return len(strconv.Itoa(maxLineNum)) + 2
99+
}
79100

80-
style := config.GetChromaStyle(themeName)
81-
return renderWithStyle(content, info, lexer, style)
101+
func renderPlainCode(lines []string, info CodeHighlightInfo) string {
102+
var result strings.Builder
103+
lineNumberWidth := 0
104+
105+
if info.ShowLineNumbers {
106+
lineNumberWidth = getLineNumberWidth(info.StartLine, len(lines))
107+
}
108+
109+
for i, line := range lines {
110+
displayLineNum := info.StartLine + i
111+
112+
if info.ShowLineNumbers {
113+
result.WriteString(formatLineNumber(displayLineNum, lineNumberWidth))
114+
}
115+
116+
result.WriteString(line)
117+
118+
if i < len(lines)-1 {
119+
result.WriteString("\n")
120+
}
121+
}
122+
123+
return result.String()
82124
}
83125

84-
func renderWithStyle(content string, info CodeHighlightInfo, lexer chroma.Lexer, style *chroma.Style) string {
126+
func renderHighlightedCode(content string, lines []string, info CodeHighlightInfo, lexer chroma.Lexer, style *chroma.Style) string {
85127
formatter := formatters.Get("terminal256")
86128
if formatter == nil {
87-
formatter = formatters.Fallback
129+
return renderPlainCode(lines, info)
88130
}
89131

90-
lines := strings.Split(content, "\n")
132+
iterator, err := lexer.Tokenise(nil, content)
133+
if err != nil {
134+
return renderPlainCode(lines, info)
135+
}
136+
137+
var formattedBuf strings.Builder
138+
if err := formatter.Format(&formattedBuf, style, iterator); err != nil {
139+
return renderPlainCode(lines, info)
140+
}
91141

92-
highlightStyle := lipgloss.NewStyle()
142+
formattedLines := strings.Split(formattedBuf.String(), "\n")
93143

144+
// Ensure we have the same number of lines
145+
for len(formattedLines) < len(lines) {
146+
formattedLines = append(formattedLines, "")
147+
}
148+
if len(formattedLines) > len(lines) {
149+
formattedLines = formattedLines[:len(lines)]
150+
}
151+
152+
var result strings.Builder
94153
lineNumberWidth := 0
154+
95155
if info.ShowLineNumbers {
96-
maxLineNum := info.StartLine + len(lines) - 1
97-
lineNumberWidth = len(strconv.Itoa(maxLineNum)) + 2
156+
lineNumberWidth = getLineNumberWidth(info.StartLine, len(lines))
98157
}
99158

100-
lineNumberStyle := lipgloss.NewStyle().
101-
Foreground(lipgloss.Color("240")).
102-
PaddingRight(1)
159+
for i, line := range lines {
160+
displayLineNum := info.StartLine + i
161+
relativeLineNum := i + 1
103162

104-
var result strings.Builder
105-
for lineNum, line := range lines {
106-
displayLineNum := info.StartLine + lineNum
107-
relativeLineNum := lineNum + 1
108-
109-
// Add line number if requested
110163
if info.ShowLineNumbers {
111-
lineNumStr := strconv.Itoa(displayLineNum)
112-
paddedLineNum := fmt.Sprintf("%*s", lineNumberWidth-1, lineNumStr)
113-
result.WriteString(lineNumberStyle.Render(paddedLineNum))
164+
result.WriteString(formatLineNumber(displayLineNum, lineNumberWidth))
114165
}
115166

116-
shouldHighlight := len(info.Ranges) == 0 || shouldHighlightLine(relativeLineNum, info.Ranges)
117-
118-
if !shouldHighlight {
119-
result.WriteString(line)
120-
if lineNum < len(lines)-1 {
121-
result.WriteString("\n")
167+
if shouldHighlightLine(relativeLineNum, info.Ranges) {
168+
formattedLine := ""
169+
if i < len(formattedLines) {
170+
formattedLine = strings.TrimRight(formattedLines[i], " \t\n\r")
122171
}
123-
continue
124-
}
125-
126-
lineIterator, err := lexer.Tokenise(nil, line)
127-
if err != nil {
128-
result.WriteString(highlightStyle.Render(line))
129-
if lineNum < len(lines)-1 {
130-
result.WriteString("\n")
172+
if formattedLine == "" {
173+
formattedLine = line
131174
}
132-
continue
175+
result.WriteString(formattedLine)
176+
} else {
177+
result.WriteString(line)
133178
}
134179

135-
var lineBuf strings.Builder
136-
err = formatter.Format(&lineBuf, style, lineIterator)
137-
if err != nil {
138-
result.WriteString(highlightStyle.Render(line))
139-
if lineNum < len(lines)-1 {
140-
result.WriteString("\n")
141-
}
142-
continue
180+
if i < len(lines)-1 {
181+
result.WriteString("\n")
143182
}
183+
}
144184

145-
syntaxHighlighted := lineBuf.String()
146-
syntaxHighlighted = strings.TrimRight(syntaxHighlighted, " \t\n\r")
147-
result.WriteString(highlightStyle.Render(syntaxHighlighted))
185+
return result.String()
186+
}
148187

149-
if lineNum < len(lines)-1 {
150-
result.WriteString("\n")
188+
func renderCustomCodeBlock(content string, info CodeHighlightInfo, themeName string) string {
189+
lines := strings.Split(content, "\n")
190+
191+
var renderedContent string
192+
193+
if info.Language != "" {
194+
lexer := lexers.Get(info.Language)
195+
if lexer == nil {
196+
lexer = lexers.Fallback
151197
}
198+
lexer = chroma.Coalesce(lexer)
199+
style := config.GetChromaStyle(themeName)
200+
201+
renderedContent = renderHighlightedCode(content, lines, info, lexer, style)
202+
} else {
203+
renderedContent = renderPlainCode(lines, info)
152204
}
153205

206+
// Apply consistent styling
154207
codeStyle := lipgloss.NewStyle().
155208
Padding(1, 2).
156209
Width(78).
157210
MarginTop(1).
158211
MarginBottom(1)
159212

160-
return codeStyle.Render(result.String())
213+
return codeStyle.Render(renderedContent)
214+
}
215+
216+
func parseCodeBlockInfo(match []string) CodeHighlightInfo {
217+
if len(match) < 7 {
218+
return CodeHighlightInfo{}
219+
}
220+
221+
language := match[1]
222+
highlightSyntax := ""
223+
if len(match) > 2 && match[2] != "" {
224+
highlightSyntax = match[2]
225+
}
226+
numberedFlag := strings.TrimSpace(match[3])
227+
startLineStr := match[5]
228+
229+
startLine := 1
230+
if startLineStr != "" {
231+
if parsed, err := strconv.Atoi(startLineStr); err == nil && parsed > 0 {
232+
startLine = parsed
233+
}
234+
}
235+
236+
return CodeHighlightInfo{
237+
Language: language,
238+
Ranges: parseHighlightSyntax(highlightSyntax),
239+
ShowLineNumbers: strings.Contains(numberedFlag, "--numbered"),
240+
StartLine: startLine,
241+
}
242+
}
243+
244+
func renderMarkdownSection(text, themeName string) string {
245+
if strings.TrimSpace(text) == "" {
246+
return ""
247+
}
248+
249+
rendered, err := glamour.Render(text, themeName)
250+
if err != nil {
251+
return text
252+
}
253+
return rendered
161254
}
162255

163256
func processMarkdownWithHighlighting(markdown string, themeName string) (string, error) {
164-
re := regexp.MustCompile("(?s)```([a-zA-Z0-9_+-]*)({[^}]*})?(\\s+--numbered)?(\\s+--start-at-line\\s+(\\d+))?\\s*\n(.*?)\n```")
257+
// Regex to match code blocks with optional highlighting syntax
258+
re := regexp.MustCompile(`(?s)` + "`" + `{3}([a-zA-Z0-9_+-]*)({[^}]*})?(\s+--numbered)?(\s+--start-at-line\s+(\d+))?\s*\n(.*?)\n` + "`" + `{3}`)
165259

166260
matches := re.FindAllStringSubmatch(markdown, -1)
167261
if len(matches) == 0 {
168-
// No code blocks found, render the entire markdown with Glamour
169262
return glamour.Render(markdown, themeName)
170263
}
171264

@@ -174,80 +267,35 @@ func processMarkdownWithHighlighting(markdown string, themeName string) (string,
174267
indices := re.FindAllStringIndex(markdown, -1)
175268

176269
for i, match := range matches {
177-
if len(match) < 7 {
178-
continue
179-
}
180-
181270
matchStart := indices[i][0]
182271
matchEnd := indices[i][1]
183272

184-
// Add the text before this code block
273+
// Add text before this code block
185274
if matchStart > lastIndex {
186275
beforeText := markdown[lastIndex:matchStart]
187-
if strings.TrimSpace(beforeText) != "" {
188-
// Render regular markdown content with Glamour
189-
rendered, err := glamour.Render(beforeText, themeName)
190-
if err != nil {
191-
parts = append(parts, beforeText)
192-
} else {
193-
parts = append(parts, rendered)
194-
}
195-
}
276+
parts = append(parts, renderMarkdownSection(beforeText, themeName))
196277
}
197278

198-
// Extract code block info
199-
language := match[1] // e.g., "typescript"
200-
highlightSyntax := ""
201-
if len(match) > 2 && match[2] != "" {
202-
highlightSyntax = match[2] // e.g., "{1-2}"
203-
}
204-
numberedFlag := strings.TrimSpace(match[3]) // " --numbered"
205-
startLineStr := match[5] // The line number from --start-at-line
206-
content := match[6] // The actual code content
207-
208-
startLine := 1
209-
if startLineStr != "" {
210-
if parsed, err := strconv.Atoi(startLineStr); err == nil && parsed > 0 {
211-
startLine = parsed
212-
}
213-
}
279+
// Process the code block
280+
info := parseCodeBlockInfo(match)
281+
content := match[6]
214282

215-
info := CodeHighlightInfo{
216-
Language: language,
217-
Ranges: parseHighlightSyntax(highlightSyntax),
218-
ShowLineNumbers: strings.Contains(numberedFlag, "--numbered"),
219-
StartLine: startLine,
220-
}
221-
222-
if language != "" || info.ShowLineNumbers {
283+
if info.Language != "" || info.ShowLineNumbers {
223284
customRendered := renderCustomCodeBlock(content, info, themeName)
224285
parts = append(parts, customRendered)
225286
} else {
226-
// No language specified and no line numbers, use Glamour directly for the entire code block
287+
// Use Glamour for plain code blocks
227288
codeBlock := match[0]
228-
rendered, err := glamour.Render(codeBlock, themeName)
229-
if err != nil {
230-
parts = append(parts, codeBlock)
231-
} else {
232-
parts = append(parts, rendered)
233-
}
289+
parts = append(parts, renderMarkdownSection(codeBlock, themeName))
234290
}
235291

236292
lastIndex = matchEnd
237293
}
238294

239-
// Add any remaining text after the last code block
295+
// Add remaining text after the last code block
240296
if lastIndex < len(markdown) {
241297
remainingText := markdown[lastIndex:]
242-
if strings.TrimSpace(remainingText) != "" {
243-
// Render remaining markdown content with Glamour
244-
rendered, err := glamour.Render(remainingText, themeName)
245-
if err != nil {
246-
parts = append(parts, remainingText)
247-
} else {
248-
parts = append(parts, rendered)
249-
}
250-
}
298+
parts = append(parts, renderMarkdownSection(remainingText, themeName))
251299
}
252300

253301
return strings.Join(parts, ""), nil

0 commit comments

Comments
 (0)