Skip to content

Commit b428d1c

Browse files
committed
Add SoftWrap to handle Lines and ShowLineNumbers behavior
This commit introduces a `SoftWrap` option that adjusts the behavior of the `Lines` option and `ShowLineNumbers` when used with the `Wrap` option. - `Lines`: Currently, when both `Lines` and `Wrap` options are used, the line count is computed after wrapping occurs. This can lead to discrepancies where wrapped lines count as multiple lines, causing the final output to exclude some original input lines. With the new `SoftWrap` option, wrapped lines will count as a single line, ensuring that all lines specified in the `Lines` option are correctly included. - `ShowLineNumbers`: When `SoftWrap` is enabled, line numbers are not added to wrapped lines. This preserves the original line numbering, ensuring consistency with the input file.
1 parent e505682 commit b428d1c

File tree

4 files changed

+118
-2
lines changed

4 files changed

+118
-2
lines changed

config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type Config struct {
3131
Language string `json:"language,omitempty" help:"Language of code file." short:"l" group:"Settings" placeholder:"go"`
3232
Theme string `json:"theme" help:"Theme to use for syntax highlighting." short:"t" group:"Settings" placeholder:"charm"`
3333
Wrap int `json:"wrap" help:"Wrap lines at a specific width." short:"w" group:"Settings" default:"0" placeholder:"80"`
34+
SoftWrap bool `json:"soft-wrap" help:"Do not count wrapped lines (Lines & LineHeight)." group:"Settings"`
3435

3536
Output string `json:"output,omitempty" help:"Output location for {{.svg}}, {{.png}}, or {{.webp}}." short:"o" group:"Settings" default:"" placeholder:"freeze.svg"`
3637
Execute string `json:"-" help:"Capture output of command execution." short:"x" group:"Settings" default:""`

main.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func main() {
177177
strippedInput = cut(strippedInput, config.Lines)
178178

179179
// wrap to character limit.
180-
if config.Wrap > 0 {
180+
if config.Wrap > 0 && !config.SoftWrap {
181181
strippedInput = wordwrap.String(strippedInput, config.Wrap)
182182
input = wordwrap.String(input, config.Wrap)
183183
}
@@ -195,6 +195,23 @@ func main() {
195195
}
196196
}
197197

198+
isRealLine := []bool{}
199+
strippedIsRealLine := []bool{}
200+
// wrap to character limit.
201+
if config.Wrap > 0 && config.SoftWrap {
202+
isRealLine = SoftWrap(input, config.Wrap)
203+
strippedIsRealLine = SoftWrap(strippedInput, config.Wrap)
204+
strippedInput = wordwrap.String(strippedInput, config.Wrap)
205+
input = wordwrap.String(input, config.Wrap)
206+
}
207+
208+
if config.Wrap <= 0 {
209+
// If Wrap is disabled, but SoftWrap enabled, we force disable SoftWrap as it does not make sense
210+
// to keep this option enabled.
211+
printError("Wrap option disabled, but SoftWrap option enabled, disabling SoftWrap option", nil)
212+
config.SoftWrap = false
213+
}
214+
198215
s, ok := styles.Registry[strings.ToLower(config.Theme)]
199216
if s == nil || !ok {
200217
s = charmStyle
@@ -320,6 +337,7 @@ func main() {
320337

321338
config.LineHeight *= float64(scale)
322339

340+
softWrapOffset := 0
323341
for i, line := range text {
324342
if isAnsi {
325343
line.SetText("")
@@ -330,9 +348,20 @@ func main() {
330348
ln := etree.NewElement("tspan")
331349
ln.CreateAttr("xml:space", "preserve")
332350
ln.CreateAttr("fill", s.Get(chroma.LineNumbers).Colour.String())
333-
ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine))
351+
if config.SoftWrap {
352+
if (isAnsi && strippedIsRealLine[i]) || (!isAnsi && isRealLine[i]) {
353+
ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine-softWrapOffset))
354+
} else {
355+
ln.SetText(" ")
356+
}
357+
} else {
358+
ln.SetText(fmt.Sprintf("%3d ", i+1+offsetLine))
359+
}
334360
line.InsertChildAt(0, ln)
335361
}
362+
if config.SoftWrap && !((isAnsi && strippedIsRealLine[i]) || (!isAnsi && isRealLine[i])) {
363+
softWrapOffset++
364+
}
336365
x := float64(config.Padding[left] + config.Margin[left])
337366
y := (float64(i+1))*(config.Font.Size*config.LineHeight) + float64(config.Padding[top]) + float64(config.Margin[top])
338367

soft_wrap.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"github.com/muesli/reflow/wordwrap"
5+
"strings"
6+
)
7+
8+
func SoftWrap(input string, wrapLength int) []bool {
9+
var wrap []bool
10+
for _, line := range strings.Split(input, "\n") {
11+
wrappedLine := wordwrap.String(line, wrapLength)
12+
13+
for i := range strings.Split(wrappedLine, "\n") {
14+
if i == 0 {
15+
// We want line number on the original line
16+
wrap = append(wrap, true)
17+
} else {
18+
// for wrapped line, we do not want line number
19+
wrap = append(wrap, false)
20+
}
21+
}
22+
}
23+
return wrap
24+
}

soft_wrap_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package main
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
// Mock the dependency if needed, assuming wordwrap.String works correctly.
9+
func TestSoftWrap(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
input string
13+
wrapLength int
14+
expected []bool
15+
}{
16+
{
17+
name: "Single short line, no wrapping",
18+
input: "Hello",
19+
wrapLength: 10,
20+
expected: []bool{true},
21+
},
22+
{
23+
name: "Single long line, wrapping",
24+
input: "Hello World, this is a long line",
25+
wrapLength: 10,
26+
expected: []bool{true, false, false, false},
27+
},
28+
{
29+
name: "Multiple lines, some wrapped",
30+
input: "Short\nThis is a long line",
31+
wrapLength: 10,
32+
expected: []bool{true, true, false},
33+
},
34+
{
35+
name: "Multiple lines, multiple wraps",
36+
input: "This is an long line\nThis is an other long line\nThis is the last long line\nShort line",
37+
wrapLength: 10,
38+
expected: []bool{true, false, true, false, false, true, false, false, true},
39+
},
40+
{
41+
name: "Empty input",
42+
input: "",
43+
wrapLength: 10,
44+
expected: []bool{true},
45+
},
46+
{
47+
name: "Lines with spaces only",
48+
input: " \n ",
49+
wrapLength: 5,
50+
expected: []bool{true, true},
51+
},
52+
}
53+
54+
for _, tt := range tests {
55+
t.Run(tt.name, func(t *testing.T) {
56+
got := SoftWrap(tt.input, tt.wrapLength)
57+
if !reflect.DeepEqual(got, tt.expected) {
58+
t.Errorf("SoftWrap() = %v, expected %v", got, tt.expected)
59+
}
60+
})
61+
}
62+
}

0 commit comments

Comments
 (0)