Skip to content

Commit 420d050

Browse files
committed
feat(tui): add support for custom line number starting position
- Add `StartLine` field to `CodeHighlightInfo` struct - Support `--start-at-line` flag in markdown code blocks - Update line number calculation to use custom starting position - Fix highlight range logic to work with relative line numbers - Add tests for custom line number offset functionality
1 parent e76db08 commit 420d050

File tree

2 files changed

+61
-9
lines changed

2 files changed

+61
-9
lines changed

internal/tui/highlight.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type CodeHighlightInfo struct {
2424
Language string
2525
Ranges []LineRange
2626
ShowLineNumbers bool
27+
StartLine int
2728
}
2829

2930
func parseHighlightSyntax(syntax string) []LineRange {
@@ -92,7 +93,8 @@ func renderWithStyle(content string, info CodeHighlightInfo, lexer chroma.Lexer,
9293

9394
lineNumberWidth := 0
9495
if info.ShowLineNumbers {
95-
lineNumberWidth = len(strconv.Itoa(len(lines))) + 2
96+
maxLineNum := info.StartLine + len(lines) - 1
97+
lineNumberWidth = len(strconv.Itoa(maxLineNum)) + 2
9698
}
9799

98100
lineNumberStyle := lipgloss.NewStyle().
@@ -101,19 +103,19 @@ func renderWithStyle(content string, info CodeHighlightInfo, lexer chroma.Lexer,
101103

102104
var result strings.Builder
103105
for lineNum, line := range lines {
104-
lineNumDisplay := lineNum + 1
106+
displayLineNum := info.StartLine + lineNum
107+
relativeLineNum := lineNum + 1
105108

106109
// Add line number if requested
107110
if info.ShowLineNumbers {
108-
lineNumStr := strconv.Itoa(lineNumDisplay)
111+
lineNumStr := strconv.Itoa(displayLineNum)
109112
paddedLineNum := fmt.Sprintf("%*s", lineNumberWidth-1, lineNumStr)
110113
result.WriteString(lineNumberStyle.Render(paddedLineNum))
111114
}
112115

113-
shouldHighlight := len(info.Ranges) == 0 || shouldHighlightLine(lineNumDisplay, info.Ranges)
116+
shouldHighlight := len(info.Ranges) == 0 || shouldHighlightLine(relativeLineNum, info.Ranges)
114117

115118
if !shouldHighlight {
116-
// No syntax highlighting for this line
117119
result.WriteString(line)
118120
if lineNum < len(lines)-1 {
119121
result.WriteString("\n")
@@ -159,7 +161,7 @@ func renderWithStyle(content string, info CodeHighlightInfo, lexer chroma.Lexer,
159161
}
160162

161163
func processMarkdownWithHighlighting(markdown string, themeName string) (string, error) {
162-
re := regexp.MustCompile("(?s)```([a-zA-Z0-9_+-]*)({[^}]*})?(\\s+--numbered)?\\s*\n(.*?)\n```")
164+
re := regexp.MustCompile("(?s)```([a-zA-Z0-9_+-]*)({[^}]*})?(\\s+--numbered)?(\\s+--start-at-line\\s+(\\d+))?\\s*\n(.*?)\n```")
163165

164166
matches := re.FindAllStringSubmatch(markdown, -1)
165167
if len(matches) == 0 {
@@ -172,7 +174,7 @@ func processMarkdownWithHighlighting(markdown string, themeName string) (string,
172174
indices := re.FindAllStringIndex(markdown, -1)
173175

174176
for i, match := range matches {
175-
if len(match) < 5 {
177+
if len(match) < 7 {
176178
continue
177179
}
178180

@@ -200,12 +202,21 @@ func processMarkdownWithHighlighting(markdown string, themeName string) (string,
200202
highlightSyntax = match[2] // e.g., "{1-2}"
201203
}
202204
numberedFlag := strings.TrimSpace(match[3]) // " --numbered"
203-
content := match[4] // The actual code content
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+
}
204214

205215
info := CodeHighlightInfo{
206216
Language: language,
207217
Ranges: parseHighlightSyntax(highlightSyntax),
208218
ShowLineNumbers: strings.Contains(numberedFlag, "--numbered"),
219+
StartLine: startLine,
209220
}
210221

211222
if language != "" || info.ShowLineNumbers {

internal/tui/highlight_test.go

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tui
22

33
import (
4+
"strconv"
45
"strings"
56
"testing"
67

@@ -186,6 +187,17 @@ func TestRenderCustomCodeBlock(t *testing.T) {
186187
},
187188
themeName: "dark",
188189
},
190+
{
191+
name: "code with start line offset",
192+
content: "console.log('hello');\nconsole.log('world');\nvar x = 1;",
193+
info: CodeHighlightInfo{
194+
Language: "javascript",
195+
Ranges: []LineRange{{Start: 2, End: 2}}, // Should highlight the second line
196+
ShowLineNumbers: true,
197+
StartLine: 10, // Line numbers should start at 10
198+
},
199+
themeName: "dark",
200+
},
189201
}
190202

191203
for _, tt := range tests {
@@ -204,9 +216,15 @@ func TestRenderCustomCodeBlock(t *testing.T) {
204216

205217
// Check for line numbers if requested
206218
if tt.info.ShowLineNumbers {
207-
if !strings.Contains(result, "1") {
219+
if !strings.Contains(result, "1") && tt.info.StartLine == 1 {
208220
t.Error("renderCustomCodeBlock with ShowLineNumbers should contain line number 1")
209221
}
222+
if tt.info.StartLine > 1 {
223+
expectedFirstLine := strconv.Itoa(tt.info.StartLine)
224+
if !strings.Contains(result, expectedFirstLine) {
225+
t.Errorf("renderCustomCodeBlock with StartLine %d should contain line number %s", tt.info.StartLine, expectedFirstLine)
226+
}
227+
}
210228
}
211229

212230
// For known languages, check that it contains some recognizable elements
@@ -358,6 +376,24 @@ func TestProcessMarkdownWithHighlighting(t *testing.T) {
358376
theme: "dark",
359377
wantErr: false,
360378
},
379+
{
380+
name: "markdown with --start-at-line flag",
381+
markdown: "```javascript --numbered --start-at-line 10\nconsole.log('hello');\nconsole.log('world');\n```",
382+
theme: "dark",
383+
wantErr: false,
384+
},
385+
{
386+
name: "markdown with highlighting and --start-at-line",
387+
markdown: "```python{1-2} --numbered --start-at-line 5\ndef hello():\n print('hello')\n return True\n```",
388+
theme: "dark",
389+
wantErr: false,
390+
},
391+
{
392+
name: "markdown with --start-at-line only (no --numbered)",
393+
markdown: "```typescript --start-at-line 100\nconst x = 1;\nconst y = 2;\n```",
394+
theme: "dark",
395+
wantErr: false,
396+
},
361397
}
362398

363399
for _, tt := range tests {
@@ -396,6 +432,7 @@ func TestCodeHighlightInfo(t *testing.T) {
396432
Language: "javascript",
397433
Ranges: []LineRange{{Start: 1, End: 3}, {Start: 5, End: 5}},
398434
ShowLineNumbers: true,
435+
StartLine: 10,
399436
}
400437

401438
if info.Language != "javascript" {
@@ -414,6 +451,10 @@ func TestCodeHighlightInfo(t *testing.T) {
414451
if !info.ShowLineNumbers {
415452
t.Error("Expected ShowLineNumbers to be true")
416453
}
454+
455+
if info.StartLine != 10 {
456+
t.Errorf("Expected StartLine to be 10, got %d", info.StartLine)
457+
}
417458
}
418459

419460
func TestLineRange(t *testing.T) {

0 commit comments

Comments
 (0)