on main help sign in
Merge branch 'main' into spinner-improvements
e036088Raphael Amorim
cmd/root.go+1
@@ -256,6 +256,7 @@ func setupSubscriptions(app *app.App, parentCtx context.Context) (chan tea.Msg,
256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
256 setupSubscriber(ctx, &wg, "messages", app.Messages.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
257 setupSubscriber(ctx, &wg, "permissions", app.Permissions.Subscribe, ch)
258 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
258 setupSubscriber(ctx, &wg, "coderAgent", app.CoderAgent.Subscribe, ch)
259+ setupSubscriber(ctx, &wg, "history", app.History.Subscribe, ch)
259
260
260 cleanupFunc := func() {
261 cleanupFunc := func() {
261 logging.Info("Cancelling all subscriptions")
262 logging.Info("Cancelling all subscriptions")
cspell.json+1−1
@@ -1 +1 @@
1−{"flagWords":[],"language":"en","words":["crush","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos"],"version":"0.2"}
1+{"version":"0.2","language":"en","flagWords":[],"words":["crush","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos","fsext"]}
go.mod+3−2
@@ -9,7 +9,7 @@ require (
9 github.com/PuerkitoBio/goquery v1.9.2
9 github.com/PuerkitoBio/goquery v1.9.2
10 github.com/alecthomas/chroma/v2 v2.15.0
10 github.com/alecthomas/chroma/v2 v2.15.0
11 github.com/anthropics/anthropic-sdk-go v1.4.0
11 github.com/anthropics/anthropic-sdk-go v1.4.0
12− github.com/aymanbagabas/go-udiff v0.2.0
12+ github.com/aymanbagabas/go-udiff v0.3.1
13 github.com/bmatcuk/doublestar/v4 v4.8.1
13 github.com/bmatcuk/doublestar/v4 v4.8.1
14 github.com/charlievieth/fastwalk v1.0.11
14 github.com/charlievieth/fastwalk v1.0.11
15 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
15 github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1.0.20250607113720-eb5e1cf3b09e
@@ -18,6 +18,7 @@ require (
18 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
18 github.com/charmbracelet/lipgloss/v2 v2.0.0-beta.1.0.20250523195325-2d1af06b557c
19 github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413
19 github.com/charmbracelet/x/ansi v0.9.3-0.20250602153603-fb931ed90413
20 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9
20 github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9
21+ github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a
21 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
22 github.com/disintegration/imageorient v0.0.0-20180920195336-8147d86e83ec
22 github.com/fsnotify/fsnotify v1.8.0
23 github.com/fsnotify/fsnotify v1.8.0
23 github.com/go-logfmt/logfmt v0.6.0
24 github.com/go-logfmt/logfmt v0.6.0
@@ -65,7 +66,7 @@ require (
65 github.com/aymerick/douceur v0.2.0 // indirect
66 github.com/aymerick/douceur v0.2.0 // indirect
66 github.com/charmbracelet/colorprofile v0.3.1 // indirect
67 github.com/charmbracelet/colorprofile v0.3.1 // indirect
67 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
68 github.com/charmbracelet/x/cellbuf v0.0.14-0.20250516160309-24eee56f89fa // indirect
68− github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620
69+ github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef
69 github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 // indirect
70 github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 // indirect
70 github.com/charmbracelet/x/term v0.2.1 // indirect
71 github.com/charmbracelet/x/term v0.2.1 // indirect
71 github.com/charmbracelet/x/windows v0.2.1 // indirect
72 github.com/charmbracelet/x/windows v0.2.1 // indirect
go.sum+4−4
@@ -60,8 +60,8 @@ github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
60github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
60github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
61github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
61github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
62github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
62github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
63−github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
63+github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
64−github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
64+github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
65github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
65github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
66github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
66github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
67github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
67github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
@@ -86,8 +86,8 @@ github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9 h1:f
86github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9/go.mod h1:vr+xCFylsPYq2qSz+n5/jItjcK2/PgrKFMTI7VRR6CI=
86github.com/charmbracelet/x/exp/charmtone v0.0.0-20250530202730-6ba1785cd7b9/go.mod h1:vr+xCFylsPYq2qSz+n5/jItjcK2/PgrKFMTI7VRR6CI=
87github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
87github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a h1:FsHEJ52OC4VuTzU8t+n5frMjLvpYWEznSr/u8tnkCYw=
88github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
88github.com/charmbracelet/x/exp/golden v0.0.0-20250207160936-21c02780d27a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
89−github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620 h1:/PN4jqP3ki9NvtHRrYZ9ewCutKZB6DK8frTW+Dj/MWs=
89+github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef h1:v7qwsZ2OxzlwvpKwz8dtZXp7fIJlcDEUOyFBNE4fz4Q=
90−github.com/charmbracelet/x/exp/slice v0.0.0-20250528180458-2d5d6cb84620/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
90+github.com/charmbracelet/x/exp/slice v0.0.0-20250611152503-f53cdd7e01ef/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
91github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 h1:BxAEmOBIDajkgao3EsbBxKQCYvgYPGdT62WASLvtf4Y=
91github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86 h1:BxAEmOBIDajkgao3EsbBxKQCYvgYPGdT62WASLvtf4Y=
92github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86/go.mod h1:62Rp/6EtTxoeJDSdtpA3tJp3y3ZRpsiekBSje+K8htA=
92github.com/charmbracelet/x/input v0.3.5-0.20250509021451-13796e822d86/go.mod h1:62Rp/6EtTxoeJDSdtpA3tJp3y3ZRpsiekBSje+K8htA=
93github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
93github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
internal/db/db.go−10
@@ -78,9 +78,6 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
78 if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil {
78 if q.listSessionsStmt, err = db.PrepareContext(ctx, listSessions); err != nil {
79 return nil, fmt.Errorf("error preparing query ListSessions: %w", err)
79 return nil, fmt.Errorf("error preparing query ListSessions: %w", err)
80 }
80 }
81− if q.updateFileStmt, err = db.PrepareContext(ctx, updateFile); err != nil {
82− return nil, fmt.Errorf("error preparing query UpdateFile: %w", err)
83− }
84 if q.updateMessageStmt, err = db.PrepareContext(ctx, updateMessage); err != nil {
81 if q.updateMessageStmt, err = db.PrepareContext(ctx, updateMessage); err != nil {
85 return nil, fmt.Errorf("error preparing query UpdateMessage: %w", err)
82 return nil, fmt.Errorf("error preparing query UpdateMessage: %w", err)
86 }
83 }
@@ -182,11 +179,6 @@ func (q *Queries) Close() error {
182 err = fmt.Errorf("error closing listSessionsStmt: %w", cerr)
179 err = fmt.Errorf("error closing listSessionsStmt: %w", cerr)
183 }
180 }
184 }
181 }
185− if q.updateFileStmt != nil {
186− if cerr := q.updateFileStmt.Close(); cerr != nil {
187− err = fmt.Errorf("error closing updateFileStmt: %w", cerr)
188− }
189− }
190 if q.updateMessageStmt != nil {
182 if q.updateMessageStmt != nil {
191 if cerr := q.updateMessageStmt.Close(); cerr != nil {
183 if cerr := q.updateMessageStmt.Close(); cerr != nil {
192 err = fmt.Errorf("error closing updateMessageStmt: %w", cerr)
184 err = fmt.Errorf("error closing updateMessageStmt: %w", cerr)
@@ -254,7 +246,6 @@ type Queries struct {
254 listMessagesBySessionStmt *sql.Stmt
246 listMessagesBySessionStmt *sql.Stmt
255 listNewFilesStmt *sql.Stmt
247 listNewFilesStmt *sql.Stmt
256 listSessionsStmt *sql.Stmt
248 listSessionsStmt *sql.Stmt
257− updateFileStmt *sql.Stmt
258 updateMessageStmt *sql.Stmt
249 updateMessageStmt *sql.Stmt
259 updateSessionStmt *sql.Stmt
250 updateSessionStmt *sql.Stmt
260}
251}
@@ -281,7 +272,6 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
281 listMessagesBySessionStmt: q.listMessagesBySessionStmt,
272 listMessagesBySessionStmt: q.listMessagesBySessionStmt,
282 listNewFilesStmt: q.listNewFilesStmt,
273 listNewFilesStmt: q.listNewFilesStmt,
283 listSessionsStmt: q.listSessionsStmt,
274 listSessionsStmt: q.listSessionsStmt,
284− updateFileStmt: q.updateFileStmt,
285 updateMessageStmt: q.updateMessageStmt,
275 updateMessageStmt: q.updateMessageStmt,
286 updateSessionStmt: q.updateSessionStmt,
276 updateSessionStmt: q.updateSessionStmt,
287 }
277 }
internal/db/files.sql.go+7−38
@@ -29,7 +29,7 @@ type CreateFileParams struct {
29 SessionID string `json:"session_id"`
29 SessionID string `json:"session_id"`
30 Path string `json:"path"`
30 Path string `json:"path"`
31 Content string `json:"content"`
31 Content string `json:"content"`
32− Version string `json:"version"`
32+ Version int64 `json:"version"`
33}
33}
34
34
35func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
35func (q *Queries) CreateFile(ctx context.Context, arg CreateFileParams) (File, error) {
@@ -98,7 +98,7 @@ const getFileByPathAndSession = `-- name: GetFileByPathAndSession :one
98SELECT id, session_id, path, content, version, created_at, updated_at
98SELECT id, session_id, path, content, version, created_at, updated_at
99FROM files
99FROM files
100WHERE path = ? AND session_id = ?
100WHERE path = ? AND session_id = ?
101−ORDER BY created_at DESC
101+ORDER BY version DESC, created_at DESC
102LIMIT 1
102LIMIT 1
103`
103`
104
104
@@ -126,7 +126,7 @@ const listFilesByPath = `-- name: ListFilesByPath :many
126SELECT id, session_id, path, content, version, created_at, updated_at
126SELECT id, session_id, path, content, version, created_at, updated_at
127FROM files
127FROM files
128WHERE path = ?
128WHERE path = ?
129−ORDER BY created_at DESC
129+ORDER BY version DESC, created_at DESC
130`
130`
131
131
132func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) {
132func (q *Queries) ListFilesByPath(ctx context.Context, path string) ([]File, error) {
@@ -164,7 +164,7 @@ const listFilesBySession = `-- name: ListFilesBySession :many
164SELECT id, session_id, path, content, version, created_at, updated_at
164SELECT id, session_id, path, content, version, created_at, updated_at
165FROM files
165FROM files
166WHERE session_id = ?
166WHERE session_id = ?
167−ORDER BY created_at ASC
167+ORDER BY version ASC, created_at ASC
168`
168`
169
169
170func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) {
170func (q *Queries) ListFilesBySession(ctx context.Context, sessionID string) ([]File, error) {
@@ -202,10 +202,10 @@ const listLatestSessionFiles = `-- name: ListLatestSessionFiles :many
202SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
202SELECT f.id, f.session_id, f.path, f.content, f.version, f.created_at, f.updated_at
203FROM files f
203FROM files f
204INNER JOIN (
204INNER JOIN (
205− SELECT path, MAX(created_at) as max_created_at
205+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
206 FROM files
206 FROM files
207 GROUP BY path
207 GROUP BY path
208−) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
208+) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
209WHERE f.session_id = ?
209WHERE f.session_id = ?
210ORDER BY f.path
210ORDER BY f.path
211`
211`
@@ -245,7 +245,7 @@ const listNewFiles = `-- name: ListNewFiles :many
245SELECT id, session_id, path, content, version, created_at, updated_at
245SELECT id, session_id, path, content, version, created_at, updated_at
246FROM files
246FROM files
247WHERE is_new = 1
247WHERE is_new = 1
248−ORDER BY created_at DESC
248+ORDER BY version DESC, created_at DESC
249`
249`
250
250
251func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
251func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
@@ -278,34 +278,3 @@ func (q *Queries) ListNewFiles(ctx context.Context) ([]File, error) {
278 }
278 }
279 return items, nil
279 return items, nil
280}
280}
281−
282−const updateFile = `-- name: UpdateFile :one
283−UPDATE files
284−SET
285− content = ?,
286− version = ?,
287− updated_at = strftime('%s', 'now')
288−WHERE id = ?
289−RETURNING id, session_id, path, content, version, created_at, updated_at
290−`
291−
292−type UpdateFileParams struct {
293− Content string `json:"content"`
294− Version string `json:"version"`
295− ID string `json:"id"`
296−}
297−
298−func (q *Queries) UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error) {
299− row := q.queryRow(ctx, q.updateFileStmt, updateFile, arg.Content, arg.Version, arg.ID)
300− var i File
301− err := row.Scan(
302− &i.ID,
303− &i.SessionID,
304− &i.Path,
305− &i.Content,
306− &i.Version,
307− &i.CreatedAt,
308− &i.UpdatedAt,
309− )
310− return i, err
311−}
internal/db/migrations/20250424200609_initial.sql+1−1
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS files (
26 session_id TEXT NOT NULL,
26 session_id TEXT NOT NULL,
27 path TEXT NOT NULL,
27 path TEXT NOT NULL,
28 content TEXT NOT NULL,
28 content TEXT NOT NULL,
29− version TEXT NOT NULL,
29+ version INTEGER NOT NULL DEFAULT 0,
30 created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
30 created_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
31 updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
31 updated_at INTEGER NOT NULL, -- Unix timestamp in milliseconds
32 FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
32 FOREIGN KEY (session_id) REFERENCES sessions (id) ON DELETE CASCADE,
internal/db/models.go+1−1
@@ -13,7 +13,7 @@ type File struct {
13 SessionID string `json:"session_id"`
13 SessionID string `json:"session_id"`
14 Path string `json:"path"`
14 Path string `json:"path"`
15 Content string `json:"content"`
15 Content string `json:"content"`
16− Version string `json:"version"`
16+ Version int64 `json:"version"`
17 CreatedAt int64 `json:"created_at"`
17 CreatedAt int64 `json:"created_at"`
18 UpdatedAt int64 `json:"updated_at"`
18 UpdatedAt int64 `json:"updated_at"`
19}
19}
internal/db/querier.go−1
@@ -27,7 +27,6 @@ type Querier interface {
27 ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
27 ListMessagesBySession(ctx context.Context, sessionID string) ([]Message, error)
28 ListNewFiles(ctx context.Context) ([]File, error)
28 ListNewFiles(ctx context.Context) ([]File, error)
29 ListSessions(ctx context.Context) ([]Session, error)
29 ListSessions(ctx context.Context) ([]Session, error)
30− UpdateFile(ctx context.Context, arg UpdateFileParams) (File, error)
31 UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
30 UpdateMessage(ctx context.Context, arg UpdateMessageParams) error
32 UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
31 UpdateSession(ctx context.Context, arg UpdateSessionParams) (Session, error)
33}
32}
internal/db/sql/files.sql+6−15
@@ -7,20 +7,20 @@ WHERE id = ? LIMIT 1;
7SELECT *
7SELECT *
8FROM files
8FROM files
9WHERE path = ? AND session_id = ?
9WHERE path = ? AND session_id = ?
10−ORDER BY created_at DESC
10+ORDER BY version DESC, created_at DESC
11LIMIT 1;
11LIMIT 1;
12
12
13-- name: ListFilesBySession :many
13-- name: ListFilesBySession :many
14SELECT *
14SELECT *
15FROM files
15FROM files
16WHERE session_id = ?
16WHERE session_id = ?
17−ORDER BY created_at ASC;
17+ORDER BY version ASC, created_at ASC;
18
18
19-- name: ListFilesByPath :many
19-- name: ListFilesByPath :many
20SELECT *
20SELECT *
21FROM files
21FROM files
22WHERE path = ?
22WHERE path = ?
23−ORDER BY created_at DESC;
23+ORDER BY version DESC, created_at DESC;
24
24
25-- name: CreateFile :one
25-- name: CreateFile :one
26INSERT INTO files (
26INSERT INTO files (
@@ -36,15 +36,6 @@ INSERT INTO files (
36)
36)
37RETURNING *;
37RETURNING *;
38
38
39−-- name: UpdateFile :one
40−UPDATE files
41−SET
42− content = ?,
43− version = ?,
44− updated_at = strftime('%s', 'now')
45−WHERE id = ?
46−RETURNING *;
47−
48-- name: DeleteFile :exec
39-- name: DeleteFile :exec
49DELETE FROM files
40DELETE FROM files
50WHERE id = ?;
41WHERE id = ?;
@@ -57,10 +48,10 @@ WHERE session_id = ?;
57SELECT f.*
48SELECT f.*
58FROM files f
49FROM files f
59INNER JOIN (
50INNER JOIN (
60− SELECT path, MAX(created_at) as max_created_at
51+ SELECT path, MAX(version) as max_version, MAX(created_at) as max_created_at
61 FROM files
52 FROM files
62 GROUP BY path
53 GROUP BY path
63−) latest ON f.path = latest.path AND f.created_at = latest.max_created_at
54+) latest ON f.path = latest.path AND f.version = latest.max_version AND f.created_at = latest.max_created_at
64WHERE f.session_id = ?
55WHERE f.session_id = ?
65ORDER BY f.path;
56ORDER BY f.path;
66
57
@@ -68,4 +59,4 @@ ORDER BY f.path;
68SELECT *
59SELECT *
69FROM files
60FROM files
70WHERE is_new = 1
61WHERE is_new = 1
71−ORDER BY created_at DESC;
62+ORDER BY version DESC, created_at DESC;
internal/exp/diffview/Taskfile.yamlnew file+102
@@ -0,0 +1,102 @@
1+version: "3"
2+
3+tasks:
4+ test:
5+ desc: Run tests
6+ cmds:
7+ - go test ./... {{.CLI_ARGS}}
8+
9+ test:update:
10+ desc: Run tests and update golden files
11+ cmds:
12+ - go test ./... -update {{.CLI_ARGS}}
13+
14+ test:print:
15+ desc: Print golden files for debugging
16+ method: none
17+ sources:
18+ - ./testdata/TestDiffView/**/*.golden
19+ cmds:
20+ - for: sources
21+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
22+ silent: true
23+
24+ test:print:width:unified:
25+ desc: Print golden files for debugging
26+ method: none
27+ sources:
28+ - ./testdata/TestDiffViewWidth/Unified/*.golden
29+ cmds:
30+ - for: sources
31+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
32+ silent: true
33+
34+ test:print:width:split:
35+ desc: Print golden files for debugging
36+ method: none
37+ sources:
38+ - ./testdata/TestDiffViewWidth/Split/*.golden
39+ cmds:
40+ - for: sources
41+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
42+ silent: true
43+
44+ test:print:height:unified:
45+ desc: Print golden files for debugging
46+ method: none
47+ sources:
48+ - ./testdata/TestDiffViewHeight/Unified/*.golden
49+ cmds:
50+ - for: sources
51+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
52+ silent: true
53+
54+ test:print:height:split:
55+ desc: Print golden files for debugging
56+ method: none
57+ sources:
58+ - ./testdata/TestDiffViewHeight/Split/*.golden
59+ cmds:
60+ - for: sources
61+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
62+ silent: true
63+
64+ test:print:xoffset:unified:
65+ desc: Print golden files for debugging
66+ method: none
67+ sources:
68+ - ./testdata/TestDiffViewXOffset/Unified/*.golden
69+ cmds:
70+ - for: sources
71+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
72+ silent: true
73+
74+ test:print:xoffset:split:
75+ desc: Print golden files for debugging
76+ method: none
77+ sources:
78+ - ./testdata/TestDiffViewXOffset/Split/*.golden
79+ cmds:
80+ - for: sources
81+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
82+ silent: true
83+
84+ test:print:yoffset:unified:
85+ desc: Print golden files for debugging
86+ method: none
87+ sources:
88+ - ./testdata/TestDiffViewYOffset/Unified/*.golden
89+ cmds:
90+ - for: sources
91+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
92+ silent: true
93+
94+ test:print:yoffset:split:
95+ desc: Print golden files for debugging
96+ method: none
97+ sources:
98+ - ./testdata/TestDiffViewYOffset/Split/*.golden
99+ cmds:
100+ - for: sources
101+ cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}}
102+ silent: true
internal/exp/diffview/chroma.gonew file+52
@@ -0,0 +1,52 @@
1+package diffview
2+
3+import (
4+ "fmt"
5+ "image/color"
6+ "io"
7+
8+ "github.com/alecthomas/chroma/v2"
9+ "github.com/charmbracelet/lipgloss/v2"
10+)
11+
12+var _ chroma.Formatter = chromaFormatter{}
13+
14+// chromaFormatter is a custom formatter for Chroma that uses Lip Gloss for
15+// foreground styling, while keeping a forced background color.
16+type chromaFormatter struct {
17+ bgColor color.Color
18+}
19+
20+// Format implements the chroma.Formatter interface.
21+func (c chromaFormatter) Format(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
22+ for token := it(); token != chroma.EOF; token = it() {
23+ entry := style.Get(token.Type)
24+ if entry.IsZero() {
25+ if _, err := fmt.Fprint(w, token.Value); err != nil {
26+ return err
27+ }
28+ continue
29+ }
30+
31+ s := lipgloss.NewStyle().
32+ Background(c.bgColor)
33+
34+ if entry.Bold == chroma.Yes {
35+ s = s.Bold(true)
36+ }
37+ if entry.Underline == chroma.Yes {
38+ s = s.Underline(true)
39+ }
40+ if entry.Italic == chroma.Yes {
41+ s = s.Italic(true)
42+ }
43+ if entry.Colour.IsSet() {
44+ s = s.Foreground(lipgloss.Color(entry.Colour.String()))
45+ }
46+
47+ if _, err := fmt.Fprint(w, s.Render(token.Value)); err != nil {
48+ return err
49+ }
50+ }
51+ return nil
52+}
internal/exp/diffview/diffview.gonew file+681
@@ -0,0 +1,681 @@
1+package diffview
2+
3+import (
4+ "fmt"
5+ "image/color"
6+ "strconv"
7+ "strings"
8+
9+ "github.com/alecthomas/chroma/v2"
10+ "github.com/alecthomas/chroma/v2/lexers"
11+ "github.com/aymanbagabas/go-udiff"
12+ "github.com/aymanbagabas/go-udiff/myers"
13+ "github.com/charmbracelet/lipgloss/v2"
14+ "github.com/charmbracelet/x/ansi"
15+)
16+
17+const (
18+ leadingSymbolsSize = 2
19+ lineNumPadding = 1
20+)
21+
22+type file struct {
23+ path string
24+ content string
25+}
26+
27+type layout int
28+
29+const (
30+ layoutUnified layout = iota + 1
31+ layoutSplit
32+)
33+
34+// DiffView represents a view for displaying differences between two files.
35+type DiffView struct {
36+ layout layout
37+ before file
38+ after file
39+ contextLines int
40+ lineNumbers bool
41+ highlight bool
42+ height int
43+ width int
44+ xOffset int
45+ yOffset int
46+ style Style
47+ tabWidth int
48+ chromaStyle *chroma.Style
49+
50+ isComputed bool
51+ err error
52+ unified udiff.UnifiedDiff
53+ edits []udiff.Edit
54+
55+ splitHunks []splitHunk
56+
57+ codeWidth int
58+ fullCodeWidth int // with leading symbols
59+ extraColOnAfter bool // add extra column on after panel
60+ beforeNumDigits int
61+ afterNumDigits int
62+}
63+
64+// New creates a new DiffView with default settings.
65+func New() *DiffView {
66+ dv := &DiffView{
67+ layout: layoutUnified,
68+ contextLines: udiff.DefaultContextLines,
69+ lineNumbers: true,
70+ tabWidth: 8,
71+ }
72+ dv.style = DefaultDarkStyle()
73+ return dv
74+}
75+
76+// Unified sets the layout of the DiffView to unified.
77+func (dv *DiffView) Unified() *DiffView {
78+ dv.layout = layoutUnified
79+ return dv
80+}
81+
82+// Split sets the layout of the DiffView to split (side-by-side).
83+func (dv *DiffView) Split() *DiffView {
84+ dv.layout = layoutSplit
85+ return dv
86+}
87+
88+// Before sets the "before" file for the DiffView.
89+func (dv *DiffView) Before(path, content string) *DiffView {
90+ dv.before = file{path: path, content: content}
91+ return dv
92+}
93+
94+// After sets the "after" file for the DiffView.
95+func (dv *DiffView) After(path, content string) *DiffView {
96+ dv.after = file{path: path, content: content}
97+ return dv
98+}
99+
100+// ContextLines sets the number of context lines for the DiffView.
101+func (dv *DiffView) ContextLines(contextLines int) *DiffView {
102+ dv.contextLines = contextLines
103+ return dv
104+}
105+
106+// Style sets the style for the DiffView.
107+func (dv *DiffView) Style(style Style) *DiffView {
108+ dv.style = style
109+ return dv
110+}
111+
112+// LineNumbers sets whether to display line numbers in the DiffView.
113+func (dv *DiffView) LineNumbers(lineNumbers bool) *DiffView {
114+ dv.lineNumbers = lineNumbers
115+ return dv
116+}
117+
118+// SyntaxHightlight sets whether to enable syntax highlighting in the DiffView.
119+func (dv *DiffView) SyntaxHightlight(highlight bool) *DiffView {
120+ dv.highlight = highlight
121+ return dv
122+}
123+
124+// Height sets the height of the DiffView.
125+func (dv *DiffView) Height(height int) *DiffView {
126+ dv.height = height
127+ return dv
128+}
129+
130+// Width sets the width of the DiffView.
131+func (dv *DiffView) Width(width int) *DiffView {
132+ dv.width = width
133+ return dv
134+}
135+
136+// XOffset sets the horizontal offset for the DiffView.
137+func (dv *DiffView) XOffset(xOffset int) *DiffView {
138+ dv.xOffset = xOffset
139+ return dv
140+}
141+
142+// YOffset sets the vertical offset for the DiffView.
143+func (dv *DiffView) YOffset(yOffset int) *DiffView {
144+ dv.yOffset = yOffset
145+ return dv
146+}
147+
148+// TabWidth sets the tab width. Only relevant for code that contains tabs, like
149+// Go code.
150+func (dv *DiffView) TabWidth(tabWidth int) *DiffView {
151+ dv.tabWidth = tabWidth
152+ return dv
153+}
154+
155+// ChromaStyle sets the chroma style for syntax highlighting.
156+// If nil, no syntax highlighting will be applied.
157+func (dv *DiffView) ChromaStyle(style *chroma.Style) *DiffView {
158+ dv.chromaStyle = style
159+ return dv
160+}
161+
162+// String returns the string representation of the DiffView.
163+func (dv *DiffView) String() string {
164+ dv.replaceTabs()
165+ if err := dv.computeDiff(); err != nil {
166+ return err.Error()
167+ }
168+ dv.convertDiffToSplit()
169+ dv.adjustStyles()
170+ dv.detectNumDigits()
171+
172+ if dv.width <= 0 {
173+ dv.detectCodeWidth()
174+ } else {
175+ dv.resizeCodeWidth()
176+ }
177+
178+ style := lipgloss.NewStyle()
179+ if dv.width > 0 {
180+ style = style.MaxWidth(dv.width)
181+ }
182+ if dv.height > 0 {
183+ style = style.MaxHeight(dv.height)
184+ }
185+
186+ switch dv.layout {
187+ case layoutUnified:
188+ return style.Render(strings.TrimSuffix(dv.renderUnified(), "\n"))
189+ case layoutSplit:
190+ return style.Render(strings.TrimSuffix(dv.renderSplit(), "\n"))
191+ default:
192+ panic("unknown diffview layout")
193+ }
194+}
195+
196+// replaceTabs replaces tabs in the before and after file contents with spaces
197+// according to the specified tab width.
198+func (dv *DiffView) replaceTabs() {
199+ spaces := strings.Repeat(" ", dv.tabWidth)
200+ dv.before.content = strings.ReplaceAll(dv.before.content, "\t", spaces)
201+ dv.after.content = strings.ReplaceAll(dv.after.content, "\t", spaces)
202+}
203+
204+// computeDiff computes the differences between the "before" and "after" files.
205+func (dv *DiffView) computeDiff() error {
206+ if dv.isComputed {
207+ return dv.err
208+ }
209+ dv.isComputed = true
210+ dv.edits = myers.ComputeEdits( //nolint:staticcheck
211+ dv.before.content,
212+ dv.after.content,
213+ )
214+ dv.unified, dv.err = udiff.ToUnifiedDiff(
215+ dv.before.path,
216+ dv.after.path,
217+ dv.before.content,
218+ dv.edits,
219+ dv.contextLines,
220+ )
221+ return dv.err
222+}
223+
224+// convertDiffToSplit converts the unified diff to a split diff if the layout is
225+// set to split.
226+func (dv *DiffView) convertDiffToSplit() {
227+ if dv.layout != layoutSplit {
228+ return
229+ }
230+
231+ dv.splitHunks = make([]splitHunk, len(dv.unified.Hunks))
232+ for i, h := range dv.unified.Hunks {
233+ dv.splitHunks[i] = hunkToSplit(h)
234+ }
235+}
236+
237+// adjustStyles adjusts adds padding and alignment to the styles.
238+func (dv *DiffView) adjustStyles() {
239+ setPadding := func(s lipgloss.Style) lipgloss.Style {
240+ return s.Padding(0, lineNumPadding).Align(lipgloss.Right)
241+ }
242+ dv.style.MissingLine.LineNumber = setPadding(dv.style.MissingLine.LineNumber)
243+ dv.style.DividerLine.LineNumber = setPadding(dv.style.DividerLine.LineNumber)
244+ dv.style.EqualLine.LineNumber = setPadding(dv.style.EqualLine.LineNumber)
245+ dv.style.InsertLine.LineNumber = setPadding(dv.style.InsertLine.LineNumber)
246+ dv.style.DeleteLine.LineNumber = setPadding(dv.style.DeleteLine.LineNumber)
247+}
248+
249+// detectNumDigits calculates the maximum number of digits needed for before and
250+// after line numbers.
251+func (dv *DiffView) detectNumDigits() {
252+ dv.beforeNumDigits = 0
253+ dv.afterNumDigits = 0
254+
255+ for _, h := range dv.unified.Hunks {
256+ dv.beforeNumDigits = max(dv.beforeNumDigits, len(strconv.Itoa(h.FromLine+len(h.Lines))))
257+ dv.afterNumDigits = max(dv.afterNumDigits, len(strconv.Itoa(h.ToLine+len(h.Lines))))
258+ }
259+}
260+
261+// detectCodeWidth calculates the maximum width of code lines in the diff view.
262+func (dv *DiffView) detectCodeWidth() {
263+ switch dv.layout {
264+ case layoutUnified:
265+ dv.detectUnifiedCodeWidth()
266+ case layoutSplit:
267+ dv.detectSplitCodeWidth()
268+ }
269+ dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
270+}
271+
272+// detectUnifiedCodeWidth calculates the maximum width of code lines in a
273+// unified diff.
274+func (dv *DiffView) detectUnifiedCodeWidth() {
275+ dv.codeWidth = 0
276+
277+ for _, h := range dv.unified.Hunks {
278+ shownLines := ansi.StringWidth(dv.hunkLineFor(h))
279+
280+ for _, l := range h.Lines {
281+ lineWidth := ansi.StringWidth(strings.TrimSuffix(l.Content, "\n")) + 1
282+ dv.codeWidth = max(dv.codeWidth, lineWidth, shownLines)
283+ }
284+ }
285+}
286+
287+// detectSplitCodeWidth calculates the maximum width of code lines in a
288+// split diff.
289+func (dv *DiffView) detectSplitCodeWidth() {
290+ dv.codeWidth = 0
291+
292+ for i, h := range dv.splitHunks {
293+ shownLines := ansi.StringWidth(dv.hunkLineFor(dv.unified.Hunks[i]))
294+
295+ for _, l := range h.lines {
296+ if l.before != nil {
297+ codeWidth := ansi.StringWidth(strings.TrimSuffix(l.before.Content, "\n")) + 1
298+ dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
299+ }
300+ if l.after != nil {
301+ codeWidth := ansi.StringWidth(strings.TrimSuffix(l.after.Content, "\n")) + 1
302+ dv.codeWidth = max(dv.codeWidth, codeWidth, shownLines)
303+ }
304+ }
305+ }
306+}
307+
308+// resizeCodeWidth resizes the code width to fit within the specified width.
309+func (dv *DiffView) resizeCodeWidth() {
310+ fullNumWidth := dv.beforeNumDigits + dv.afterNumDigits
311+ fullNumWidth += lineNumPadding * 4 // left and right padding for both line numbers
312+
313+ switch dv.layout {
314+ case layoutUnified:
315+ dv.codeWidth = dv.width - fullNumWidth - leadingSymbolsSize
316+ case layoutSplit:
317+ remainingWidth := dv.width - fullNumWidth - leadingSymbolsSize*2
318+ dv.codeWidth = remainingWidth / 2
319+ dv.extraColOnAfter = isOdd(remainingWidth)
320+ }
321+
322+ dv.fullCodeWidth = dv.codeWidth + leadingSymbolsSize
323+}
324+
325+// renderUnified renders the unified diff view as a string.
326+func (dv *DiffView) renderUnified() string {
327+ var b strings.Builder
328+
329+ fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
330+ printedLines := -dv.yOffset
331+
332+ write := func(s string) {
333+ if printedLines >= 0 {
334+ b.WriteString(s)
335+ }
336+ }
337+
338+outer:
339+ for i, h := range dv.unified.Hunks {
340+ ls := dv.style.DividerLine
341+ if dv.lineNumbers {
342+ write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
343+ write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
344+ }
345+ content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…")
346+ write(ls.Code.Width(dv.fullCodeWidth).Render(content))
347+ write("\n")
348+ printedLines++
349+
350+ beforeLine := h.FromLine
351+ afterLine := h.ToLine
352+
353+ for j, l := range h.Lines {
354+ // print ellipis if we don't have enough space to print the rest of the diff
355+ hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
356+ isLastHunk := i+1 == len(dv.unified.Hunks)
357+ isLastLine := j+1 == len(h.Lines)
358+ if hasReachedHeight && (!isLastHunk || !isLastLine) {
359+ ls := dv.lineStyleForType(l.Kind)
360+ if dv.lineNumbers {
361+ write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
362+ write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
363+ }
364+ write(fullContentStyle.Render(
365+ ls.Code.Width(dv.fullCodeWidth).Render(" …"),
366+ ))
367+ write("\n")
368+ break outer
369+ }
370+
371+ getContent := func(ls LineStyle) string {
372+ content := strings.TrimSuffix(l.Content, "\n")
373+ content = dv.hightlightCode(content, ls.Code.GetBackground())
374+ content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
375+ content = ansi.Truncate(content, dv.codeWidth, "…")
376+ return content
377+ }
378+
379+ leadingEllipsis := dv.xOffset > 0 && strings.TrimSpace(content) != ""
380+
381+ switch l.Kind {
382+ case udiff.Equal:
383+ ls := dv.style.EqualLine
384+ content := getContent(ls)
385+ if dv.lineNumbers {
386+ write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
387+ write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
388+ }
389+ write(fullContentStyle.Render(
390+ ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
391+ ))
392+ beforeLine++
393+ afterLine++
394+ case udiff.Insert:
395+ ls := dv.style.InsertLine
396+ content := getContent(ls)
397+ if dv.lineNumbers {
398+ write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
399+ write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
400+ }
401+ write(fullContentStyle.Render(
402+ ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
403+ ls.Code.Width(dv.codeWidth).Render(content),
404+ ))
405+ afterLine++
406+ case udiff.Delete:
407+ ls := dv.style.DeleteLine
408+ content := getContent(ls)
409+ if dv.lineNumbers {
410+ write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
411+ write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
412+ }
413+ write(fullContentStyle.Render(
414+ ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
415+ ls.Code.Width(dv.codeWidth).Render(content),
416+ ))
417+ beforeLine++
418+ }
419+ write("\n")
420+
421+ printedLines++
422+ }
423+ }
424+
425+ for printedLines < dv.height {
426+ ls := dv.style.MissingLine
427+ if dv.lineNumbers {
428+ write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
429+ write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
430+ }
431+ write(ls.Code.Width(dv.fullCodeWidth).Render(" "))
432+ write("\n")
433+ printedLines++
434+ }
435+
436+ return b.String()
437+}
438+
439+// renderSplit renders the split (side-by-side) diff view as a string.
440+func (dv *DiffView) renderSplit() string {
441+ var b strings.Builder
442+
443+ beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth)
444+ afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter))
445+ printedLines := -dv.yOffset
446+
447+ write := func(s string) {
448+ if printedLines >= 0 {
449+ b.WriteString(s)
450+ }
451+ }
452+
453+outer:
454+ for i, h := range dv.splitHunks {
455+ ls := dv.style.DividerLine
456+ if dv.lineNumbers {
457+ write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
458+ }
459+ content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…")
460+ write(ls.Code.Width(dv.fullCodeWidth).Render(content))
461+ if dv.lineNumbers {
462+ write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
463+ }
464+ write(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
465+ write("\n")
466+ printedLines++
467+
468+ beforeLine := h.fromLine
469+ afterLine := h.toLine
470+
471+ for j, l := range h.lines {
472+ // print ellipis if we don't have enough space to print the rest of the diff
473+ hasReachedHeight := dv.height > 0 && printedLines+1 == dv.height
474+ isLastHunk := i+1 == len(dv.unified.Hunks)
475+ isLastLine := j+1 == len(h.lines)
476+ if hasReachedHeight && (!isLastHunk || !isLastLine) {
477+ ls := dv.style.MissingLine
478+ if l.before != nil {
479+ ls = dv.lineStyleForType(l.before.Kind)
480+ }
481+ if dv.lineNumbers {
482+ write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits)))
483+ }
484+ write(beforeFullContentStyle.Render(
485+ ls.Code.Width(dv.fullCodeWidth).Render(" …"),
486+ ))
487+ ls = dv.style.MissingLine
488+ if l.after != nil {
489+ ls = dv.lineStyleForType(l.after.Kind)
490+ }
491+ if dv.lineNumbers {
492+ write(ls.LineNumber.Render(pad("…", dv.afterNumDigits)))
493+ }
494+ write(afterFullContentStyle.Render(
495+ ls.Code.Width(dv.fullCodeWidth).Render(" …"),
496+ ))
497+ write("\n")
498+ break outer
499+ }
500+
501+ getContent := func(content string, ls LineStyle) string {
502+ content = strings.TrimSuffix(content, "\n")
503+ content = dv.hightlightCode(content, ls.Code.GetBackground())
504+ content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content))
505+ content = ansi.Truncate(content, dv.codeWidth, "…")
506+ return content
507+ }
508+ getLeadingEllipsis := func(content string) bool {
509+ return dv.xOffset > 0 && strings.TrimSpace(content) != ""
510+ }
511+
512+ switch {
513+ case l.before == nil:
514+ ls := dv.style.MissingLine
515+ if dv.lineNumbers {
516+ write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
517+ }
518+ write(beforeFullContentStyle.Render(
519+ ls.Code.Width(dv.fullCodeWidth).Render(" "),
520+ ))
521+ case l.before.Kind == udiff.Equal:
522+ ls := dv.style.EqualLine
523+ content := getContent(l.before.Content, ls)
524+ leadingEllipsis := getLeadingEllipsis(content)
525+ if dv.lineNumbers {
526+ write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
527+ }
528+ write(beforeFullContentStyle.Render(
529+ ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content),
530+ ))
531+ beforeLine++
532+ case l.before.Kind == udiff.Delete:
533+ ls := dv.style.DeleteLine
534+ content := getContent(l.before.Content, ls)
535+ leadingEllipsis := getLeadingEllipsis(content)
536+ if dv.lineNumbers {
537+ write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits)))
538+ }
539+ write(beforeFullContentStyle.Render(
540+ ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) +
541+ ls.Code.Width(dv.codeWidth).Render(content),
542+ ))
543+ beforeLine++
544+ }
545+
546+ switch {
547+ case l.after == nil:
548+ ls := dv.style.MissingLine
549+ if dv.lineNumbers {
550+ write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
551+ }
552+ write(afterFullContentStyle.Render(
553+ ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "),
554+ ))
555+ case l.after.Kind == udiff.Equal:
556+ ls := dv.style.EqualLine
557+ content := getContent(l.after.Content, ls)
558+ leadingEllipsis := getLeadingEllipsis(content)
559+ if dv.lineNumbers {
560+ write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
561+ }
562+ write(afterFullContentStyle.Render(
563+ ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content),
564+ ))
565+ afterLine++
566+ case l.after.Kind == udiff.Insert:
567+ ls := dv.style.InsertLine
568+ content := getContent(l.after.Content, ls)
569+ leadingEllipsis := getLeadingEllipsis(content)
570+ if dv.lineNumbers {
571+ write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits)))
572+ }
573+ write(afterFullContentStyle.Render(
574+ ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) +
575+ ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content),
576+ ))
577+ afterLine++
578+ }
579+
580+ write("\n")
581+
582+ printedLines++
583+ }
584+ }
585+
586+ for printedLines < dv.height {
587+ ls := dv.style.MissingLine
588+ if dv.lineNumbers {
589+ write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits)))
590+ }
591+ write(ls.Code.Width(dv.fullCodeWidth).Render(" "))
592+ if dv.lineNumbers {
593+ write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits)))
594+ }
595+ write(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "))
596+ write("\n")
597+ printedLines++
598+ }
599+
600+ return b.String()
601+}
602+
603+// hunkLineFor formats the header line for a hunk in the unified diff view.
604+func (dv *DiffView) hunkLineFor(h *udiff.Hunk) string {
605+ beforeShownLines, afterShownLines := dv.hunkShownLines(h)
606+
607+ return fmt.Sprintf(
608+ " @@ -%d,%d +%d,%d @@ ",
609+ h.FromLine,
610+ beforeShownLines,
611+ h.ToLine,
612+ afterShownLines,
613+ )
614+}
615+
616+// hunkShownLines calculates the number of lines shown in a hunk for both before
617+// and after versions.
618+func (dv *DiffView) hunkShownLines(h *udiff.Hunk) (before, after int) {
619+ for _, l := range h.Lines {
620+ switch l.Kind {
621+ case udiff.Equal:
622+ before++
623+ after++
624+ case udiff.Insert:
625+ after++
626+ case udiff.Delete:
627+ before++
628+ }
629+ }
630+ return
631+}
632+
633+func (dv *DiffView) lineStyleForType(t udiff.OpKind) LineStyle {
634+ switch t {
635+ case udiff.Equal:
636+ return dv.style.EqualLine
637+ case udiff.Insert:
638+ return dv.style.InsertLine
639+ case udiff.Delete:
640+ return dv.style.DeleteLine
641+ default:
642+ return dv.style.MissingLine
643+ }
644+}
645+
646+func (dv *DiffView) hightlightCode(source string, bgColor color.Color) string {
647+ if dv.chromaStyle == nil {
648+ return source
649+ }
650+
651+ l := dv.getChromaLexer(source)
652+ f := dv.getChromaFormatter(bgColor)
653+
654+ it, err := l.Tokenise(nil, source)
655+ if err != nil {
656+ return source
657+ }
658+
659+ var b strings.Builder
660+ if err := f.Format(&b, dv.chromaStyle, it); err != nil {
661+ return source
662+ }
663+ return b.String()
664+}
665+
666+func (dv *DiffView) getChromaLexer(source string) chroma.Lexer {
667+ l := lexers.Match(dv.before.path)
668+ if l == nil {
669+ l = lexers.Analyse(source)
670+ }
671+ if l == nil {
672+ l = lexers.Fallback
673+ }
674+ return chroma.Coalesce(l)
675+}
676+
677+func (dv *DiffView) getChromaFormatter(gbColor color.Color) chroma.Formatter {
678+ return chromaFormatter{
679+ bgColor: gbColor,
680+ }
681+}
internal/exp/diffview/diffview_test.gonew file+300
@@ -0,0 +1,300 @@
1+package diffview_test
2+
3+import (
4+ _ "embed"
5+ "fmt"
6+ "strings"
7+ "testing"
8+
9+ "github.com/alecthomas/chroma/v2/styles"
10+ "github.com/charmbracelet/crush/internal/exp/diffview"
11+ "github.com/charmbracelet/x/ansi"
12+ "github.com/charmbracelet/x/exp/golden"
13+)
14+
15+//go:embed testdata/TestDefault.before
16+var TestDefaultBefore string
17+
18+//go:embed testdata/TestDefault.after
19+var TestDefaultAfter string
20+
21+//go:embed testdata/TestMultipleHunks.before
22+var TestMultipleHunksBefore string
23+
24+//go:embed testdata/TestMultipleHunks.after
25+var TestMultipleHunksAfter string
26+
27+//go:embed testdata/TestNarrow.before
28+var TestNarrowBefore string
29+
30+//go:embed testdata/TestNarrow.after
31+var TestNarrowAfter string
32+
33+//go:embed testdata/TestTabs.before
34+var TestTabsBefore string
35+
36+//go:embed testdata/TestTabs.after
37+var TestTabsAfter string
38+
39+type (
40+ TestFunc func(dv *diffview.DiffView) *diffview.DiffView
41+ TestFuncs map[string]TestFunc
42+)
43+
44+var (
45+ UnifiedFunc = func(dv *diffview.DiffView) *diffview.DiffView {
46+ return dv.Unified()
47+ }
48+ SplitFunc = func(dv *diffview.DiffView) *diffview.DiffView {
49+ return dv.Split()
50+ }
51+
52+ DefaultFunc = func(dv *diffview.DiffView) *diffview.DiffView {
53+ return dv.
54+ Before("main.go", TestDefaultBefore).
55+ After("main.go", TestDefaultAfter)
56+ }
57+ NoLineNumbersFunc = func(dv *diffview.DiffView) *diffview.DiffView {
58+ return dv.
59+ Before("main.go", TestDefaultBefore).
60+ After("main.go", TestDefaultAfter).
61+ LineNumbers(false)
62+ }
63+ MultipleHunksFunc = func(dv *diffview.DiffView) *diffview.DiffView {
64+ return dv.
65+ Before("main.go", TestMultipleHunksBefore).
66+ After("main.go", TestMultipleHunksAfter)
67+ }
68+ CustomContextLinesFunc = func(dv *diffview.DiffView) *diffview.DiffView {
69+ return dv.
70+ Before("main.go", TestMultipleHunksBefore).
71+ After("main.go", TestMultipleHunksAfter).
72+ ContextLines(4)
73+ }
74+ NarrowFunc = func(dv *diffview.DiffView) *diffview.DiffView {
75+ return dv.
76+ Before("text.txt", TestNarrowBefore).
77+ After("text.txt", TestNarrowAfter)
78+ }
79+ SmallWidthFunc = func(dv *diffview.DiffView) *diffview.DiffView {
80+ return dv.
81+ Before("main.go", TestMultipleHunksBefore).
82+ After("main.go", TestMultipleHunksAfter).
83+ Width(40)
84+ }
85+ LargeWidthFunc = func(dv *diffview.DiffView) *diffview.DiffView {
86+ return dv.
87+ Before("main.go", TestMultipleHunksBefore).
88+ After("main.go", TestMultipleHunksAfter).
89+ Width(120)
90+ }
91+ NoSyntaxHighlightFunc = func(dv *diffview.DiffView) *diffview.DiffView {
92+ return dv.
93+ Before("main.go", TestMultipleHunksBefore).
94+ After("main.go", TestMultipleHunksAfter).
95+ ChromaStyle(nil)
96+ }
97+
98+ LightModeFunc = func(dv *diffview.DiffView) *diffview.DiffView {
99+ return dv.
100+ Style(diffview.DefaultLightStyle()).
101+ ChromaStyle(styles.Get("catppuccin-latte"))
102+ }
103+ DarkModeFunc = func(dv *diffview.DiffView) *diffview.DiffView {
104+ return dv.
105+ Style(diffview.DefaultDarkStyle()).
106+ ChromaStyle(styles.Get("catppuccin-macchiato"))
107+ }
108+
109+ LayoutFuncs = TestFuncs{
110+ "Unified": UnifiedFunc,
111+ "Split": SplitFunc,
112+ }
113+ BehaviorFuncs = TestFuncs{
114+ "Default": DefaultFunc,
115+ "NoLineNumbers": NoLineNumbersFunc,
116+ "MultipleHunks": MultipleHunksFunc,
117+ "CustomContextLines": CustomContextLinesFunc,
118+ "Narrow": NarrowFunc,
119+ "SmallWidth": SmallWidthFunc,
120+ "LargeWidth": LargeWidthFunc,
121+ "NoSyntaxHighlight": NoSyntaxHighlightFunc,
122+ }
123+ ThemeFuncs = TestFuncs{
124+ "LightMode": LightModeFunc,
125+ "DarkMode": DarkModeFunc,
126+ }
127+)
128+
129+func TestDiffView(t *testing.T) {
130+ for layoutName, layoutFunc := range LayoutFuncs {
131+ t.Run(layoutName, func(t *testing.T) {
132+ for behaviorName, behaviorFunc := range BehaviorFuncs {
133+ t.Run(behaviorName, func(t *testing.T) {
134+ for themeName, themeFunc := range ThemeFuncs {
135+ t.Run(themeName, func(t *testing.T) {
136+ t.Parallel()
137+
138+ dv := diffview.New()
139+ dv = layoutFunc(dv)
140+ dv = themeFunc(dv)
141+ dv = behaviorFunc(dv)
142+
143+ output := dv.String()
144+ golden.RequireEqual(t, []byte(output))
145+
146+ switch behaviorName {
147+ case "SmallWidth":
148+ assertLineWidth(t, 40, output)
149+ case "LargeWidth":
150+ assertLineWidth(t, 120, output)
151+ }
152+ })
153+ }
154+ })
155+ }
156+ })
157+ }
158+}
159+
160+func TestDiffViewTabs(t *testing.T) {
161+ for layoutName, layoutFunc := range LayoutFuncs {
162+ t.Run(layoutName, func(t *testing.T) {
163+ t.Parallel()
164+
165+ dv := diffview.New().
166+ Before("main.go", TestTabsBefore).
167+ After("main.go", TestTabsAfter).
168+ Style(diffview.DefaultLightStyle()).
169+ ChromaStyle(styles.Get("catppuccin-latte"))
170+ dv = layoutFunc(dv)
171+
172+ output := dv.String()
173+ golden.RequireEqual(t, []byte(output))
174+ })
175+ }
176+}
177+
178+func TestDiffViewWidth(t *testing.T) {
179+ for layoutName, layoutFunc := range LayoutFuncs {
180+ t.Run(layoutName, func(t *testing.T) {
181+ for width := 1; width <= 110; width++ {
182+ if layoutName == "Unified" && width > 60 {
183+ continue
184+ }
185+
186+ t.Run(fmt.Sprintf("WidthOf%03d", width), func(t *testing.T) {
187+ t.Parallel()
188+
189+ dv := diffview.New().
190+ Before("main.go", TestMultipleHunksBefore).
191+ After("main.go", TestMultipleHunksAfter).
192+ Width(width).
193+ Style(diffview.DefaultLightStyle()).
194+ ChromaStyle(styles.Get("catppuccin-latte"))
195+ dv = layoutFunc(dv)
196+
197+ output := dv.String()
198+ golden.RequireEqual(t, []byte(output))
199+
200+ assertLineWidth(t, width, output)
201+ })
202+ }
203+ })
204+ }
205+}
206+
207+func TestDiffViewHeight(t *testing.T) {
208+ for layoutName, layoutFunc := range LayoutFuncs {
209+ t.Run(layoutName, func(t *testing.T) {
210+ for height := 1; height <= 20; height++ {
211+ t.Run(fmt.Sprintf("HeightOf%03d", height), func(t *testing.T) {
212+ t.Parallel()
213+
214+ dv := diffview.New().
215+ Before("main.go", TestMultipleHunksBefore).
216+ After("main.go", TestMultipleHunksAfter).
217+ Height(height).
218+ Style(diffview.DefaultLightStyle()).
219+ ChromaStyle(styles.Get("catppuccin-latte"))
220+ dv = layoutFunc(dv)
221+
222+ output := dv.String()
223+ golden.RequireEqual(t, []byte(output))
224+
225+ assertHeight(t, height, output)
226+ })
227+ }
228+ })
229+ }
230+}
231+
232+func TestDiffViewXOffset(t *testing.T) {
233+ for layoutName, layoutFunc := range LayoutFuncs {
234+ t.Run(layoutName, func(t *testing.T) {
235+ for xOffset := range 21 {
236+ t.Run(fmt.Sprintf("XOffsetOf%02d", xOffset), func(t *testing.T) {
237+ t.Parallel()
238+
239+ dv := diffview.New().
240+ Before("main.go", TestDefaultBefore).
241+ After("main.go", TestDefaultAfter).
242+ Style(diffview.DefaultLightStyle()).
243+ ChromaStyle(styles.Get("catppuccin-latte")).
244+ Width(60).
245+ XOffset(xOffset)
246+ dv = layoutFunc(dv)
247+
248+ output := dv.String()
249+ golden.RequireEqual(t, []byte(output))
250+
251+ assertLineWidth(t, 60, output)
252+ })
253+ }
254+ })
255+ }
256+}
257+
258+func TestDiffViewYOffset(t *testing.T) {
259+ for layoutName, layoutFunc := range LayoutFuncs {
260+ t.Run(layoutName, func(t *testing.T) {
261+ for yOffset := range 17 {
262+ t.Run(fmt.Sprintf("YOffsetOf%02d", yOffset), func(t *testing.T) {
263+ t.Parallel()
264+
265+ dv := diffview.New().
266+ Before("main.go", TestMultipleHunksBefore).
267+ After("main.go", TestMultipleHunksAfter).
268+ Style(diffview.DefaultLightStyle()).
269+ ChromaStyle(styles.Get("catppuccin-latte")).
270+ Height(5).
271+ YOffset(yOffset)
272+ dv = layoutFunc(dv)
273+
274+ output := dv.String()
275+ golden.RequireEqual(t, []byte(output))
276+
277+ assertHeight(t, 5, output)
278+ })
279+ }
280+ })
281+ }
282+}
283+
284+func assertLineWidth(t *testing.T, expected int, output string) {
285+ var lineWidth int
286+ for line := range strings.SplitSeq(output, "\n") {
287+ lineWidth = max(lineWidth, ansi.StringWidth(line))
288+ }
289+ if lineWidth != expected {
290+ t.Errorf("expected output width to be == %d, got %d", expected, lineWidth)
291+ }
292+}
293+
294+func assertHeight(t *testing.T, expected int, output string) {
295+ output = strings.TrimSuffix(output, "\n")
296+ lines := strings.Count(output, "\n") + 1
297+ if lines != expected {
298+ t.Errorf("expected output height to be == %d, got %d", expected, lines)
299+ }
300+}
internal/exp/diffview/split.gonew file+74
@@ -0,0 +1,74 @@
1+package diffview
2+
3+import (
4+ "slices"
5+
6+ "github.com/aymanbagabas/go-udiff"
7+ "github.com/charmbracelet/x/exp/slice"
8+)
9+
10+type splitHunk struct {
11+ fromLine int
12+ toLine int
13+ lines []*splitLine
14+}
15+
16+type splitLine struct {
17+ before *udiff.Line
18+ after *udiff.Line
19+}
20+
21+func hunkToSplit(h *udiff.Hunk) (sh splitHunk) {
22+ lines := slices.Clone(h.Lines)
23+ sh = splitHunk{
24+ fromLine: h.FromLine,
25+ toLine: h.ToLine,
26+ lines: make([]*splitLine, 0, len(lines)),
27+ }
28+
29+ for {
30+ var ul udiff.Line
31+ var ok bool
32+ ul, lines, ok = slice.Shift(lines)
33+ if !ok {
34+ break
35+ }
36+
37+ var sl splitLine
38+
39+ switch ul.Kind {
40+
41+ // For equal lines, add as is
42+ case udiff.Equal:
43+ sl.before = &ul
44+ sl.after = &ul
45+
46+ // For inserted lines, set after and keep before as nil
47+ case udiff.Insert:
48+ sl.before = nil
49+ sl.after = &ul
50+
51+ // For deleted lines, set before and loop over the next lines
52+ // searching for the equivalent after line.
53+ case udiff.Delete:
54+ sl.before = &ul
55+
56+ inner:
57+ for i, l := range lines {
58+ switch l.Kind {
59+ case udiff.Insert:
60+ var ll udiff.Line
61+ ll, lines, _ = slice.DeleteAt(lines, i)
62+ sl.after = &ll
63+ break inner
64+ case udiff.Equal:
65+ break inner
66+ }
67+ }
68+ }
69+
70+ sh.lines = append(sh.lines, &sl)
71+ }
72+
73+ return
74+}
internal/exp/diffview/style.gonew file+124
@@ -0,0 +1,124 @@
1+package diffview
2+
3+import (
4+ "github.com/charmbracelet/lipgloss/v2"
5+ "github.com/charmbracelet/x/exp/charmtone"
6+)
7+
8+// LineStyle defines the styles for a given line type in the diff view.
9+type LineStyle struct {
10+ LineNumber lipgloss.Style
11+ Symbol lipgloss.Style
12+ Code lipgloss.Style
13+}
14+
15+// Style defines the overall style for the diff view, including styles for
16+// different line types such as divider, missing, equal, insert, and delete
17+// lines.
18+type Style struct {
19+ DividerLine LineStyle
20+ MissingLine LineStyle
21+ EqualLine LineStyle
22+ InsertLine LineStyle
23+ DeleteLine LineStyle
24+}
25+
26+// DefaultLightStyle provides a default light theme style for the diff view.
27+func DefaultLightStyle() Style {
28+ return Style{
29+ DividerLine: LineStyle{
30+ LineNumber: lipgloss.NewStyle().
31+ Foreground(charmtone.Iron).
32+ Background(charmtone.Thunder),
33+ Code: lipgloss.NewStyle().
34+ Foreground(charmtone.Oyster).
35+ Background(charmtone.Anchovy),
36+ },
37+ MissingLine: LineStyle{
38+ LineNumber: lipgloss.NewStyle().
39+ Background(charmtone.Ash),
40+ Code: lipgloss.NewStyle().
41+ Background(charmtone.Ash),
42+ },
43+ EqualLine: LineStyle{
44+ LineNumber: lipgloss.NewStyle().
45+ Foreground(charmtone.Charcoal).
46+ Background(charmtone.Ash),
47+ Code: lipgloss.NewStyle().
48+ Foreground(charmtone.Pepper).
49+ Background(charmtone.Salt),
50+ },
51+ InsertLine: LineStyle{
52+ LineNumber: lipgloss.NewStyle().
53+ Foreground(charmtone.Turtle).
54+ Background(lipgloss.Color("#c8e6c9")),
55+ Symbol: lipgloss.NewStyle().
56+ Foreground(charmtone.Turtle).
57+ Background(lipgloss.Color("#e8f5e9")),
58+ Code: lipgloss.NewStyle().
59+ Foreground(charmtone.Pepper).
60+ Background(lipgloss.Color("#e8f5e9")),
61+ },
62+ DeleteLine: LineStyle{
63+ LineNumber: lipgloss.NewStyle().
64+ Foreground(charmtone.Cherry).
65+ Background(lipgloss.Color("#ffcdd2")),
66+ Symbol: lipgloss.NewStyle().
67+ Foreground(charmtone.Cherry).
68+ Background(lipgloss.Color("#ffebee")),
69+ Code: lipgloss.NewStyle().
70+ Foreground(charmtone.Pepper).
71+ Background(lipgloss.Color("#ffebee")),
72+ },
73+ }
74+}
75+
76+// DefaultDarkStyle provides a default dark theme style for the diff view.
77+func DefaultDarkStyle() Style {
78+ return Style{
79+ DividerLine: LineStyle{
80+ LineNumber: lipgloss.NewStyle().
81+ Foreground(charmtone.Smoke).
82+ Background(charmtone.Sapphire),
83+ Code: lipgloss.NewStyle().
84+ Foreground(charmtone.Smoke).
85+ Background(charmtone.Ox),
86+ },
87+ MissingLine: LineStyle{
88+ LineNumber: lipgloss.NewStyle().
89+ Background(charmtone.Charcoal),
90+ Code: lipgloss.NewStyle().
91+ Background(charmtone.Charcoal),
92+ },
93+ EqualLine: LineStyle{
94+ LineNumber: lipgloss.NewStyle().
95+ Foreground(charmtone.Ash).
96+ Background(charmtone.Charcoal),
97+ Code: lipgloss.NewStyle().
98+ Foreground(charmtone.Salt).
99+ Background(charmtone.Pepper),
100+ },
101+ InsertLine: LineStyle{
102+ LineNumber: lipgloss.NewStyle().
103+ Foreground(charmtone.Turtle).
104+ Background(lipgloss.Color("#293229")),
105+ Symbol: lipgloss.NewStyle().
106+ Foreground(charmtone.Turtle).
107+ Background(lipgloss.Color("#303a30")),
108+ Code: lipgloss.NewStyle().
109+ Foreground(charmtone.Salt).
110+ Background(lipgloss.Color("#303a30")),
111+ },
112+ DeleteLine: LineStyle{
113+ LineNumber: lipgloss.NewStyle().
114+ Foreground(charmtone.Cherry).
115+ Background(lipgloss.Color("#332929")),
116+ Symbol: lipgloss.NewStyle().
117+ Foreground(charmtone.Cherry).
118+ Background(lipgloss.Color("#3a3030")),
119+ Code: lipgloss.NewStyle().
120+ Foreground(charmtone.Salt).
121+ Background(lipgloss.Color("#3a3030")),
122+ },
123+ }
124+}
internal/exp/diffview/testdata/TestDefault.afternew file+10
@@ -0,0 +1,10 @@
1+package main
2+
3+import (
4+ "fmt"
5+)
6+
7+func main() {
8+ content := "Hello, world!"
9+ fmt.Println(content)
10+}
internal/exp/diffview/testdata/TestDefault.beforenew file+9
@@ -0,0 +1,9 @@
1+package main
2+
3+import (
4+ "fmt"
5+)
6+
7+func main() {
8+ fmt.Println("Hello, world!")
9+}
internal/exp/diffview/testdata/TestDiffView/Split/CustomContextLines/DarkMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …  @@ -1,13 +1,15 @@    …   
2+  1  package main   1  package main 
3+  2     2   
4+  3  import (   3  import ( 
5+  4   "fmt"   4   "fmt" 
6+       5 +  "strings" 
7+  5  )   6  ) 
8+  6     7   
9+  7  func main() {   8  func main() { 
10+  8   fmt.Println(getContent())   9   fmt.Println(getContent()) 
11+  9  }  10  } 
12+ 10    11   
13+ 11  func getContent() string {  12  func getContent() string { 
14+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
15+      14 +  return content 
16+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/CustomContextLines/LightMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …  @@ -1,13 +1,15 @@    …   
2+  1  package main   1  package main 
3+  2     2   
4+  3  import (   3  import ( 
5+  4   "fmt"   4   "fmt" 
6+       5 +  "strings" 
7+  5  )   6  ) 
8+  6     7   
9+  7  func main() {   8  func main() { 
10+  8   fmt.Println(getContent())   9   fmt.Println(getContent()) 
11+  9  }  10  } 
12+ 10    11   
13+ 11  func getContent() string {  12  func getContent() string { 
14+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
15+      14 +  return content 
16+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/Default/DarkMode.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  )   5  ) 
3+  6     6   
4+  7  func main() {   7  func main() { 
5+  8 -  fmt.Println("Hello, world!")   8 +  content := "Hello, world!" 
6+       9 +  fmt.Println(content) 
7+  9  }  10  } 
internal/exp/diffview/testdata/TestDiffView/Split/Default/LightMode.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  )   5  ) 
3+  6     6   
4+  7  func main() {   7  func main() { 
5+  8 -  fmt.Println("Hello, world!")   8 +  content := "Hello, world!" 
6+       9 +  fmt.Println(content) 
7+  9  }  10  } 
internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/DarkMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/LargeWidth/LightMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/MultipleHunks/DarkMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/MultipleHunks/LightMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/Narrow/DarkMode.goldennew file+4
@@ -0,0 +1,4 @@
1+ …  @@ -1,3 +1,3 @@   …   
2+ 1 - a  1 + d 
3+ 2 - b  2 + e 
4+ 3 - c  3 + f 
internal/exp/diffview/testdata/TestDiffView/Split/Narrow/LightMode.goldennew file+4
@@ -0,0 +1,4 @@
1+ …  @@ -1,3 +1,3 @@   …   
2+ 1 - a  1 + d 
3+ 2 - b  2 + e 
4+ 3 - c  3 + f 
internal/exp/diffview/testdata/TestDiffView/Split/NoLineNumbers/DarkMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ @@ -5,5 +5,6 @@    
2+ )  ) 
3+    
4+ func main() {  func main() { 
5+-  fmt.Println("Hello, world!") +  content := "Hello, world!" 
6+  +  fmt.Println(content) 
7+ }  } 
internal/exp/diffview/testdata/TestDiffView/Split/NoLineNumbers/LightMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ @@ -5,5 +5,6 @@    
2+ )  ) 
3+    
4+ func main() {  func main() { 
5+-  fmt.Println("Hello, world!") +  content := "Hello, world!" 
6+  +  fmt.Println(content) 
7+ }  } 
internal/exp/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/DarkMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4  "fmt"   4  "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/NoSyntaxHighlight/LightMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4  "fmt"   4  "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/DarkMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConte… 12  func getConte…
13+ 12 -  return "H… 13 +  content :…
14+      14 +  return co…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Split/SmallWidth/LightMode.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConte… 12  func getConte…
13+ 12 -  return "H… 13 +  content :…
14+      14 +  return co…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/CustomContextLines/DarkMode.goldennew file+17
@@ -0,0 +1,17 @@
1+  …   …  @@ -1,13 +1,15 @@  
2+  1   1  package main 
3+  2   2   
4+  3   3  import ( 
5+  4   4   "fmt" 
6+     5 +  "strings" 
7+  5   6  ) 
8+  6   7   
9+  7   8  func main() { 
10+  8   9   fmt.Println(getContent()) 
11+  9  10  } 
12+ 10  11   
13+ 11  12  func getContent() string { 
14+ 12    -  return "Hello, world!" 
15+    13 +  content := strings.ToUpper("Hello, World!") 
16+    14 +  return content 
17+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/CustomContextLines/LightMode.goldennew file+17
@@ -0,0 +1,17 @@
1+  …   …  @@ -1,13 +1,15 @@  
2+  1   1  package main 
3+  2   2   
4+  3   3  import ( 
5+  4   4   "fmt" 
6+     5 +  "strings" 
7+  5   6  ) 
8+  6   7   
9+  7   8  func main() { 
10+  8   9   fmt.Println(getContent()) 
11+  9  10  } 
12+ 10  11   
13+ 11  12  func getContent() string { 
14+ 12    -  return "Hello, world!" 
15+    13 +  content := strings.ToUpper("Hello, World!") 
16+    14 +  return content 
17+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/Default/DarkMode.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  ) 
3+  6   6   
4+  7   7  func main() { 
5+  8    -  fmt.Println("Hello, world!") 
6+     8 +  content := "Hello, world!" 
7+     9 +  fmt.Println(content) 
8+  9  10  } 
internal/exp/diffview/testdata/TestDiffView/Unified/Default/LightMode.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  ) 
3+  6   6   
4+  7   7  func main() { 
5+  8    -  fmt.Println("Hello, world!") 
6+     8 +  content := "Hello, world!" 
7+     9 +  fmt.Println(content) 
8+  9  10  } 
internal/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/DarkMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/LargeWidth/LightMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/MultipleHunks/DarkMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/MultipleHunks/LightMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/Narrow/DarkMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ …  …  @@ -1,3 +1,3 @@  
2+ 1    - a 
3+ 2    - b 
4+ 3    - c 
5+    1 + d 
6+    2 + e 
7+    3 + f 
internal/exp/diffview/testdata/TestDiffView/Unified/Narrow/LightMode.goldennew file+7
@@ -0,0 +1,7 @@
1+ …  …  @@ -1,3 +1,3 @@  
2+ 1    - a 
3+ 2    - b 
4+ 3    - c 
5+    1 + d 
6+    2 + e 
7+    3 + f 
internal/exp/diffview/testdata/TestDiffView/Unified/NoLineNumbers/DarkMode.goldennew file+8
@@ -0,0 +1,8 @@
1+ @@ -5,5 +5,6 @@  
2+ ) 
3+  
4+ func main() { 
5+-  fmt.Println("Hello, world!") 
6++  content := "Hello, world!" 
7++  fmt.Println(content) 
8+ } 
internal/exp/diffview/testdata/TestDiffView/Unified/NoLineNumbers/LightMode.goldennew file+8
@@ -0,0 +1,8 @@
1+ @@ -5,5 +5,6 @@  
2+ ) 
3+  
4+ func main() { 
5+-  fmt.Println("Hello, world!") 
6++  content := "Hello, world!" 
7++  fmt.Println(content) 
8+ } 
internal/exp/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/DarkMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4  "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/NoSyntaxHighlight/LightMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4  "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/DarkMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUppe…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffView/Unified/SmallWidth/LightMode.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUppe…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf001.goldennew file+1
@@ -0,0 +1 @@
1+  …  @@ -2,6 +2,7 @@    …   
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf002.goldennew file+2
@@ -0,0 +1,2 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf003.goldennew file+3
@@ -0,0 +1,3 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf004.goldennew file+4
@@ -0,0 +1,4 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf005.goldennew file+5
@@ -0,0 +1,5 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf006.goldennew file+6
@@ -0,0 +1,6 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf007.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf008.goldennew file+8
@@ -0,0 +1,8 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf009.goldennew file+9
@@ -0,0 +1,9 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf010.goldennew file+10
@@ -0,0 +1,10 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf011.goldennew file+11
@@ -0,0 +1,11 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf012.goldennew file+12
@@ -0,0 +1,12 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf013.goldennew file+13
@@ -0,0 +1,13 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf014.goldennew file+14
@@ -0,0 +1,14 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf015.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf016.goldennew file+16
@@ -0,0 +1,16 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
16+          
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf017.goldennew file+17
@@ -0,0 +1,17 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
16+          
17+          
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf018.goldennew file+18
@@ -0,0 +1,18 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
16+          
17+          
18+          
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf019.goldennew file+19
@@ -0,0 +1,19 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
16+          
17+          
18+          
19+          
internal/exp/diffview/testdata/TestDiffViewHeight/Split/HeightOf020.goldennew file+20
@@ -0,0 +1,20 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
16+          
17+          
18+          
19+          
20+          
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf001.goldennew file+1
@@ -0,0 +1 @@
1+  …   …  @@ -2,6 +2,7 @@  
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf002.goldennew file+2
@@ -0,0 +1,2 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf003.goldennew file+3
@@ -0,0 +1,3 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf004.goldennew file+4
@@ -0,0 +1,4 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf005.goldennew file+5
@@ -0,0 +1,5 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf006.goldennew file+6
@@ -0,0 +1,6 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf007.goldennew file+7
@@ -0,0 +1,7 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf008.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf009.goldennew file+9
@@ -0,0 +1,9 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf010.goldennew file+10
@@ -0,0 +1,10 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf011.goldennew file+11
@@ -0,0 +1,11 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf012.goldennew file+12
@@ -0,0 +1,12 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf013.goldennew file+13
@@ -0,0 +1,13 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf014.goldennew file+14
@@ -0,0 +1,14 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf015.goldennew file+15
@@ -0,0 +1,15 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf016.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf017.goldennew file+17
@@ -0,0 +1,17 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
17+        
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf018.goldennew file+18
@@ -0,0 +1,18 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
17+        
18+        
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf019.goldennew file+19
@@ -0,0 +1,19 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
17+        
18+        
19+        
internal/exp/diffview/testdata/TestDiffViewHeight/Unified/HeightOf020.goldennew file+20
@@ -0,0 +1,20 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
17+        
18+        
19+        
20+        
internal/exp/diffview/testdata/TestDiffViewTabs/Split.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewTabs/Unified.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf001.goldennew file+15
@@ -0,0 +1,15 @@
1+ 
2+ 
3+ 
4+ 
5+ 
6+ 
7+ 
8+ 
9+ 
10+ 
11+ 
12+ 
13+ 
14+ 
15+ 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf002.goldennew file+15
@@ -0,0 +1,15 @@
1+  
2+  
3+  
4+  
5+  
6+  
7+  
8+  
9+  
10+  
11+ 1
12+ 1
13+ 1
14+  
15+ 1
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf003.goldennew file+15
@@ -0,0 +1,15 @@
1+  …
2+  2
3+  3
4+  4
5+  
6+  5
7+  6
8+  7
9+  …
10+  9
11+ 10
12+ 11
13+ 12
14+  
15+ 13
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf004.goldennew file+15
@@ -0,0 +1,15 @@
1+  … 
2+  2 
3+  3 
4+  4 
5+   
6+  5 
7+  6 
8+  7 
9+  … 
10+  9 
11+ 10 
12+ 11 
13+ 12 
14+   
15+ 13 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf005.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  
2+  2  
3+  3  
4+  4  
5+    
6+  5  
7+  6  
8+  7  
9+  …  
10+  9  
11+ 10  
12+ 11  
13+ 12 -
14+    
15+ 13  
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf006.goldennew file+15
@@ -0,0 +1,15 @@
1+  …   
2+  2  
3+  3  
4+  4  
5+    
6+  5  
7+  6  
8+  7  
9+  …   
10+  9  
11+ 10  
12+ 11  
13+ 12 - 
14+    
15+ 13  
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf007.goldennew file+15
@@ -0,0 +1,15 @@
1+  …   …
2+  2   
3+  3   
4+  4   
5+     
6+  5   
7+  6   
8+  7   
9+  …   …
10+  9   
11+ 10   
12+ 11   
13+ 12 -  
14+     
15+ 13   
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf008.goldennew file+15
@@ -0,0 +1,15 @@
1+  …   … 
2+  2    
3+  3    
4+  4    
5+      
6+  5    
7+  6    
8+  7    
9+  …   … 
10+  9   1
11+ 10   1
12+ 11   1
13+ 12 -  1
14+     1
15+ 13   1
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf009.goldennew file+15
@@ -0,0 +1,15 @@
1+  … …  … 
2+  2    2 
3+  3    3 
4+  4    4 
5+      5 
6+  5    6 
7+  6    7 
8+  7    8 
9+  … …  … 
10+  9   10 
11+ 10   11 
12+ 11   12 
13+ 12 - 13 
14+     14 
15+ 13   15 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf010.goldennew file+15
@@ -0,0 +1,15 @@
1+  … …  …  
2+  2    2  
3+  3    3  
4+  4    4  
5+      5 +
6+  5    6  
7+  6    7  
8+  7    8  
9+  … …  …  
10+  9   10  
11+ 10   11  
12+ 11   12  
13+ 12 - 13 +
14+     14 +
15+ 13   15  
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf011.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  …  …  
2+  2    2  
3+  3    3  
4+  4    4  
5+      5 +
6+  5    6  
7+  6    7  
8+  7    8  
9+  …  …  …  
10+  9   10  
11+ 10   11  
12+ 11   12  
13+ 12 -  13 +
14+     14 +
15+ 13   15  
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf012.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  …  …   
2+  2    2  
3+  3    3  
4+  4    4  
5+      5 + 
6+  5    6  
7+  6    7  
8+  7    8  
9+  …  …  …   
10+  9   10  
11+ 10   11  
12+ 11   12  
13+ 12 -  13 + 
14+     14 + 
15+ 13   15  
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf013.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  …  …   
2+  2    2   
3+  3    3   
4+  4    4   
5+      5 +  
6+  5    6   
7+  6    7   
8+  7    8   
9+  …  …  …   
10+  9   10   
11+ 10   11   
12+ 11   12   
13+ 12 -  13 +  
14+     14 +  
15+ 13   15   
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf014.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  …  …   
2+  2     2   
3+  3  …  3  …
4+  4  …  4  …
5+       5 + …
6+  5  )  6  )
7+  6     7   
8+  7  …  8  …
9+  …  …  …   
10+  9  } 10  }
11+ 10    11   
12+ 11  … 12  …
13+ 12 - … 13 + …
14+      14 + …
15+ 13  } 15  }
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf015.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  …  …   
2+  2     2   
3+  3  …  3  … 
4+  4  …  4  … 
5+       5 + … 
6+  5  )  6  ) 
7+  6     7   
8+  7  …  8  … 
9+  …  …  …   
10+  9  } 10  } 
11+ 10    11   
12+ 11  … 12  … 
13+ 12 - … 13 + … 
14+      14 + … 
15+ 13  } 15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf016.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @…  …   
2+  2     2   
3+  3  i…  3  i…
4+  4   …  4   …
5+       5 +  …
6+  5  )   6  ) 
7+  6     7   
8+  7  f…  8  f…
9+  …  @…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  f… 12  f…
13+ 12 -  … 13 +  …
14+      14 +  …
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf017.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @…  …   
2+  2     2   
3+  3  i…  3  i… 
4+  4   …  4   … 
5+       5 +  … 
6+  5  )   6  ) 
7+  6     7   
8+  7  f…  8  f… 
9+  …  @…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  f… 12  f… 
13+ 12 -  … 13 +  … 
14+      14 +  … 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf018.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@…  …   
2+  2     2   
3+  3  im…  3  im…
4+  4   …  4   …
5+       5 +  …
6+  5  )   6  ) 
7+  6     7   
8+  7  fu…  8  fu…
9+  …  @@…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  fu… 12  fu…
13+ 12 -  … 13 +  …
14+      14 +  …
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf019.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@…  …   
2+  2     2   
3+  3  im…  3  im… 
4+  4   …  4   … 
5+       5 +  … 
6+  5  )   6  ) 
7+  6     7   
8+  7  fu…  8  fu… 
9+  …  @@…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  fu… 12  fu… 
13+ 12 -  … 13 +  … 
14+      14 +  … 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf020.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ …  …   
2+  2     2   
3+  3  imp…  3  imp…
4+  4   …  4   …
5+       5 +  …
6+  5  )   6  ) 
7+  6     7   
8+  7  fun…  8  fun…
9+  …  @@ …  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  fun… 12  fun…
13+ 12 -  … 13 +  …
14+      14 +  …
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf021.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ …  …   
2+  2     2   
3+  3  imp…  3  imp… 
4+  4   …  4   … 
5+       5 +  … 
6+  5  )   6  ) 
7+  6     7   
8+  7  fun…  8  fun… 
9+  …  @@ …  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  fun… 12  fun… 
13+ 12 -  … 13 +  … 
14+      14 +  … 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf022.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -…  …   
2+  2     2   
3+  3  impo…  3  impo…
4+  4   …  4   …
5+       5 +  …
6+  5  )   6  ) 
7+  6     7   
8+  7  func…  8  func…
9+  …  @@ -…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func… 12  func…
13+ 12 -  … 13 +  …
14+      14 +  …
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf023.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -…  …   
2+  2     2   
3+  3  impo…  3  impo… 
4+  4   …  4   … 
5+       5 +  … 
6+  5  )   6  ) 
7+  6     7   
8+  7  func…  8  func… 
9+  …  @@ -…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func… 12  func… 
13+ 12 -  … 13 +  … 
14+      14 +  … 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf024.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2…  …   
2+  2     2   
3+  3  impor…  3  impor…
4+  4   "…  4   "…
5+       5 +  "…
6+  5  )   6  ) 
7+  6     7   
8+  7  func …  8  func …
9+  …  @@ -9…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func … 12  func …
13+ 12 -  r… 13 +  c…
14+      14 +  r…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf025.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2…  …   
2+  2     2   
3+  3  impor…  3  impor… 
4+  4   "…  4   "… 
5+       5 +  "… 
6+  5  )   6  ) 
7+  6     7   
8+  7  func …  8  func … 
9+  …  @@ -9…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func … 12  func … 
13+ 12 -  r… 13 +  c… 
14+      14 +  r… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf026.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,…  …   
2+  2     2   
3+  3  import…  3  import…
4+  4   "f…  4   "f…
5+       5 +  "s…
6+  5  )   6  ) 
7+  6     7   
8+  7  func m…  8  func m…
9+  …  @@ -9,…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func g… 12  func g…
13+ 12 -  re… 13 +  co…
14+      14 +  re…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf027.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,…  …   
2+  2     2   
3+  3  import…  3  import… 
4+  4   "f…  4   "f… 
5+       5 +  "s… 
6+  5  )   6  ) 
7+  6     7   
8+  7  func m…  8  func m… 
9+  …  @@ -9,…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func g… 12  func g… 
13+ 12 -  re… 13 +  co… 
14+      14 +  re… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf028.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6…  …   
2+  2     2   
3+  3  import (  3  import (
4+  4   "fm…  4   "fm…
5+       5 +  "st…
6+  5  )   6  ) 
7+  6     7   
8+  7  func ma…  8  func ma…
9+  …  @@ -9,5…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func ge… 12  func ge…
13+ 12 -  ret… 13 +  con…
14+      14 +  ret…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf029.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6…  …   
2+  2     2   
3+  3  import (  3  import ( 
4+  4   "fm…  4   "fm… 
5+       5 +  "st… 
6+  5  )   6  ) 
7+  6     7   
8+  7  func ma…  8  func ma… 
9+  …  @@ -9,5…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func ge… 12  func ge… 
13+ 12 -  ret… 13 +  con… 
14+      14 +  ret… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf030.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"  4   "fmt"
5+       5 +  "str…
6+  5  )   6  ) 
7+  6     7   
8+  7  func mai…  8  func mai…
9+  …  @@ -9,5 …  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func get… 12  func get…
13+ 12 -  retu… 13 +  cont…
14+      14 +  retu…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf031.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"  4   "fmt" 
5+       5 +  "str… 
6+  5  )   6  ) 
7+  6     7   
8+  7  func mai…  8  func mai… 
9+  …  @@ -9,5 …  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func get… 12  func get… 
13+ 12 -  retu… 13 +  cont… 
14+      14 +  retu… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf032.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "stri…
6+  5  )   6  ) 
7+  6     7   
8+  7  func main…  8  func main…
9+  …  @@ -9,5 +…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getC… 12  func getC…
13+ 12 -  retur… 13 +  conte…
14+      14 +  retur…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf033.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "stri… 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main…  8  func main… 
9+  …  @@ -9,5 +…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getC… 12  func getC… 
13+ 12 -  retur… 13 +  conte… 
14+      14 +  retur… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf034.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strin…
6+  5  )   6  ) 
7+  6     7   
8+  7  func main(…  8  func main(…
9+  …  @@ -9,5 +1…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getCo… 12  func getCo…
13+ 12 -  return… 13 +  conten…
14+      14 +  return…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf035.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strin… 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main(…  8  func main(… 
9+  …  @@ -9,5 +1…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getCo… 12  func getCo… 
13+ 12 -  return… 13 +  conten… 
14+      14 +  return… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf036.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "string…
6+  5  )   6  ) 
7+  6     7   
8+  7  func main()…  8  func main()…
9+  …  @@ -9,5 +10…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getCon… 12  func getCon…
13+ 12 -  return … 13 +  content…
14+      14 +  return …
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf037.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "string… 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main()…  8  func main()… 
9+  …  @@ -9,5 +10…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getCon… 12  func getCon… 
13+ 12 -  return … 13 +  content… 
14+      14 +  return … 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf038.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings"
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {  8  func main() {
9+  …  @@ -9,5 +10,…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getCont… 12  func getCont…
13+ 12 -  return "… 13 +  content …
14+      14 +  return c…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf039.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {  8  func main() { 
9+  …  @@ -9,5 +10,…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getCont… 12  func getCont… 
13+ 12 -  return "… 13 +  content … 
14+      14 +  return c… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf040.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConte… 12  func getConte…
13+ 12 -  return "H… 13 +  content :…
14+      14 +  return co…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf041.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 …  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConte… 12  func getConte… 
13+ 12 -  return "H… 13 +  content :… 
14+      14 +  return co… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf042.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 …  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConten… 12  func getConten…
13+ 12 -  return "He… 13 +  content :=…
14+      14 +  return con…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf043.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @…  …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 …  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getConten… 12  func getConten… 
13+ 12 -  return "He… 13 +  content :=… 
14+      14 +  return con… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf044.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@   …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent… 12  func getContent…
13+ 12 -  return "Hel… 13 +  content := …
14+      14 +  return cont…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf045.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@   …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @…  …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent… 12  func getContent… 
13+ 12 -  return "Hel… 13 +  content := … 
14+      14 +  return cont… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf046.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@   …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent(… 12  func getContent(…
13+ 12 -  return "Hell… 13 +  content := s…
14+      14 +  return conte…
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf047.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@   …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent(… 12  func getContent(… 
13+ 12 -  return "Hell… 13 +  content := s… 
14+      14 +  return conte… 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf048.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent()… 12  func getContent()…
13+ 12 -  return "Hello… 13 +  content := st…
14+      14 +  return content
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf049.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent()… 12  func getContent()… 
13+ 12 -  return "Hello… 13 +  content := st… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf050.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() … 12  func getContent() …
13+ 12 -  return "Hello,… 13 +  content := str…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf051.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() … 12  func getContent() … 
13+ 12 -  return "Hello,… 13 +  content := str… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf052.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() s… 12  func getContent() s…
13+ 12 -  return "Hello, … 13 +  content := stri…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf053.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() s… 12  func getContent() s… 
13+ 12 -  return "Hello, … 13 +  content := stri… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf054.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() st… 12  func getContent() st…
13+ 12 -  return "Hello, w… 13 +  content := strin…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf055.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() st… 12  func getContent() st… 
13+ 12 -  return "Hello, w… 13 +  content := strin… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf056.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() str… 12  func getContent() str…
13+ 12 -  return "Hello, wo… 13 +  content := string…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf057.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() str… 12  func getContent() str… 
13+ 12 -  return "Hello, wo… 13 +  content := string… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf058.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() stri… 12  func getContent() stri…
13+ 12 -  return "Hello, wor… 13 +  content := strings…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf059.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() stri… 12  func getContent() stri… 
13+ 12 -  return "Hello, wor… 13 +  content := strings… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf060.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() strin… 12  func getContent() strin…
13+ 12 -  return "Hello, worl… 13 +  content := strings.…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf061.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() strin… 12  func getContent() strin… 
13+ 12 -  return "Hello, worl… 13 +  content := strings.… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf062.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string… 12  func getContent() string…
13+ 12 -  return "Hello, world… 13 +  content := strings.T…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf063.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string… 12  func getContent() string… 
13+ 12 -  return "Hello, world… 13 +  content := strings.T… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf064.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string { 12  func getContent() string {
13+ 12 -  return "Hello, world!" 13 +  content := strings.To…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf065.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string { 12  func getContent() string { 
13+ 12 -  return "Hello, world!" 13 +  content := strings.To… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf066.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToU…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf067.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToU… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf068.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUp…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf069.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUp… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf070.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpp…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf071.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpp… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf072.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUppe…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf073.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUppe… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf074.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf075.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf076.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper(…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf077.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper(… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf078.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf079.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf080.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("H…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf081.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("H… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf082.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("He…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf083.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("He… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf084.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hel…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf085.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hel… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf086.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hell…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf087.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hell… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf088.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf089.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf090.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello,…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf091.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello,… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf092.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, …
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf093.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, … 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf094.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, W…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf095.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, W… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf096.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wo…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf097.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wo… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf098.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wor…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf099.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Wor… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf100.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Worl…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf101.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, Worl… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf102.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf103.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf104.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!…
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf105.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!… 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf106.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf107.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf108.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf109.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Split/WidthOf110.goldennew file+15
@@ -0,0 +1,15 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+       5 +  "strings" 
6+  5  )   6  ) 
7+  6     7   
8+  7  func main() {   8  func main() { 
9+  …  @@ -9,5 +10,6 @@    …   
10+  9  }  10  } 
11+ 10    11   
12+ 11  func getContent() string {  12  func getContent() string { 
13+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
14+      14 +  return content 
15+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf001.goldennew file+16
@@ -0,0 +1,16 @@
1+ 
2+ 
3+ 
4+ 
5+ 
6+ 
7+ 
8+ 
9+ 
10+ 
11+ 
12+ 
13+ 
14+ 
15+ 
16+ 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf002.goldennew file+16
@@ -0,0 +1,16 @@
1+  
2+  
3+  
4+  
5+  
6+  
7+  
8+  
9+  
10+  
11+ 1
12+ 1
13+ 1
14+  
15+  
16+ 1
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf003.goldennew file+16
@@ -0,0 +1,16 @@
1+  …
2+  2
3+  3
4+  4
5+  
6+  5
7+  6
8+  7
9+  …
10+  9
11+ 10
12+ 11
13+ 12
14+  
15+  
16+ 13
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf004.goldennew file+16
@@ -0,0 +1,16 @@
1+  … 
2+  2 
3+  3 
4+  4 
5+   
6+  5 
7+  6 
8+  7 
9+  … 
10+  9 
11+ 10 
12+ 11 
13+ 12 
14+   
15+   
16+ 13 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf005.goldennew file+16
@@ -0,0 +1,16 @@
1+  …  
2+  2  
3+  3  
4+  4  
5+    
6+  5  
7+  6  
8+  7  
9+  …  
10+  9  
11+ 10  
12+ 11  
13+ 12  
14+    
15+    
16+ 13  
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf006.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   
2+  2   
3+  3   
4+  4   
5+     
6+  5   
7+  6   
8+  7   
9+  …   
10+  9  1
11+ 10  1
12+ 11  1
13+ 12   
14+    1
15+    1
16+ 13  1
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf007.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …
2+  2   2
3+  3   3
4+  4   4
5+     5
6+  5   6
7+  6   7
8+  7   8
9+  …   …
10+  9  10
11+ 10  11
12+ 11  12
13+ 12   
14+    13
15+    14
16+ 13  15
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf008.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   … 
2+  2   2 
3+  3   3 
4+  4   4 
5+     5 
6+  5   6 
7+  6   7 
8+  7   8 
9+  …   … 
10+  9  10 
11+ 10  11 
12+ 11  12 
13+ 12    
14+    13 
15+    14 
16+ 13  15 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf009.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   … …
2+  2   2  
3+  3   3  
4+  4   4  
5+     5 +
6+  5   6  
7+  6   7  
8+  7   8  
9+  …   … …
10+  9  10  
11+ 10  11  
12+ 11  12  
13+ 12    -
14+    13 +
15+    14 +
16+ 13  15  
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf010.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  …
2+  2   2  
3+  3   3  
4+  4   4  
5+     5 + 
6+  5   6  
7+  6   7  
8+  7   8  
9+  …   …  …
10+  9  10  
11+ 10  11  
12+ 11  12  
13+ 12    - 
14+    13 + 
15+    14 + 
16+ 13  15  
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf011.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  …
2+  2   2   
3+  3   3  …
4+  4   4  …
5+     5 + …
6+  5   6  )
7+  6   7   
8+  7   8  …
9+  …   …  …
10+  9  10  }
11+ 10  11   
12+ 11  12  …
13+ 12    - …
14+    13 + …
15+    14 + …
16+ 13  15  }
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf012.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @…
2+  2   2   
3+  3   3  i…
4+  4   4   …
5+     5 +  …
6+  5   6  ) 
7+  6   7   
8+  7   8  f…
9+  …   …  @…
10+  9  10  } 
11+ 10  11   
12+ 11  12  f…
13+ 12    -  …
14+    13 +  …
15+    14 +  …
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf013.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@…
2+  2   2   
3+  3   3  im…
4+  4   4   …
5+     5 +  …
6+  5   6  ) 
7+  6   7   
8+  7   8  fu…
9+  …   …  @@…
10+  9  10  } 
11+ 10  11   
12+ 11  12  fu…
13+ 12    -  …
14+    13 +  …
15+    14 +  …
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf014.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ …
2+  2   2   
3+  3   3  imp…
4+  4   4   …
5+     5 +  …
6+  5   6  ) 
7+  6   7   
8+  7   8  fun…
9+  …   …  @@ …
10+  9  10  } 
11+ 10  11   
12+ 11  12  fun…
13+ 12    -  …
14+    13 +  …
15+    14 +  …
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf015.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -…
2+  2   2   
3+  3   3  impo…
4+  4   4   …
5+     5 +  …
6+  5   6  ) 
7+  6   7   
8+  7   8  func…
9+  …   …  @@ -…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func…
13+ 12    -  …
14+    13 +  …
15+    14 +  …
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf016.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2…
2+  2   2   
3+  3   3  impor…
4+  4   4   "…
5+     5 +  "…
6+  5   6  ) 
7+  6   7   
8+  7   8  func …
9+  …   …  @@ -9…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func …
13+ 12    -  r…
14+    13 +  c…
15+    14 +  r…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf017.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,…
2+  2   2   
3+  3   3  import…
4+  4   4   "f…
5+     5 +  "s…
6+  5   6  ) 
7+  6   7   
8+  7   8  func m…
9+  …   …  @@ -9,…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func g…
13+ 12    -  re…
14+    13 +  co…
15+    14 +  re…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf018.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6…
2+  2   2   
3+  3   3  import (
4+  4   4   "fm…
5+     5 +  "st…
6+  5   6  ) 
7+  6   7   
8+  7   8  func ma…
9+  …   …  @@ -9,5…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func ge…
13+ 12    -  ret…
14+    13 +  con…
15+    14 +  ret…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf019.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 …
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt"
5+     5 +  "str…
6+  5   6  ) 
7+  6   7   
8+  7   8  func mai…
9+  …   …  @@ -9,5 …
10+  9  10  } 
11+ 10  11   
12+ 11  12  func get…
13+ 12    -  retu…
14+    13 +  cont…
15+    14 +  retu…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf020.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +…
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "stri…
6+  5   6  ) 
7+  6   7   
8+  7   8  func main…
9+  …   …  @@ -9,5 +…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getC…
13+ 12    -  retur…
14+    13 +  conte…
15+    14 +  retur…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf021.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2…
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strin…
6+  5   6  ) 
7+  6   7   
8+  7   8  func main(…
9+  …   …  @@ -9,5 +1…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getCo…
13+ 12    -  return…
14+    13 +  conten…
15+    14 +  return…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf022.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,…
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "string…
6+  5   6  ) 
7+  6   7   
8+  7   8  func main()…
9+  …   …  @@ -9,5 +10…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getCon…
13+ 12    -  return …
14+    13 +  content…
15+    14 +  return …
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf023.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7…
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings"
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() {
9+  …   …  @@ -9,5 +10,…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getCont…
13+ 12    -  return "…
14+    13 +  content …
15+    14 +  return c…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf024.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 …
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getConte…
13+ 12    -  return "H…
14+    13 +  content :…
15+    14 +  return co…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf025.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @…
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 …
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getConten…
13+ 12    -  return "He…
14+    13 +  content :=…
15+    14 +  return con…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf026.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@ 
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @…
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent…
13+ 12    -  return "Hel…
14+    13 +  content := …
15+    14 +  return cont…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf027.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@ 
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent(…
13+ 12    -  return "Hell…
14+    13 +  content := s…
15+    14 +  return conte…
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf028.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent()…
13+ 12    -  return "Hello…
14+    13 +  content := st…
15+    14 +  return content
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf029.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() …
13+ 12    -  return "Hello,…
14+    13 +  content := str…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf030.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() s…
13+ 12    -  return "Hello, …
14+    13 +  content := stri…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf031.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() st…
13+ 12    -  return "Hello, w…
14+    13 +  content := strin…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf032.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() str…
13+ 12    -  return "Hello, wo…
14+    13 +  content := string…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf033.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() stri…
13+ 12    -  return "Hello, wor…
14+    13 +  content := strings…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf034.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() strin…
13+ 12    -  return "Hello, worl…
14+    13 +  content := strings.…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf035.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string…
13+ 12    -  return "Hello, world…
14+    13 +  content := strings.T…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf036.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string {
13+ 12    -  return "Hello, world!"
14+    13 +  content := strings.To…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf037.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToU…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf038.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUp…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf039.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpp…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf040.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUppe…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf041.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf042.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper(…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf043.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf044.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("H…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf045.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("He…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf046.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hel…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf047.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hell…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf048.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf049.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello,…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf050.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, …
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf051.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, W…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf052.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, Wo…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf053.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, Wor…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf054.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, Worl…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf055.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf056.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!…
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf057.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!")
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf058.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf059.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewWidth/Unified/WidthOf060.goldennew file+16
@@ -0,0 +1,16 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+     5 +  "strings" 
6+  5   6  ) 
7+  6   7   
8+  7   8  func main() { 
9+  …   …  @@ -9,5 +10,6 @@  
10+  9  10  } 
11+ 10  11   
12+ 11  12  func getContent() string { 
13+ 12    -  return "Hello, world!" 
14+    13 +  content := strings.ToUpper("Hello, World!") 
15+    14 +  return content 
16+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf00.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  )   5  ) 
3+  6     6   
4+  7  func main() {   7  func main() { 
5+  8 -  fmt.Println("Hello,…  8 +  content := "Hello, …
6+       9 +  fmt.Println(content)
7+  9  }  10  } 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf01.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …unc main() {   7  …unc main() { 
5+  8 -… fmt.Println("Hello, …  8 +… content := "Hello, w…
6+       9 +… fmt.Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf02.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …nc main() {   7  …nc main() { 
5+  8 -… fmt.Println("Hello, w…  8 +… content := "Hello, wo…
6+       9 +… fmt.Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf03.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …c main() {   7  …c main() { 
5+  8 -… fmt.Println("Hello, wo…  8 +… content := "Hello, wor…
6+       9 +… fmt.Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf04.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  … main() {   7  … main() { 
5+  8 -…fmt.Println("Hello, wor…  8 +…content := "Hello, worl…
6+       9 +…fmt.Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf05.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …main() {   7  …main() { 
5+  8 -…mt.Println("Hello, worl…  8 +…ontent := "Hello, world…
6+       9 +…mt.Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf06.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …ain() {   7  …ain() { 
5+  8 -…t.Println("Hello, world…  8 +…ntent := "Hello, world!"
6+       9 +…t.Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf07.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …in() {   7  …in() { 
5+  8 -….Println("Hello, world!…  8 +…tent := "Hello, world!" 
6+       9 +….Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf08.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …n() {   7  …n() { 
5+  8 -…Println("Hello, world!")  8 +…ent := "Hello, world!" 
6+       9 +…Println(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf09.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …() {   7  …() { 
5+  8 -…rintln("Hello, world!")   8 +…nt := "Hello, world!" 
6+       9 +…rintln(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf10.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …) {   7  …) { 
5+  8 -…intln("Hello, world!")   8 +…t := "Hello, world!" 
6+       9 +…intln(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf11.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  … {   7  … { 
5+  8 -…ntln("Hello, world!")   8 +… := "Hello, world!" 
6+       9 +…ntln(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf12.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …{   7  …{ 
5+  8 -…tln("Hello, world!")   8 +…:= "Hello, world!" 
6+       9 +…tln(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf13.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…ln("Hello, world!")   8 +…= "Hello, world!" 
6+       9 +…ln(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf14.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…n("Hello, world!")   8 +… "Hello, world!" 
6+       9 +…n(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf15.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…("Hello, world!")   8 +…"Hello, world!" 
6+       9 +…(content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf16.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…"Hello, world!")   8 +…Hello, world!" 
6+       9 +…content) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf17.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…Hello, world!")   8 +…ello, world!" 
6+       9 +…ontent) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf18.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…ello, world!")   8 +…llo, world!" 
6+       9 +…ntent) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf19.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…llo, world!")   8 +…lo, world!" 
6+       9 +…tent) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Split/XOffsetOf20.goldennew file+7
@@ -0,0 +1,7 @@
1+  …  @@ -5,5 +5,6 @@    …   
2+  5  …   5  … 
3+  6     6   
4+  7  …   7  … 
5+  8 -…lo, world!")   8 +…o, world!" 
6+       9 +…ent) 
7+  9  …  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf00.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  ) 
3+  6   6   
4+  7   7  func main() { 
5+  8    -  fmt.Println("Hello, world!") 
6+     8 +  content := "Hello, world!" 
7+     9 +  fmt.Println(content) 
8+  9  10  } 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …unc main() { 
5+  8    -… fmt.Println("Hello, world!") 
6+     8 +… content := "Hello, world!" 
7+     9 +… fmt.Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …nc main() { 
5+  8    -… fmt.Println("Hello, world!") 
6+     8 +… content := "Hello, world!" 
7+     9 +… fmt.Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …c main() { 
5+  8    -… fmt.Println("Hello, world!") 
6+     8 +… content := "Hello, world!" 
7+     9 +… fmt.Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … main() { 
5+  8    -…fmt.Println("Hello, world!") 
6+     8 +…content := "Hello, world!" 
7+     9 +…fmt.Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …main() { 
5+  8    -…mt.Println("Hello, world!") 
6+     8 +…ontent := "Hello, world!" 
7+     9 +…mt.Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …ain() { 
5+  8    -…t.Println("Hello, world!") 
6+     8 +…ntent := "Hello, world!" 
7+     9 +…t.Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …in() { 
5+  8    -….Println("Hello, world!") 
6+     8 +…tent := "Hello, world!" 
7+     9 +….Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …n() { 
5+  8    -…Println("Hello, world!") 
6+     8 +…ent := "Hello, world!" 
7+     9 +…Println(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …() { 
5+  8    -…rintln("Hello, world!") 
6+     8 +…nt := "Hello, world!" 
7+     9 +…rintln(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …) { 
5+  8    -…intln("Hello, world!") 
6+     8 +…t := "Hello, world!" 
7+     9 +…intln(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … { 
5+  8    -…ntln("Hello, world!") 
6+     8 +… := "Hello, world!" 
7+     9 +…ntln(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  …{ 
5+  8    -…tln("Hello, world!") 
6+     8 +…:= "Hello, world!" 
7+     9 +…tln(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…ln("Hello, world!") 
6+     8 +…= "Hello, world!" 
7+     9 +…ln(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…n("Hello, world!") 
6+     8 +… "Hello, world!" 
7+     9 +…n(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…("Hello, world!") 
6+     8 +…"Hello, world!" 
7+     9 +…(content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…"Hello, world!") 
6+     8 +…Hello, world!" 
7+     9 +…content) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…Hello, world!") 
6+     8 +…ello, world!" 
7+     9 +…ontent) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…ello, world!") 
6+     8 +…llo, world!" 
7+     9 +…ntent) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…llo, world!") 
6+     8 +…lo, world!" 
7+     9 +…tent) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.goldennew file+8
@@ -0,0 +1,8 @@
1+  …   …  @@ -5,5 +5,6 @@  
2+  5   5  … 
3+  6   6  … 
4+  7   7  … 
5+  8    -…lo, world!") 
6+     8 +…o, world!" 
7+     9 +…ent) 
8+  9  10  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf00.goldennew file+5
@@ -0,0 +1,5 @@
1+  …  @@ -2,6 +2,7 @@    …   
2+  2     2   
3+  3  import (   3  import ( 
4+  4   "fmt"   4   "fmt" 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf01.goldennew file+5
@@ -0,0 +1,5 @@
1+  2     2   
2+  3  import (   3  import ( 
3+  4   "fmt"   4   "fmt" 
4+       5 +  "strings" 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf02.goldennew file+5
@@ -0,0 +1,5 @@
1+  3  import (   3  import ( 
2+  4   "fmt"   4   "fmt" 
3+       5 +  "strings" 
4+  5  )   6  ) 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf03.goldennew file+5
@@ -0,0 +1,5 @@
1+  4   "fmt"   4   "fmt" 
2+       5 +  "strings" 
3+  5  )   6  ) 
4+  6     7   
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf04.goldennew file+5
@@ -0,0 +1,5 @@
1+       5 +  "strings" 
2+  5  )   6  ) 
3+  6     7   
4+  7  func main() {   8  func main() { 
5+  …  @@ -9,5 +10,6 @@    …   
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf05.goldennew file+5
@@ -0,0 +1,5 @@
1+  5  )   6  ) 
2+  6     7   
3+  7  func main() {   8  func main() { 
4+  …  @@ -9,5 +10,6 @@    …   
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf06.goldennew file+5
@@ -0,0 +1,5 @@
1+  6     7   
2+  7  func main() {   8  func main() { 
3+  …  @@ -9,5 +10,6 @@    …   
4+  9  }  10  } 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf07.goldennew file+5
@@ -0,0 +1,5 @@
1+  7  func main() {   8  func main() { 
2+  …  @@ -9,5 +10,6 @@    …   
3+  9  }  10  } 
4+ 10    11   
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf08.goldennew file+5
@@ -0,0 +1,5 @@
1+  …  @@ -9,5 +10,6 @@    …   
2+  9  }  10  } 
3+ 10    11   
4+ 11  func getContent() string {  12  func getContent() string { 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf09.goldennew file+5
@@ -0,0 +1,5 @@
1+  9  }  10  } 
2+ 10    11   
3+ 11  func getContent() string {  12  func getContent() string { 
4+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
5+  …  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf10.goldennew file+5
@@ -0,0 +1,5 @@
1+ 10    11   
2+ 11  func getContent() string {  12  func getContent() string { 
3+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
4+      14 +  return content 
5+ 13  }  15  } 
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.goldennew file+5
@@ -0,0 +1,5 @@
1+ 11  func getContent() string {  12  func getContent() string { 
2+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
3+      14 +  return content 
4+ 13  }  15  } 
5+          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.goldennew file+5
@@ -0,0 +1,5 @@
1+ 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!") 
2+      14 +  return content 
3+ 13  }  15  } 
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.goldennew file+5
@@ -0,0 +1,5 @@
1+      14 +  return content 
2+ 13  }  15  } 
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.goldennew file+5
@@ -0,0 +1,5 @@
1+ 13  }  15  } 
2+          
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.goldennew file+5
@@ -0,0 +1,5 @@
1+          
2+          
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.goldennew file+5
@@ -0,0 +1,5 @@
1+          
2+          
3+          
4+          
5+          
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf00.goldennew file+5
@@ -0,0 +1,5 @@
1+  …   …  @@ -2,6 +2,7 @@  
2+  2   2   
3+  3   3  import ( 
4+  4   4   "fmt" 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf01.goldennew file+5
@@ -0,0 +1,5 @@
1+  2   2   
2+  3   3  import ( 
3+  4   4   "fmt" 
4+     5 +  "strings" 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf02.goldennew file+5
@@ -0,0 +1,5 @@
1+  3   3  import ( 
2+  4   4   "fmt" 
3+     5 +  "strings" 
4+  5   6  ) 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf03.goldennew file+5
@@ -0,0 +1,5 @@
1+  4   4   "fmt" 
2+     5 +  "strings" 
3+  5   6  ) 
4+  6   7   
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf04.goldennew file+5
@@ -0,0 +1,5 @@
1+     5 +  "strings" 
2+  5   6  ) 
3+  6   7   
4+  7   8  func main() { 
5+  …   …  @@ -9,5 +10,6 @@  
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf05.goldennew file+5
@@ -0,0 +1,5 @@
1+  5   6  ) 
2+  6   7   
3+  7   8  func main() { 
4+  …   …  @@ -9,5 +10,6 @@  
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf06.goldennew file+5
@@ -0,0 +1,5 @@
1+  6   7   
2+  7   8  func main() { 
3+  …   …  @@ -9,5 +10,6 @@  
4+  9  10  } 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf07.goldennew file+5
@@ -0,0 +1,5 @@
1+  7   8  func main() { 
2+  …   …  @@ -9,5 +10,6 @@  
3+  9  10  } 
4+ 10  11   
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf08.goldennew file+5
@@ -0,0 +1,5 @@
1+  …   …  @@ -9,5 +10,6 @@  
2+  9  10  } 
3+ 10  11   
4+ 11  12  func getContent() string { 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf09.goldennew file+5
@@ -0,0 +1,5 @@
1+  9  10  } 
2+ 10  11   
3+ 11  12  func getContent() string { 
4+ 12    -  return "Hello, world!" 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf10.goldennew file+5
@@ -0,0 +1,5 @@
1+ 10  11   
2+ 11  12  func getContent() string { 
3+ 12    -  return "Hello, world!" 
4+    13 +  content := strings.ToUpper("Hello, World!") 
5+  …   …  … 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf11.goldennew file+5
@@ -0,0 +1,5 @@
1+ 11  12  func getContent() string { 
2+ 12    -  return "Hello, world!" 
3+    13 +  content := strings.ToUpper("Hello, World!") 
4+    14 +  return content 
5+ 13  15  } 
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.goldennew file+5
@@ -0,0 +1,5 @@
1+ 12    -  return "Hello, world!" 
2+    13 +  content := strings.ToUpper("Hello, World!") 
3+    14 +  return content 
4+ 13  15  } 
5+        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.goldennew file+5
@@ -0,0 +1,5 @@
1+    13 +  content := strings.ToUpper("Hello, World!") 
2+    14 +  return content 
3+ 13  15  } 
4+        
5+        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.goldennew file+5
@@ -0,0 +1,5 @@
1+    14 +  return content 
2+ 13  15  } 
3+        
4+        
5+        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.goldennew file+5
@@ -0,0 +1,5 @@
1+ 13  15  } 
2+        
3+        
4+        
5+        
internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.goldennew file+5
@@ -0,0 +1,5 @@
1+        
2+        
3+        
4+        
5+        
internal/exp/diffview/testdata/TestMultipleHunks.afternew file+15
@@ -0,0 +1,15 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "strings"
6+)
7+
8+func main() {
9+ fmt.Println(getContent())
10+}
11+
12+func getContent() string {
13+ content := strings.ToUpper("Hello, World!")
14+ return content
15+}
internal/exp/diffview/testdata/TestMultipleHunks.beforenew file+13
@@ -0,0 +1,13 @@
1+package main
2+
3+import (
4+ "fmt"
5+)
6+
7+func main() {
8+ fmt.Println(getContent())
9+}
10+
11+func getContent() string {
12+ return "Hello, world!"
13+}
internal/exp/diffview/testdata/TestNarrow.afternew file+3
@@ -0,0 +1,3 @@
1+d
2+e
3+f
internal/exp/diffview/testdata/TestNarrow.beforenew file+3
@@ -0,0 +1,3 @@
1+a
2+b
3+c
internal/exp/diffview/testdata/TestTabs.afternew file+15
@@ -0,0 +1,15 @@
1+package main
2+
3+import (
4+ "fmt"
5+ "strings"
6+)
7+
8+func main() {
9+ fmt.Println(getContent())
10+}
11+
12+func getContent() string {
13+ content := strings.ToUpper("Hello, World!")
14+ return content
15+}
internal/exp/diffview/testdata/TestTabs.beforenew file+13
@@ -0,0 +1,13 @@
1+package main
2+
3+import (
4+ "fmt"
5+)
6+
7+func main() {
8+ fmt.Println(getContent())
9+}
10+
11+func getContent() string {
12+ return "Hello, world!"
13+}
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLines/Content.goldennew file+11
@@ -0,0 +1,11 @@
1+--- main.go
2++++ main.go
3+@@ -5,5 +5,6 @@
4+ )
5+
6+ func main() {
7+- fmt.Println("Hello, World!")
8++ content := "Hello, World!"
9++ fmt.Println(content)
10+ }
11+\ No newline at end of file
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLines/JSON.goldennew file+40
@@ -0,0 +1,40 @@
1+{
2+ "From": "main.go",
3+ "To": "main.go",
4+ "Hunks": [
5+ {
6+ "FromLine": 5,
7+ "ToLine": 5,
8+ "Lines": [
9+ {
10+ "Kind": 2,
11+ "Content": "\t)\n"
12+ },
13+ {
14+ "Kind": 2,
15+ "Content": "\n"
16+ },
17+ {
18+ "Kind": 2,
19+ "Content": "\tfunc main() {\n"
20+ },
21+ {
22+ "Kind": 0,
23+ "Content": "\t\tfmt.Println(\"Hello, World!\")\n"
24+ },
25+ {
26+ "Kind": 1,
27+ "Content": "\t\tcontent := \"Hello, World!\"\n"
28+ },
29+ {
30+ "Kind": 1,
31+ "Content": "\t\tfmt.Println(content)\n"
32+ },
33+ {
34+ "Kind": 2,
35+ "Content": "\t}"
36+ }
37+ ]
38+ }
39+ ]
40+}
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusOne/Content.goldennew file+12
@@ -0,0 +1,12 @@
1+--- main.go
2++++ main.go
3+@@ -4,6 +4,7 @@
4+ "fmt"
5+ )
6+
7+ func main() {
8+- fmt.Println("Hello, World!")
9++ content := "Hello, World!"
10++ fmt.Println(content)
11+ }
12+\ No newline at end of file
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusOne/JSON.goldennew file+44
@@ -0,0 +1,44 @@
1+{
2+ "From": "main.go",
3+ "To": "main.go",
4+ "Hunks": [
5+ {
6+ "FromLine": 4,
7+ "ToLine": 4,
8+ "Lines": [
9+ {
10+ "Kind": 2,
11+ "Content": "\t\t\"fmt\"\n"
12+ },
13+ {
14+ "Kind": 2,
15+ "Content": "\t)\n"
16+ },
17+ {
18+ "Kind": 2,
19+ "Content": "\n"
20+ },
21+ {
22+ "Kind": 2,
23+ "Content": "\tfunc main() {\n"
24+ },
25+ {
26+ "Kind": 0,
27+ "Content": "\t\tfmt.Println(\"Hello, World!\")\n"
28+ },
29+ {
30+ "Kind": 1,
31+ "Content": "\t\tcontent := \"Hello, World!\"\n"
32+ },
33+ {
34+ "Kind": 1,
35+ "Content": "\t\tfmt.Println(content)\n"
36+ },
37+ {
38+ "Kind": 2,
39+ "Content": "\t}"
40+ }
41+ ]
42+ }
43+ ]
44+}
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusTwo/Content.goldennew file+13
@@ -0,0 +1,13 @@
1+--- main.go
2++++ main.go
3+@@ -3,7 +3,8 @@
4+ import (
5+ "fmt"
6+ )
7+
8+ func main() {
9+- fmt.Println("Hello, World!")
10++ content := "Hello, World!"
11++ fmt.Println(content)
12+ }
13+\ No newline at end of file
internal/exp/diffview/testdata/TestUdiff/ToUnifiedDiff/DefaultContextLinesPlusTwo/JSON.goldennew file+48
@@ -0,0 +1,48 @@
1+{
2+ "From": "main.go",
3+ "To": "main.go",
4+ "Hunks": [
5+ {
6+ "FromLine": 3,
7+ "ToLine": 3,
8+ "Lines": [
9+ {
10+ "Kind": 2,
11+ "Content": "\timport (\n"
12+ },
13+ {
14+ "Kind": 2,
15+ "Content": "\t\t\"fmt\"\n"
16+ },
17+ {
18+ "Kind": 2,
19+ "Content": "\t)\n"
20+ },
21+ {
22+ "Kind": 2,
23+ "Content": "\n"
24+ },
25+ {
26+ "Kind": 2,
27+ "Content": "\tfunc main() {\n"
28+ },
29+ {
30+ "Kind": 0,
31+ "Content": "\t\tfmt.Println(\"Hello, World!\")\n"
32+ },
33+ {
34+ "Kind": 1,
35+ "Content": "\t\tcontent := \"Hello, World!\"\n"
36+ },
37+ {
38+ "Kind": 1,
39+ "Content": "\t\tfmt.Println(content)\n"
40+ },
41+ {
42+ "Kind": 2,
43+ "Content": "\t}"
44+ }
45+ ]
46+ }
47+ ]
48+}
internal/exp/diffview/testdata/TestUdiff/Unified.goldennew file+11
@@ -0,0 +1,11 @@
1+--- main.go
2++++ main.go
3+@@ -5,5 +5,6 @@
4+ )
5+
6+ func main() {
7+- fmt.Println("Hello, World!")
8++ content := "Hello, World!"
9++ fmt.Println(content)
10+ }
11+\ No newline at end of file
internal/exp/diffview/udiff_test.gonew file+92
@@ -0,0 +1,92 @@
1+package diffview_test
2+
3+import (
4+ "bytes"
5+ "encoding/json"
6+ "testing"
7+
8+ "github.com/aymanbagabas/go-udiff"
9+ "github.com/aymanbagabas/go-udiff/myers"
10+ "github.com/charmbracelet/x/exp/golden"
11+)
12+
13+func TestUdiff(t *testing.T) {
14+ before := `package main
15+
16+ import (
17+ "fmt"
18+ )
19+
20+ func main() {
21+ fmt.Println("Hello, World!")
22+ }`
23+
24+ after := `package main
25+
26+ import (
27+ "fmt"
28+ )
29+
30+ func main() {
31+ content := "Hello, World!"
32+ fmt.Println(content)
33+ }`
34+
35+ t.Run("Unified", func(t *testing.T) {
36+ content := udiff.Unified("main.go", "main.go", before, after)
37+ golden.RequireEqual(t, []byte(content))
38+ })
39+
40+ t.Run("ToUnifiedDiff", func(t *testing.T) {
41+ toUnifiedDiff := func(t *testing.T, before, after string, contextLines int) udiff.UnifiedDiff {
42+ edits := myers.ComputeEdits(before, after) //nolint:staticcheck
43+ unifiedDiff, err := udiff.ToUnifiedDiff("main.go", "main.go", before, edits, contextLines)
44+ if err != nil {
45+ t.Fatalf("ToUnifiedDiff failed: %v", err)
46+ }
47+ return unifiedDiff
48+ }
49+ toJSON := func(t *testing.T, unifiedDiff udiff.UnifiedDiff) []byte {
50+ var buff bytes.Buffer
51+ encoder := json.NewEncoder(&buff)
52+ encoder.SetIndent("", " ")
53+ if err := encoder.Encode(unifiedDiff); err != nil {
54+ t.Fatalf("Failed to encode unified diff: %v", err)
55+ }
56+ return buff.Bytes()
57+ }
58+
59+ t.Run("DefaultContextLines", func(t *testing.T) {
60+ unifiedDiff := toUnifiedDiff(t, before, after, udiff.DefaultContextLines)
61+
62+ t.Run("Content", func(t *testing.T) {
63+ golden.RequireEqual(t, []byte(unifiedDiff.String()))
64+ })
65+ t.Run("JSON", func(t *testing.T) {
66+ golden.RequireEqual(t, toJSON(t, unifiedDiff))
67+ })
68+ })
69+
70+ t.Run("DefaultContextLinesPlusOne", func(t *testing.T) {
71+ unifiedDiff := toUnifiedDiff(t, before, after, udiff.DefaultContextLines+1)
72+
73+ t.Run("Content", func(t *testing.T) {
74+ golden.RequireEqual(t, []byte(unifiedDiff.String()))
75+ })
76+ t.Run("JSON", func(t *testing.T) {
77+ golden.RequireEqual(t, toJSON(t, unifiedDiff))
78+ })
79+ })
80+
81+ t.Run("DefaultContextLinesPlusTwo", func(t *testing.T) {
82+ unifiedDiff := toUnifiedDiff(t, before, after, udiff.DefaultContextLines+2)
83+
84+ t.Run("Content", func(t *testing.T) {
85+ golden.RequireEqual(t, []byte(unifiedDiff.String()))
86+ })
87+ t.Run("JSON", func(t *testing.T) {
88+ golden.RequireEqual(t, toJSON(t, unifiedDiff))
89+ })
90+ })
91+ })
92+}
internal/exp/diffview/util.gonew file+39
@@ -0,0 +1,39 @@
1+package diffview
2+
3+import (
4+ "fmt"
5+ "strings"
6+
7+ "github.com/charmbracelet/x/ansi"
8+)
9+
10+func pad(v any, width int) string {
11+ s := fmt.Sprintf("%v", v)
12+ w := ansi.StringWidth(s)
13+ if w >= width {
14+ return s
15+ }
16+ return strings.Repeat(" ", width-w) + s
17+}
18+
19+func isEven(n int) bool {
20+ return n%2 == 0
21+}
22+
23+func isOdd(n int) bool {
24+ return !isEven(n)
25+}
26+
27+func btoi(b bool) int {
28+ if b {
29+ return 1
30+ }
31+ return 0
32+}
33+
34+func ternary[T any](cond bool, t, f T) T {
35+ if cond {
36+ return t
37+ }
38+ return f
39+}
internal/exp/diffview/util_test.gonew file+27
@@ -0,0 +1,27 @@
1+package diffview
2+
3+import (
4+ "testing"
5+)
6+
7+func TestPad(t *testing.T) {
8+ tests := []struct {
9+ input any
10+ width int
11+ expected string
12+ }{
13+ {7, 2, " 7"},
14+ {7, 3, " 7"},
15+ {"a", 2, " a"},
16+ {"a", 3, " a"},
17+ {"…", 2, " …"},
18+ {"…", 3, " …"},
19+ }
20+
21+ for _, tt := range tests {
22+ result := pad(tt.input, tt.width)
23+ if result != tt.expected {
24+ t.Errorf("expected %q, got %q", tt.expected, result)
25+ }
26+ }
27+}
internal/fileutil/fileutil.go → internal/fsext/fileutil.gorenamed+25−1
@@ -1,4 +1,4 @@
1−package fileutil
1+package fsext
2
2
3import (
3import (
4 "fmt"
4 "fmt"
@@ -211,3 +211,27 @@ func PrettyPath(path string) string {
211 }
211 }
212 return path
212 return path
213}
213}
214+
215+func DirTrim(pwd string, lim int) string {
216+ var (
217+ out string
218+ sep = string(filepath.Separator)
219+ )
220+ dirs := strings.Split(pwd, sep)
221+ if lim > len(dirs)-1 || lim <= 0 {
222+ return pwd
223+ }
224+ for i := len(dirs) - 1; i > 0; i-- {
225+ out = sep + out
226+ if i == len(dirs)-1 {
227+ out = dirs[i]
228+ } else if i >= len(dirs)-lim {
229+ out = string(dirs[i][0]) + out
230+ } else {
231+ out = "..." + out
232+ break
233+ }
234+ }
235+ out = filepath.Join("~", out)
236+ return out
237+}
internal/fileutil/ls.go → internal/fsext/ls.gorenamed+1−1
@@ -1,4 +1,4 @@
1−package fileutil
1+package fsext
2
2
3import (
3import (
4 "os"
4 "os"
internal/history/file.go+7−49
@@ -4,9 +4,7 @@ import (
4 "context"
4 "context"
5 "database/sql"
5 "database/sql"
6 "fmt"
6 "fmt"
7− "strconv"
8 "strings"
7 "strings"
9− "time"
10
8
11 "github.com/charmbracelet/crush/internal/db"
9 "github.com/charmbracelet/crush/internal/db"
12 "github.com/charmbracelet/crush/internal/pubsub"
10 "github.com/charmbracelet/crush/internal/pubsub"
@@ -14,7 +12,7 @@ import (
14)
12)
15
13
16const (
14const (
17− InitialVersion = "initial"
15+ InitialVersion = 0
18)
16)
19
17
20type File struct {
18type File struct {
@@ -22,7 +20,7 @@ type File struct {
22 SessionID string
20 SessionID string
23 Path string
21 Path string
24 Content string
22 Content string
25− Version string
23+ Version int64
26 CreatedAt int64
24 CreatedAt int64
27 UpdatedAt int64
25 UpdatedAt int64
28}
26}
@@ -35,7 +33,6 @@ type Service interface {
35 GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
33 GetByPathAndSession(ctx context.Context, path, sessionID string) (File, error)
36 ListBySession(ctx context.Context, sessionID string) ([]File, error)
34 ListBySession(ctx context.Context, sessionID string) ([]File, error)
37 ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
35 ListLatestSessionFiles(ctx context.Context, sessionID string) ([]File, error)
38− Update(ctx context.Context, file File) (File, error)
39 Delete(ctx context.Context, id string) error
36 Delete(ctx context.Context, id string) error
40 DeleteSessionFiles(ctx context.Context, sessionID string) error
37 DeleteSessionFiles(ctx context.Context, sessionID string) error
41}
38}
@@ -71,30 +68,13 @@ func (s *service) CreateVersion(ctx context.Context, sessionID, path, content st
71 }
68 }
72
69
73 // Get the latest version
70 // Get the latest version
74− latestFile := files[0] // Files are ordered by created_at DESC
71+ latestFile := files[0] // Files are ordered by version DESC, created_at DESC
75− latestVersion := latestFile.Version
72+ nextVersion := latestFile.Version + 1
76−
77− // Generate the next version
78− var nextVersion string
79− if latestVersion == InitialVersion {
80− nextVersion = "v1"
81− } else if strings.HasPrefix(latestVersion, "v") {
82− versionNum, err := strconv.Atoi(latestVersion[1:])
83− if err != nil {
84− // If we can't parse the version, just use a timestamp-based version
85− nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
86− } else {
87− nextVersion = fmt.Sprintf("v%d", versionNum+1)
88− }
89− } else {
90− // If the version format is unexpected, use a timestamp-based version
91− nextVersion = fmt.Sprintf("v%d", latestFile.CreatedAt)
92− }
93
73
94 return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
74 return s.createWithVersion(ctx, sessionID, path, content, nextVersion)
95}
75}
96
76
97−func (s *service) createWithVersion(ctx context.Context, sessionID, path, content, version string) (File, error) {
77+func (s *service) createWithVersion(ctx context.Context, sessionID, path, content string, version int64) (File, error) {
98 // Maximum number of retries for transaction conflicts
78 // Maximum number of retries for transaction conflicts
99 const maxRetries = 3
79 const maxRetries = 3
100 var file File
80 var file File
@@ -126,16 +106,8 @@ func (s *service) createWithVersion(ctx context.Context, sessionID, path, conten
126 // Check if this is a uniqueness constraint violation
106 // Check if this is a uniqueness constraint violation
127 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
107 if strings.Contains(txErr.Error(), "UNIQUE constraint failed") {
128 if attempt < maxRetries-1 {
108 if attempt < maxRetries-1 {
129− // If we have retries left, generate a new version and try again
109+ // If we have retries left, increment version and try again
130− if strings.HasPrefix(version, "v") {
110+ version++
131− versionNum, parseErr := strconv.Atoi(version[1:])
132− if parseErr == nil {
133− version = fmt.Sprintf("v%d", versionNum+1)
134− continue
135− }
136− }
137− // If we can't parse the version, use a timestamp-based version
138− version = fmt.Sprintf("v%d", time.Now().Unix())
139 continue
111 continue
140 }
112 }
141 }
113 }
@@ -198,20 +170,6 @@ func (s *service) ListLatestSessionFiles(ctx context.Context, sessionID string)
198 return files, nil
170 return files, nil
199}
171}
200
172
201−func (s *service) Update(ctx context.Context, file File) (File, error) {
202− dbFile, err := s.q.UpdateFile(ctx, db.UpdateFileParams{
203− ID: file.ID,
204− Content: file.Content,
205− Version: file.Version,
206− })
207− if err != nil {
208− return File{}, err
209− }
210− updatedFile := s.fromDBItem(dbFile)
211− s.Publish(pubsub.UpdatedEvent, updatedFile)
212− return updatedFile, nil
213−}
214−
215func (s *service) Delete(ctx context.Context, id string) error {
173func (s *service) Delete(ctx context.Context, id string) error {
216 file, err := s.Get(ctx, id)
174 file, err := s.Get(ctx, id)
217 if err != nil {
175 if err != nil {
internal/llm/agent/agent.go+16−5
@@ -163,7 +163,9 @@ func (a *agent) generateTitle(ctx context.Context, sessionID string, content str
163 return err
163 return err
164 }
164 }
165 parts := []message.ContentPart{message.TextContent{Text: content}}
165 parts := []message.ContentPart{message.TextContent{Text: content}}
166+
167+ // Use streaming approach like summarization
166− response, err := a.titleProvider.SendMessages(
168+ response := a.titleProvider.StreamResponse(
167 ctx,
169 ctx,
168 []message.Message{
170 []message.Message{
169 {
171 {
@@ -173,11 +175,20 @@ func (a *agent) generateTitle(ctx context.Context, sessionID string, content str
173 },
175 },
174 make([]tools.BaseTool, 0),
176 make([]tools.BaseTool, 0),
175 )
177 )
178+
179+ var finalResponse *provider.ProviderResponse
180+ for r := range response {
176− if err != nil {
181+ if r.Error != nil {
177− return err
182+ return r.Error
183+ }
184+ finalResponse = r.Response
185+ }
186+
187+ if finalResponse == nil {
188+ return fmt.Errorf("no response received from title provider")
178 }
189 }
179
190
180− title := strings.TrimSpace(strings.ReplaceAll(response.Content, "\n", " "))
191+ title := strings.TrimSpace(strings.ReplaceAll(finalResponse.Content, "\n", " "))
181 if title == "" {
192 if title == "" {
182 return nil
193 return nil
183 }
194 }
@@ -241,7 +252,7 @@ func (a *agent) processGeneration(ctx context.Context, sessionID, content string
241 logging.ErrorPersist("panic while generating title")
252 logging.ErrorPersist("panic while generating title")
242 })
253 })
243 titleErr := a.generateTitle(context.Background(), sessionID, content)
254 titleErr := a.generateTitle(context.Background(), sessionID, content)
244− if titleErr != nil {
255+ if titleErr != nil && !errors.Is(titleErr, context.Canceled) && !errors.Is(titleErr, context.DeadlineExceeded) {
245 logging.ErrorPersist(fmt.Sprintf("failed to generate title: %v", titleErr))
256 logging.ErrorPersist(fmt.Sprintf("failed to generate title: %v", titleErr))
246 }
257 }
247 }()
258 }()
internal/llm/agent/tools.go−1
@@ -33,7 +33,6 @@ func CoderAgentTools(
33 tools.NewLsTool(),
33 tools.NewLsTool(),
34 tools.NewSourcegraphTool(),
34 tools.NewSourcegraphTool(),
35 tools.NewViewTool(lspClients),
35 tools.NewViewTool(lspClients),
36− tools.NewPatchTool(lspClients, permissions, history),
37 tools.NewWriteTool(lspClients, permissions, history),
36 tools.NewWriteTool(lspClients, permissions, history),
38 NewAgentTool(sessions, messages, lspClients),
37 NewAgentTool(sessions, messages, lspClients),
39 }, otherTools...,
38 }, otherTools...,
internal/llm/tools/edit.go+31−23
@@ -24,14 +24,16 @@ type EditParams struct {
24}
24}
25
25
26type EditPermissionsParams struct {
26type EditPermissionsParams struct {
27− FilePath string `json:"file_path"`
27+ FilePath string `json:"file_path"`
28− Diff string `json:"diff"`
28+ OldContent string `json:"old_content,omitempty"`
29+ NewContent string `json:"new_content,omitempty"`
29}
30}
30
31
31type EditResponseMetadata struct {
32type EditResponseMetadata struct {
32− Diff string `json:"diff"`
33− Additions int `json:"additions"`
33+ Additions int `json:"additions"`
34− Removals int `json:"removals"`
34+ Removals int `json:"removals"`
35+ OldContent string `json:"old_content,omitempty"`
36+ NewContent string `json:"new_content,omitempty"`
35}
37}
36
38
37type editTool struct {
39type editTool struct {
@@ -191,7 +193,7 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
191 return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file")
193 return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file")
192 }
194 }
193
195
194− diff, additions, removals := diff.GenerateDiff(
196+ _, additions, removals := diff.GenerateDiff(
195 "",
197 "",
196 content,
198 content,
197 filePath,
199 filePath,
@@ -209,8 +211,9 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
209 Action: "write",
211 Action: "write",
210 Description: fmt.Sprintf("Create file %s", filePath),
212 Description: fmt.Sprintf("Create file %s", filePath),
211 Params: EditPermissionsParams{
213 Params: EditPermissionsParams{
212− FilePath: filePath,
214+ FilePath: filePath,
213− Diff: diff,
215+ OldContent: "",
216+ NewContent: content,
214 },
217 },
215 },
218 },
216 )
219 )
@@ -243,9 +246,10 @@ func (e *editTool) createNewFile(ctx context.Context, filePath, content string)
243 return WithResponseMetadata(
246 return WithResponseMetadata(
244 NewTextResponse("File created: "+filePath),
247 NewTextResponse("File created: "+filePath),
245 EditResponseMetadata{
248 EditResponseMetadata{
246− Diff: diff,
249+ OldContent: "",
250+ NewContent: content,
247− Additions: additions,
251+ Additions: additions,
248− Removals: removals,
252+ Removals: removals,
249 },
253 },
250 ), nil
254 ), nil
251}
255}
@@ -301,7 +305,7 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
301 return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file")
305 return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file")
302 }
306 }
303
307
304− diff, additions, removals := diff.GenerateDiff(
308+ _, additions, removals := diff.GenerateDiff(
305 oldContent,
309 oldContent,
306 newContent,
310 newContent,
307 filePath,
311 filePath,
@@ -320,8 +324,9 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
320 Action: "write",
324 Action: "write",
321 Description: fmt.Sprintf("Delete content from file %s", filePath),
325 Description: fmt.Sprintf("Delete content from file %s", filePath),
322 Params: EditPermissionsParams{
326 Params: EditPermissionsParams{
323− FilePath: filePath,
327+ FilePath: filePath,
324− Diff: diff,
328+ OldContent: oldContent,
329+ NewContent: newContent,
325 },
330 },
326 },
331 },
327 )
332 )
@@ -362,9 +367,10 @@ func (e *editTool) deleteContent(ctx context.Context, filePath, oldString string
362 return WithResponseMetadata(
367 return WithResponseMetadata(
363 NewTextResponse("Content deleted from file: "+filePath),
368 NewTextResponse("Content deleted from file: "+filePath),
364 EditResponseMetadata{
369 EditResponseMetadata{
365− Diff: diff,
370+ OldContent: oldContent,
371+ NewContent: newContent,
366− Additions: additions,
372+ Additions: additions,
367− Removals: removals,
373+ Removals: removals,
368 },
374 },
369 ), nil
375 ), nil
370}
376}
@@ -422,7 +428,7 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
422 if sessionID == "" || messageID == "" {
428 if sessionID == "" || messageID == "" {
423 return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file")
429 return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a new file")
424 }
430 }
425− diff, additions, removals := diff.GenerateDiff(
431+ _, additions, removals := diff.GenerateDiff(
426 oldContent,
432 oldContent,
427 newContent,
433 newContent,
428 filePath,
434 filePath,
@@ -440,8 +446,9 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
440 Action: "write",
446 Action: "write",
441 Description: fmt.Sprintf("Replace content in file %s", filePath),
447 Description: fmt.Sprintf("Replace content in file %s", filePath),
442 Params: EditPermissionsParams{
448 Params: EditPermissionsParams{
443− FilePath: filePath,
449+ FilePath: filePath,
444− Diff: diff,
450+ OldContent: oldContent,
451+ NewContent: newContent,
445 },
452 },
446 },
453 },
447 )
454 )
@@ -482,8 +489,9 @@ func (e *editTool) replaceContent(ctx context.Context, filePath, oldString, newS
482 return WithResponseMetadata(
489 return WithResponseMetadata(
483 NewTextResponse("Content replaced in file: "+filePath),
490 NewTextResponse("Content replaced in file: "+filePath),
484 EditResponseMetadata{
491 EditResponseMetadata{
485− Diff: diff,
492+ OldContent: oldContent,
493+ NewContent: newContent,
486− Additions: additions,
494+ Additions: additions,
487− Removals: removals,
495+ Removals: removals,
488 }), nil
496 }), nil
489}
497}
internal/llm/tools/fetch.go+15−49
@@ -7,7 +7,6 @@ import (
7 "io"
7 "io"
8 "net/http"
8 "net/http"
9 "strings"
9 "strings"
10− "sync"
11 "time"
10 "time"
12
11
13 md "github.com/JohannesKaufmann/html-to-markdown"
12 md "github.com/JohannesKaufmann/html-to-markdown"
@@ -29,10 +28,8 @@ type FetchPermissionsParams struct {
29}
28}
30
29
31type fetchTool struct {
30type fetchTool struct {
32− client *http.Client
31+ client *http.Client
33− clientPool map[int]*http.Client
34− clientPoolMu sync.RWMutex
35− permissions permission.Service
32+ permissions permission.Service
36}
33}
37
34
38const (
35const (
@@ -78,51 +75,10 @@ func NewFetchTool(permissions permission.Service) BaseTool {
78 IdleConnTimeout: 90 * time.Second,
75 IdleConnTimeout: 90 * time.Second,
79 },
76 },
80 },
77 },
81− clientPool: make(map[int]*http.Client),
82 permissions: permissions,
78 permissions: permissions,
83 }
79 }
84}
80}
85
81
86−// getClientForTimeout returns a cached client for the given timeout or the default client
87−func (t *fetchTool) getClientForTimeout(timeout int) *http.Client {
88− if timeout <= 0 {
89− return t.client
90− }
91−
92− maxTimeout := 120 // 2 minutes
93− if timeout > maxTimeout {
94− timeout = maxTimeout
95− }
96−
97− // Check if we have a cached client for this timeout
98− t.clientPoolMu.RLock()
99− if client, exists := t.clientPool[timeout]; exists {
100− t.clientPoolMu.RUnlock()
101− return client
102− }
103− t.clientPoolMu.RUnlock()
104−
105− // Create and cache a new client
106− t.clientPoolMu.Lock()
107− defer t.clientPoolMu.Unlock()
108−
109− // Double-check in case another goroutine created it
110− if client, exists := t.clientPool[timeout]; exists {
111− return client
112− }
113−
114− client := &http.Client{
115− Timeout: time.Duration(timeout) * time.Second,
116− Transport: &http.Transport{
117− MaxIdleConns: 100,
118− MaxIdleConnsPerHost: 10,
119− IdleConnTimeout: 90 * time.Second,
120− },
121− }
122− t.clientPool[timeout] = client
123− return client
124−}
125−
126func (t *fetchTool) Info() ToolInfo {
82func (t *fetchTool) Info() ToolInfo {
127 return ToolInfo{
83 return ToolInfo{
128 Name: FetchToolName,
84 Name: FetchToolName,
@@ -185,16 +141,26 @@ func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error
185 return ToolResponse{}, permission.ErrorPermissionDenied
141 return ToolResponse{}, permission.ErrorPermissionDenied
186 }
142 }
187
143
188− client := t.getClientForTimeout(params.Timeout)
144+ // Handle timeout with context
145+ requestCtx := ctx
146+ if params.Timeout > 0 {
147+ maxTimeout := 120 // 2 minutes
148+ if params.Timeout > maxTimeout {
149+ params.Timeout = maxTimeout
150+ }
151+ var cancel context.CancelFunc
152+ requestCtx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Second)
153+ defer cancel()
154+ }
189
155
190− req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil)
156+ req, err := http.NewRequestWithContext(requestCtx, "GET", params.URL, nil)
191 if err != nil {
157 if err != nil {
192 return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
158 return ToolResponse{}, fmt.Errorf("failed to create request: %w", err)
193 }
159 }
194
160
195 req.Header.Set("User-Agent", "crush/1.0")
161 req.Header.Set("User-Agent", "crush/1.0")
196
162
197− resp, err := client.Do(req)
163+ resp, err := t.client.Do(req)
198 if err != nil {
164 if err != nil {
199 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
165 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
200 }
166 }
internal/llm/tools/glob.go+4−4
@@ -11,7 +11,7 @@ import (
11 "strings"
11 "strings"
12
12
13 "github.com/charmbracelet/crush/internal/config"
13 "github.com/charmbracelet/crush/internal/config"
14− "github.com/charmbracelet/crush/internal/fileutil"
14+ "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/logging"
15 "github.com/charmbracelet/crush/internal/logging"
16)
16)
17
17
@@ -127,7 +127,7 @@ func (g *globTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
127}
127}
128
128
129func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
129func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
130− cmdRg := fileutil.GetRgCmd(pattern)
130+ cmdRg := fsext.GetRgCmd(pattern)
131 if cmdRg != nil {
131 if cmdRg != nil {
132 cmdRg.Dir = searchPath
132 cmdRg.Dir = searchPath
133 matches, err := runRipgrep(cmdRg, searchPath, limit)
133 matches, err := runRipgrep(cmdRg, searchPath, limit)
@@ -137,7 +137,7 @@ func globFiles(pattern, searchPath string, limit int) ([]string, bool, error) {
137 logging.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err))
137 logging.Warn(fmt.Sprintf("Ripgrep execution failed: %v. Falling back to doublestar.", err))
138 }
138 }
139
139
140− return fileutil.GlobWithDoubleStar(pattern, searchPath, limit)
140+ return fsext.GlobWithDoubleStar(pattern, searchPath, limit)
141}
141}
142
142
143func runRipgrep(cmd *exec.Cmd, searchRoot string, limit int) ([]string, error) {
143func runRipgrep(cmd *exec.Cmd, searchRoot string, limit int) ([]string, error) {
@@ -158,7 +158,7 @@ func runRipgrep(cmd *exec.Cmd, searchRoot string, limit int) ([]string, error) {
158 if !filepath.IsAbs(absPath) {
158 if !filepath.IsAbs(absPath) {
159 absPath = filepath.Join(searchRoot, absPath)
159 absPath = filepath.Join(searchRoot, absPath)
160 }
160 }
161− if fileutil.SkipHidden(absPath) {
161+ if fsext.SkipHidden(absPath) {
162 continue
162 continue
163 }
163 }
164 matches = append(matches, absPath)
164 matches = append(matches, absPath)
internal/llm/tools/grep.go+13−12
@@ -17,7 +17,7 @@ import (
17 "time"
17 "time"
18
18
19 "github.com/charmbracelet/crush/internal/config"
19 "github.com/charmbracelet/crush/internal/config"
20− "github.com/charmbracelet/crush/internal/fileutil"
20+ "github.com/charmbracelet/crush/internal/fsext"
21)
21)
22
22
23// regexCache provides thread-safe caching of compiled regex patterns
23// regexCache provides thread-safe caching of compiled regex patterns
@@ -342,7 +342,7 @@ func searchFilesWithRegex(pattern, rootPath, include string) ([]grepMatch, error
342 return nil // Skip directories
342 return nil // Skip directories
343 }
343 }
344
344
345− if fileutil.SkipHidden(path) {
345+ if fsext.SkipHidden(path) {
346 return nil
346 return nil
347 }
347 }
348
348
@@ -402,19 +402,20 @@ func fileContainsPattern(filePath string, pattern *regexp.Regexp) (bool, int, st
402 return false, 0, "", scanner.Err()
402 return false, 0, "", scanner.Err()
403}
403}
404
404
405+var binaryExts = map[string]struct{}{
406+ ".exe": {}, ".dll": {}, ".so": {}, ".dylib": {},
407+ ".bin": {}, ".obj": {}, ".o": {}, ".a": {},
408+ ".zip": {}, ".tar": {}, ".gz": {}, ".bz2": {},
409+ ".jpg": {}, ".jpeg": {}, ".png": {}, ".gif": {},
410+ ".pdf": {}, ".doc": {}, ".docx": {}, ".xls": {},
411+ ".mp3": {}, ".mp4": {}, ".avi": {}, ".mov": {},
412+}
413+
405// isBinaryFile performs a quick check to determine if a file is binary
414// isBinaryFile performs a quick check to determine if a file is binary
406func isBinaryFile(filePath string) bool {
415func isBinaryFile(filePath string) bool {
407 // Check file extension first (fastest)
416 // Check file extension first (fastest)
408 ext := strings.ToLower(filepath.Ext(filePath))
417 ext := strings.ToLower(filepath.Ext(filePath))
409− binaryExts := map[string]bool{
410− ".exe": true, ".dll": true, ".so": true, ".dylib": true,
411− ".bin": true, ".obj": true, ".o": true, ".a": true,
412− ".zip": true, ".tar": true, ".gz": true, ".bz2": true,
413− ".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
414− ".pdf": true, ".doc": true, ".docx": true, ".xls": true,
415− ".mp3": true, ".mp4": true, ".avi": true, ".mov": true,
416− }
417− if binaryExts[ext] {
418+ if _, isBinary := binaryExts[ext]; isBinary {
418 return true
419 return true
419 }
420 }
420
421
@@ -433,7 +434,7 @@ func isBinaryFile(filePath string) bool {
433 }
434 }
434
435
435 // Check for null bytes (common in binary files)
436 // Check for null bytes (common in binary files)
436− for i := 0; i < n; i++ {
437+ for i := range n {
437 if buffer[i] == 0 {
438 if buffer[i] == 0 {
438 return true
439 return true
439 }
440 }
internal/llm/tools/ls.go+2−2
@@ -9,7 +9,7 @@ import (
9 "strings"
9 "strings"
10
10
11 "github.com/charmbracelet/crush/internal/config"
11 "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/fileutil"
12+ "github.com/charmbracelet/crush/internal/fsext"
13)
13)
14
14
15type LSParams struct {
15type LSParams struct {
@@ -108,7 +108,7 @@ func (l *lsTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
108 return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
108 return NewTextErrorResponse(fmt.Sprintf("path does not exist: %s", searchPath)), nil
109 }
109 }
110
110
111− files, truncated, err := fileutil.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
111+ files, truncated, err := fsext.ListDirectory(searchPath, params.Ignore, MaxLSFiles)
112 if err != nil {
112 if err != nil {
113 return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
113 return ToolResponse{}, fmt.Errorf("error listing directory: %w", err)
114 }
114 }
internal/llm/tools/patch.godeleted−372
@@ -1,372 +0,0 @@
1−package tools
2−
3−import (
4− "context"
5− "encoding/json"
6− "fmt"
7− "os"
8− "path/filepath"
9− "time"
10−
11− "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/diff"
13− "github.com/charmbracelet/crush/internal/history"
14− "github.com/charmbracelet/crush/internal/logging"
15− "github.com/charmbracelet/crush/internal/lsp"
16− "github.com/charmbracelet/crush/internal/permission"
17−)
18−
19−type PatchParams struct {
20− PatchText string `json:"patch_text"`
21−}
22−
23−type PatchResponseMetadata struct {
24− FilesChanged []string `json:"files_changed"`
25− Additions int `json:"additions"`
26− Removals int `json:"removals"`
27−}
28−
29−type patchTool struct {
30− lspClients map[string]*lsp.Client
31− permissions permission.Service
32− files history.Service
33−}
34−
35−const (
36− PatchToolName = "patch"
37− patchDescription = `Applies a patch to multiple files in one operation. This tool is useful for making coordinated changes across multiple files.
38−
39−The patch text must follow this format:
40−*** Begin Patch
41−*** Update File: /path/to/file
42−@@ Context line (unique within the file)
43− Line to keep
44−-Line to remove
45−+Line to add
46− Line to keep
47−*** Add File: /path/to/new/file
48−+Content of the new file
49−+More content
50−*** Delete File: /path/to/file/to/delete
51−*** End Patch
52−
53−Before using this tool:
54−1. Use the FileRead tool to understand the files' contents and context
55−2. Verify all file paths are correct (use the LS tool)
56−
57−CRITICAL REQUIREMENTS FOR USING THIS TOOL:
58−
59−1. UNIQUENESS: Context lines MUST uniquely identify the specific sections you want to change
60−2. PRECISION: All whitespace, indentation, and surrounding code must match exactly
61−3. VALIDATION: Ensure edits result in idiomatic, correct code
62−4. PATHS: Always use absolute file paths (starting with /)
63−
64−The tool will apply all changes in a single atomic operation.`
65−)
66−
67−func NewPatchTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service) BaseTool {
68− return &patchTool{
69− lspClients: lspClients,
70− permissions: permissions,
71− files: files,
72− }
73−}
74−
75−func (p *patchTool) Info() ToolInfo {
76− return ToolInfo{
77− Name: PatchToolName,
78− Description: patchDescription,
79− Parameters: map[string]any{
80− "patch_text": map[string]any{
81− "type": "string",
82− "description": "The full patch text that describes all changes to be made",
83− },
84− },
85− Required: []string{"patch_text"},
86− }
87−}
88−
89−func (p *patchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
90− var params PatchParams
91− if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
92− return NewTextErrorResponse("invalid parameters"), nil
93− }
94−
95− if params.PatchText == "" {
96− return NewTextErrorResponse("patch_text is required"), nil
97− }
98−
99− // Identify all files needed for the patch and verify they've been read
100− filesToRead := diff.IdentifyFilesNeeded(params.PatchText)
101− for _, filePath := range filesToRead {
102− absPath := filePath
103− if !filepath.IsAbs(absPath) {
104− wd := config.WorkingDirectory()
105− absPath = filepath.Join(wd, absPath)
106− }
107−
108− if getLastReadTime(absPath).IsZero() {
109− return NewTextErrorResponse(fmt.Sprintf("you must read the file %s before patching it. Use the FileRead tool first", filePath)), nil
110− }
111−
112− fileInfo, err := os.Stat(absPath)
113− if err != nil {
114− if os.IsNotExist(err) {
115− return NewTextErrorResponse(fmt.Sprintf("file not found: %s", absPath)), nil
116− }
117− return ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
118− }
119−
120− if fileInfo.IsDir() {
121− return NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", absPath)), nil
122− }
123−
124− modTime := fileInfo.ModTime()
125− lastRead := getLastReadTime(absPath)
126− if modTime.After(lastRead) {
127− return NewTextErrorResponse(
128− fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
129− absPath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
130− )), nil
131− }
132− }
133−
134− // Check for new files to ensure they don't already exist
135− filesToAdd := diff.IdentifyFilesAdded(params.PatchText)
136− for _, filePath := range filesToAdd {
137− absPath := filePath
138− if !filepath.IsAbs(absPath) {
139− wd := config.WorkingDirectory()
140− absPath = filepath.Join(wd, absPath)
141− }
142−
143− _, err := os.Stat(absPath)
144− if err == nil {
145− return NewTextErrorResponse(fmt.Sprintf("file already exists and cannot be added: %s", absPath)), nil
146− } else if !os.IsNotExist(err) {
147− return ToolResponse{}, fmt.Errorf("failed to check file: %w", err)
148− }
149− }
150−
151− // Load all required files
152− currentFiles := make(map[string]string)
153− for _, filePath := range filesToRead {
154− absPath := filePath
155− if !filepath.IsAbs(absPath) {
156− wd := config.WorkingDirectory()
157− absPath = filepath.Join(wd, absPath)
158− }
159−
160− content, err := os.ReadFile(absPath)
161− if err != nil {
162− return ToolResponse{}, fmt.Errorf("failed to read file %s: %w", absPath, err)
163− }
164− currentFiles[filePath] = string(content)
165− }
166−
167− // Process the patch
168− patch, fuzz, err := diff.TextToPatch(params.PatchText, currentFiles)
169− if err != nil {
170− return NewTextErrorResponse(fmt.Sprintf("failed to parse patch: %s", err)), nil
171− }
172−
173− if fuzz > 3 {
174− return NewTextErrorResponse(fmt.Sprintf("patch contains fuzzy matches (fuzz level: %d). Please make your context lines more precise", fuzz)), nil
175− }
176−
177− // Convert patch to commit
178− commit, err := diff.PatchToCommit(patch, currentFiles)
179− if err != nil {
180− return NewTextErrorResponse(fmt.Sprintf("failed to create commit from patch: %s", err)), nil
181− }
182−
183− // Get session ID and message ID
184− sessionID, messageID := GetContextValues(ctx)
185− if sessionID == "" || messageID == "" {
186− return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a patch")
187− }
188−
189− // Request permission for all changes
190− for path, change := range commit.Changes {
191− switch change.Type {
192− case diff.ActionAdd:
193− dir := filepath.Dir(path)
194− patchDiff, _, _ := diff.GenerateDiff("", *change.NewContent, path)
195− p := p.permissions.Request(
196− permission.CreatePermissionRequest{
197− SessionID: sessionID,
198− Path: dir,
199− ToolName: PatchToolName,
200− Action: "create",
201− Description: fmt.Sprintf("Create file %s", path),
202− Params: EditPermissionsParams{
203− FilePath: path,
204− Diff: patchDiff,
205− },
206− },
207− )
208− if !p {
209− return ToolResponse{}, permission.ErrorPermissionDenied
210− }
211− case diff.ActionUpdate:
212− currentContent := ""
213− if change.OldContent != nil {
214− currentContent = *change.OldContent
215− }
216− newContent := ""
217− if change.NewContent != nil {
218− newContent = *change.NewContent
219− }
220− patchDiff, _, _ := diff.GenerateDiff(currentContent, newContent, path)
221− dir := filepath.Dir(path)
222− p := p.permissions.Request(
223− permission.CreatePermissionRequest{
224− SessionID: sessionID,
225− Path: dir,
226− ToolName: PatchToolName,
227− Action: "update",
228− Description: fmt.Sprintf("Update file %s", path),
229− Params: EditPermissionsParams{
230− FilePath: path,
231− Diff: patchDiff,
232− },
233− },
234− )
235− if !p {
236− return ToolResponse{}, permission.ErrorPermissionDenied
237− }
238− case diff.ActionDelete:
239− dir := filepath.Dir(path)
240− patchDiff, _, _ := diff.GenerateDiff(*change.OldContent, "", path)
241− p := p.permissions.Request(
242− permission.CreatePermissionRequest{
243− SessionID: sessionID,
244− Path: dir,
245− ToolName: PatchToolName,
246− Action: "delete",
247− Description: fmt.Sprintf("Delete file %s", path),
248− Params: EditPermissionsParams{
249− FilePath: path,
250− Diff: patchDiff,
251− },
252− },
253− )
254− if !p {
255− return ToolResponse{}, permission.ErrorPermissionDenied
256− }
257− }
258− }
259−
260− // Apply the changes to the filesystem
261− err = diff.ApplyCommit(commit, func(path string, content string) error {
262− absPath := path
263− if !filepath.IsAbs(absPath) {
264− wd := config.WorkingDirectory()
265− absPath = filepath.Join(wd, absPath)
266− }
267−
268− // Create parent directories if needed
269− dir := filepath.Dir(absPath)
270− if err := os.MkdirAll(dir, 0o755); err != nil {
271− return fmt.Errorf("failed to create parent directories for %s: %w", absPath, err)
272− }
273−
274− return os.WriteFile(absPath, []byte(content), 0o644)
275− }, func(path string) error {
276− absPath := path
277− if !filepath.IsAbs(absPath) {
278− wd := config.WorkingDirectory()
279− absPath = filepath.Join(wd, absPath)
280− }
281− return os.Remove(absPath)
282− })
283− if err != nil {
284− return NewTextErrorResponse(fmt.Sprintf("failed to apply patch: %s", err)), nil
285− }
286−
287− // Update file history for all modified files
288− changedFiles := []string{}
289− totalAdditions := 0
290− totalRemovals := 0
291−
292− for path, change := range commit.Changes {
293− absPath := path
294− if !filepath.IsAbs(absPath) {
295− wd := config.WorkingDirectory()
296− absPath = filepath.Join(wd, absPath)
297− }
298− changedFiles = append(changedFiles, absPath)
299−
300− oldContent := ""
301− if change.OldContent != nil {
302− oldContent = *change.OldContent
303− }
304−
305− newContent := ""
306− if change.NewContent != nil {
307− newContent = *change.NewContent
308− }
309−
310− // Calculate diff statistics
311− _, additions, removals := diff.GenerateDiff(oldContent, newContent, path)
312− totalAdditions += additions
313− totalRemovals += removals
314−
315− // Update history
316− file, err := p.files.GetByPathAndSession(ctx, absPath, sessionID)
317− if err != nil && change.Type != diff.ActionAdd {
318− // If not adding a file, create history entry for existing file
319− _, err = p.files.Create(ctx, sessionID, absPath, oldContent)
320− if err != nil {
321− logging.Debug("Error creating file history", "error", err)
322− }
323− }
324−
325− if err == nil && change.Type != diff.ActionAdd && file.Content != oldContent {
326− // User manually changed content, store intermediate version
327− _, err = p.files.CreateVersion(ctx, sessionID, absPath, oldContent)
328− if err != nil {
329− logging.Debug("Error creating file history version", "error", err)
330− }
331− }
332−
333− // Store new version
334− if change.Type == diff.ActionDelete {
335− _, err = p.files.CreateVersion(ctx, sessionID, absPath, "")
336− } else {
337− _, err = p.files.CreateVersion(ctx, sessionID, absPath, newContent)
338− }
339− if err != nil {
340− logging.Debug("Error creating file history version", "error", err)
341− }
342−
343− // Record file operations
344− recordFileWrite(absPath)
345− recordFileRead(absPath)
346− }
347−
348− // Run LSP diagnostics on all changed files
349− for _, filePath := range changedFiles {
350− waitForLspDiagnostics(ctx, filePath, p.lspClients)
351− }
352−
353− result := fmt.Sprintf("Patch applied successfully. %d files changed, %d additions, %d removals",
354− len(changedFiles), totalAdditions, totalRemovals)
355−
356− diagnosticsText := ""
357− for _, filePath := range changedFiles {
358− diagnosticsText += getDiagnostics(filePath, p.lspClients)
359− }
360−
361− if diagnosticsText != "" {
362− result += "\n\nDiagnostics:\n" + diagnosticsText
363− }
364−
365− return WithResponseMetadata(
366− NewTextResponse(result),
367− PatchResponseMetadata{
368− FilesChanged: changedFiles,
369− Additions: totalAdditions,
370− Removals: totalRemovals,
371− }), nil
372−}
internal/llm/tools/shell/comparison_test.go−30
@@ -3,7 +3,6 @@ package shell
3import (
3import (
4 "context"
4 "context"
5 "os"
5 "os"
6− "runtime"
7 "testing"
6 "testing"
8 "time"
7 "time"
9
8
@@ -32,35 +31,6 @@ func TestShellPerformanceComparison(t *testing.T) {
32 t.Logf("Quick command took: %v", duration)
31 t.Logf("Quick command took: %v", duration)
33}
32}
34
33
35−func TestShellCPUUsageComparison(t *testing.T) {
36− tmpDir, err := os.MkdirTemp("", "shell-test")
37− require.NoError(t, err)
38− defer os.RemoveAll(tmpDir)
39−
40− shell := GetPersistentShell(tmpDir)
41− defer shell.Close()
42−
43− // Measure CPU and memory usage during a longer command
44− var m1, m2 runtime.MemStats
45− runtime.GC()
46− runtime.ReadMemStats(&m1)
47−
48− start := time.Now()
49− _, stderr, exitCode, _, err := shell.Exec(context.Background(), "sleep 0.1", 1000)
50− duration := time.Since(start)
51−
52− runtime.ReadMemStats(&m2)
53−
54− require.NoError(t, err)
55− assert.Equal(t, 0, exitCode)
56− assert.Empty(t, stderr)
57−
58− memGrowth := m2.Alloc - m1.Alloc
59− t.Logf("Sleep 0.1s command took: %v", duration)
60− t.Logf("Memory growth during polling: %d bytes", memGrowth)
61− t.Logf("GC cycles during test: %d", m2.NumGC-m1.NumGC)
62−}
63−
64// Benchmark CPU usage during polling
34// Benchmark CPU usage during polling
65func BenchmarkShellPolling(b *testing.B) {
35func BenchmarkShellPolling(b *testing.B) {
66 tmpDir, err := os.MkdirTemp("", "shell-bench")
36 tmpDir, err := os.MkdirTemp("", "shell-bench")
internal/llm/tools/shell/shell_test.go−25
@@ -4,35 +4,10 @@ import (
4 "context"
4 "context"
5 "os"
5 "os"
6 "testing"
6 "testing"
7− "time"
8
7
9− "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
8 "github.com/stretchr/testify/require"
11)
9)
12
10
13−func TestShellPerformanceImprovement(t *testing.T) {
14− // Create a temporary directory for the shell
15− tmpDir, err := os.MkdirTemp("", "shell-test")
16− require.NoError(t, err)
17− defer os.RemoveAll(tmpDir)
18−
19− shell := GetPersistentShell(tmpDir)
20− defer shell.Close()
21−
22− // Test that quick commands complete fast
23− start := time.Now()
24− stdout, stderr, exitCode, _, err := shell.Exec(context.Background(), "echo 'hello world'", 0)
25− duration := time.Since(start)
26−
27− require.NoError(t, err)
28− assert.Equal(t, 0, exitCode)
29− assert.Contains(t, stdout, "hello world")
30− assert.Empty(t, stderr)
31−
32− // Quick commands should complete very fast with our exponential backoff
33− assert.Less(t, duration, 50*time.Millisecond, "Quick command should complete fast with exponential backoff")
34−}
35−
36// Benchmark to measure CPU efficiency
11// Benchmark to measure CPU efficiency
37func BenchmarkShellQuickCommands(b *testing.B) {
12func BenchmarkShellQuickCommands(b *testing.B) {
38 tmpDir, err := os.MkdirTemp("", "shell-bench")
13 tmpDir, err := os.MkdirTemp("", "shell-bench")
internal/llm/tools/sourcegraph.go+15−48
@@ -8,7 +8,6 @@ import (
8 "io"
8 "io"
9 "net/http"
9 "net/http"
10 "strings"
10 "strings"
11− "sync"
12 "time"
11 "time"
13)
12)
14
13
@@ -25,9 +24,7 @@ type SourcegraphResponseMetadata struct {
25}
24}
26
25
27type sourcegraphTool struct {
26type sourcegraphTool struct {
28− client *http.Client
27+ client *http.Client
29− clientPool map[int]*http.Client
30− clientPoolMu sync.RWMutex
31}
28}
32
29
33const (
30const (
@@ -138,50 +135,9 @@ func NewSourcegraphTool() BaseTool {
138 IdleConnTimeout: 90 * time.Second,
135 IdleConnTimeout: 90 * time.Second,
139 },
136 },
140 },
137 },
141− clientPool: make(map[int]*http.Client),
142 }
138 }
143}
139}
144
140
145−// getClientForTimeout returns a cached client for the given timeout or the default client
146−func (t *sourcegraphTool) getClientForTimeout(timeout int) *http.Client {
147− if timeout <= 0 {
148− return t.client
149− }
150−
151− maxTimeout := 120 // 2 minutes
152− if timeout > maxTimeout {
153− timeout = maxTimeout
154− }
155−
156− // Check if we have a cached client for this timeout
157− t.clientPoolMu.RLock()
158− if client, exists := t.clientPool[timeout]; exists {
159− t.clientPoolMu.RUnlock()
160− return client
161− }
162− t.clientPoolMu.RUnlock()
163−
164− // Create and cache a new client
165− t.clientPoolMu.Lock()
166− defer t.clientPoolMu.Unlock()
167−
168− // Double-check in case another goroutine created it
169− if client, exists := t.clientPool[timeout]; exists {
170− return client
171− }
172−
173− client := &http.Client{
174− Timeout: time.Duration(timeout) * time.Second,
175− Transport: &http.Transport{
176− MaxIdleConns: 100,
177− MaxIdleConnsPerHost: 10,
178− IdleConnTimeout: 90 * time.Second,
179− },
180− }
181− t.clientPool[timeout] = client
182− return client
183−}
184−
185func (t *sourcegraphTool) Info() ToolInfo {
141func (t *sourcegraphTool) Info() ToolInfo {
186 return ToolInfo{
142 return ToolInfo{
187 Name: SourcegraphToolName,
143 Name: SourcegraphToolName,
@@ -227,7 +183,18 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
227 if params.ContextWindow <= 0 {
183 if params.ContextWindow <= 0 {
228 params.ContextWindow = 10 // Default context window
184 params.ContextWindow = 10 // Default context window
229 }
185 }
230− client := t.getClientForTimeout(params.Timeout)
186+
187+ // Handle timeout with context
188+ requestCtx := ctx
189+ if params.Timeout > 0 {
190+ maxTimeout := 120 // 2 minutes
191+ if params.Timeout > maxTimeout {
192+ params.Timeout = maxTimeout
193+ }
194+ var cancel context.CancelFunc
195+ requestCtx, cancel = context.WithTimeout(ctx, time.Duration(params.Timeout)*time.Second)
196+ defer cancel()
197+ }
231
198
232 type graphqlRequest struct {
199 type graphqlRequest struct {
233 Query string `json:"query"`
200 Query string `json:"query"`
@@ -248,7 +215,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
248 graphqlQuery := string(graphqlQueryBytes)
215 graphqlQuery := string(graphqlQueryBytes)
249
216
250 req, err := http.NewRequestWithContext(
217 req, err := http.NewRequestWithContext(
251− ctx,
218+ requestCtx,
252 "POST",
219 "POST",
253 "https://sourcegraph.com/.api/graphql",
220 "https://sourcegraph.com/.api/graphql",
254 bytes.NewBuffer([]byte(graphqlQuery)),
221 bytes.NewBuffer([]byte(graphqlQuery)),
@@ -260,7 +227,7 @@ func (t *sourcegraphTool) Run(ctx context.Context, call ToolCall) (ToolResponse,
260 req.Header.Set("Content-Type", "application/json")
227 req.Header.Set("Content-Type", "application/json")
261 req.Header.Set("User-Agent", "crush/1.0")
228 req.Header.Set("User-Agent", "crush/1.0")
262
229
263− resp, err := client.Do(req)
230+ resp, err := t.client.Do(req)
264 if err != nil {
231 if err != nil {
265 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
232 return ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
266 }
233 }
internal/lsp/watcher/watcher.go+25−9
@@ -257,7 +257,10 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
257 }
257 }
258 }
258 }
259
259
260+ // Collect all files to open first
261+ var filesToOpen []string
262+
260− // For each pattern, find and open matching files
263+ // For each pattern, find matching files
261 for _, pattern := range patterns {
264 for _, pattern := range patterns {
262 // Use doublestar.Glob to find files matching the pattern (supports ** patterns)
265 // Use doublestar.Glob to find files matching the pattern (supports ** patterns)
263 matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
266 matches, err := doublestar.Glob(os.DirFS(w.workspacePath), pattern)
@@ -278,7 +281,23 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
278 continue
281 continue
279 }
282 }
280
283
281− // Open the file
284+ filesToOpen = append(filesToOpen, fullPath)
285+
286+ // Limit the number of files per pattern
287+ if len(filesToOpen) >= 5 && (serverName != "java" && serverName != "jdtls") {
288+ break
289+ }
290+ }
291+ }
292+
293+ // Open files in batches to reduce overhead
294+ batchSize := 3
295+ for i := 0; i < len(filesToOpen); i += batchSize {
296+ end := min(i+batchSize, len(filesToOpen))
297+
298+ // Open batch of files
299+ for j := i; j < end; j++ {
300+ fullPath := filesToOpen[j]
282 if err := w.client.OpenFile(ctx, fullPath); err != nil {
301 if err := w.client.OpenFile(ctx, fullPath); err != nil {
283 if cnf.DebugLSP {
302 if cnf.DebugLSP {
284 logging.Debug("Error opening high-priority file", "path", fullPath, "error", err)
303 logging.Debug("Error opening high-priority file", "path", fullPath, "error", err)
@@ -289,14 +308,11 @@ func (w *WorkspaceWatcher) openHighPriorityFiles(ctx context.Context, serverName
289 logging.Debug("Opened high-priority file", "path", fullPath)
308 logging.Debug("Opened high-priority file", "path", fullPath)
290 }
309 }
291 }
310 }
311+ }
292
312
293− // Add a small delay to prevent overwhelming the server
313+ // Only add delay between batches, not individual files
314+ if end < len(filesToOpen) {
294− time.Sleep(20 * time.Millisecond)
315+ time.Sleep(50 * time.Millisecond)
295−
296− // Limit the number of files opened per pattern
297− if filesOpened >= 5 && (serverName != "java" && serverName != "jdtls") {
298− break
299− }
300 }
316 }
301 }
317 }
302
318
internal/tui/components/chat/editor/editor.go+2−2
@@ -12,7 +12,7 @@ import (
12 "github.com/charmbracelet/bubbles/v2/textarea"
12 "github.com/charmbracelet/bubbles/v2/textarea"
13 tea "github.com/charmbracelet/bubbletea/v2"
13 tea "github.com/charmbracelet/bubbletea/v2"
14 "github.com/charmbracelet/crush/internal/app"
14 "github.com/charmbracelet/crush/internal/app"
15− "github.com/charmbracelet/crush/internal/fileutil"
15+ "github.com/charmbracelet/crush/internal/fsext"
16 "github.com/charmbracelet/crush/internal/logging"
16 "github.com/charmbracelet/crush/internal/logging"
17 "github.com/charmbracelet/crush/internal/message"
17 "github.com/charmbracelet/crush/internal/message"
18 "github.com/charmbracelet/crush/internal/session"
18 "github.com/charmbracelet/crush/internal/session"
@@ -327,7 +327,7 @@ func (m *editorCmp) SetPosition(x, y int) tea.Cmd {
327}
327}
328
328
329func (m *editorCmp) startCompletions() tea.Msg {
329func (m *editorCmp) startCompletions() tea.Msg {
330− files, _, _ := fileutil.ListDirectory(".", []string{}, 0)
330+ files, _, _ := fsext.ListDirectory(".", []string{}, 0)
331 completionItems := make([]completions.Completion, 0, len(files))
331 completionItems := make([]completions.Completion, 0, len(files))
332 for _, file := range files {
332 for _, file := range files {
333 file = strings.TrimPrefix(file, "./")
333 file = strings.TrimPrefix(file, "./")
internal/tui/components/chat/messages/messages.go+1−1
@@ -167,7 +167,7 @@ func (m *messageCmp) renderAssistantMessage() string {
167 case message.FinishReasonPermissionDenied:
167 case message.FinishReasonPermissionDenied:
168 infoMsg = "permission denied"
168 infoMsg = "permission denied"
169 }
169 }
170− assistant := t.S().Muted.Render(fmt.Sprintf("⬡ %s (%s)", models.SupportedModels[m.message.Model].Name, infoMsg))
170+ assistant := t.S().Muted.Render(fmt.Sprintf("%s %s (%s)", styles.ModelIcon, models.SupportedModels[m.message.Model].Name, infoMsg))
171 parts = append(parts, core.Section(assistant, m.textWidth()))
171 parts = append(parts, core.Section(assistant, m.textWidth()))
172 }
172 }
173
173
internal/tui/components/chat/messages/renderer.go+12−44
@@ -6,11 +6,11 @@ import (
6 "strings"
6 "strings"
7 "time"
7 "time"
8
8
9− "github.com/charmbracelet/crush/internal/diff"
9+ "github.com/charmbracelet/crush/internal/fsext"
10− "github.com/charmbracelet/crush/internal/fileutil"
11 "github.com/charmbracelet/crush/internal/highlight"
10 "github.com/charmbracelet/crush/internal/highlight"
12 "github.com/charmbracelet/crush/internal/llm/agent"
11 "github.com/charmbracelet/crush/internal/llm/agent"
13 "github.com/charmbracelet/crush/internal/llm/tools"
12 "github.com/charmbracelet/crush/internal/llm/tools"
13+ "github.com/charmbracelet/crush/internal/tui/components/core"
14 "github.com/charmbracelet/crush/internal/tui/styles"
14 "github.com/charmbracelet/crush/internal/tui/styles"
15 "github.com/charmbracelet/lipgloss/v2"
15 "github.com/charmbracelet/lipgloss/v2"
16 "github.com/charmbracelet/lipgloss/v2/tree"
16 "github.com/charmbracelet/lipgloss/v2/tree"
@@ -148,7 +148,6 @@ func init() {
148 registry.register(tools.GrepToolName, func() renderer { return grepRenderer{} })
148 registry.register(tools.GrepToolName, func() renderer { return grepRenderer{} })
149 registry.register(tools.LSToolName, func() renderer { return lsRenderer{} })
149 registry.register(tools.LSToolName, func() renderer { return lsRenderer{} })
150 registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
150 registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
151− registry.register(tools.PatchToolName, func() renderer { return patchRenderer{} })
152 registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
151 registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
153 registry.register(agent.AgentToolName, func() renderer { return agentRenderer{} })
152 registry.register(agent.AgentToolName, func() renderer { return agentRenderer{} })
154}
153}
@@ -209,7 +208,7 @@ func (vr viewRenderer) Render(v *toolCallCmp) string {
209 return vr.renderError(v, "Invalid view parameters")
208 return vr.renderError(v, "Invalid view parameters")
210 }
209 }
211
210
212− file := fileutil.PrettyPath(params.FilePath)
211+ file := fsext.PrettyPath(params.FilePath)
213 args := newParamBuilder().
212 args := newParamBuilder().
214 addMain(file).
213 addMain(file).
215 addKeyValue("limit", formatNonZero(params.Limit)).
214 addKeyValue("limit", formatNonZero(params.Limit)).
@@ -249,7 +248,7 @@ func (er editRenderer) Render(v *toolCallCmp) string {
249 return er.renderError(v, "Invalid edit parameters")
248 return er.renderError(v, "Invalid edit parameters")
250 }
249 }
251
250
252− file := fileutil.PrettyPath(params.FilePath)
251+ file := fsext.PrettyPath(params.FilePath)
253 args := newParamBuilder().addMain(file).build()
252 args := newParamBuilder().addMain(file).build()
254
253
255 return er.renderWithParams(v, "Edit", args, func() string {
254 return er.renderWithParams(v, "Edit", args, func() string {
@@ -258,9 +257,12 @@ func (er editRenderer) Render(v *toolCallCmp) string {
258 return renderPlainContent(v, v.result.Content)
257 return renderPlainContent(v, v.result.Content)
259 }
258 }
260
259
261− trunc := truncateHeight(meta.Diff, responseContextHeight)
260+ formatter := core.DiffFormatter().
262− diffView, _ := diff.FormatDiff(trunc, diff.WithTotalWidth(v.textWidth()-2))
261+ Before(fsext.PrettyPath(params.FilePath), meta.OldContent).
263− return diffView
262+ After(fsext.PrettyPath(params.FilePath), meta.NewContent).
263+ Split().
264+ Width(v.textWidth() - 2) // -2 for padding
265+ return formatter.String()
264 })
266 })
265}
267}
266
268
@@ -280,7 +282,7 @@ func (wr writeRenderer) Render(v *toolCallCmp) string {
280 return wr.renderError(v, "Invalid write parameters")
282 return wr.renderError(v, "Invalid write parameters")
281 }
283 }
282
284
283− file := fileutil.PrettyPath(params.FilePath)
285+ file := fsext.PrettyPath(params.FilePath)
284 args := newParamBuilder().addMain(file).build()
286 args := newParamBuilder().addMain(file).build()
285
287
286 return wr.renderWithParams(v, "Write", args, func() string {
288 return wr.renderWithParams(v, "Write", args, func() string {
@@ -410,7 +412,7 @@ func (lr lsRenderer) Render(v *toolCallCmp) string {
410 if path == "" {
412 if path == "" {
411 path = "."
413 path = "."
412 }
414 }
413− path = fileutil.PrettyPath(path)
415+ path = fsext.PrettyPath(path)
414
416
415 args := newParamBuilder().addMain(path).build()
417 args := newParamBuilder().addMain(path).build()
416
418
@@ -446,38 +448,6 @@ func (sr sourcegraphRenderer) Render(v *toolCallCmp) string {
446 })
448 })
447}
449}
448
450
449−// -----------------------------------------------------------------------------
450−// Patch renderer
451−// -----------------------------------------------------------------------------
452−
453−// patchRenderer handles multi-file patches with change summaries
454−type patchRenderer struct {
455− baseRenderer
456−}
457−
458−// Render displays patch summary with file count and change statistics
459−func (pr patchRenderer) Render(v *toolCallCmp) string {
460− var params tools.PatchParams
461− if err := pr.unmarshalParams(v.call.Input, &params); err != nil {
462− return pr.renderError(v, "Invalid patch parameters")
463− }
464−
465− args := newParamBuilder().addMain("multiple files").build()
466−
467− return pr.renderWithParams(v, "Patch", args, func() string {
468− var meta tools.PatchResponseMetadata
469− if err := pr.unmarshalParams(v.result.Metadata, &meta); err != nil {
470− return renderPlainContent(v, v.result.Content)
471− }
472−
473− summary := fmt.Sprintf("Changed %d files (%d+ %d-)",
474− len(meta.FilesChanged), meta.Additions, meta.Removals)
475− filesList := strings.Join(meta.FilesChanged, "\n")
476−
477− return renderPlainContent(v, summary+"\n\n"+filesList)
478− })
479−}
480−
481// -----------------------------------------------------------------------------
451// -----------------------------------------------------------------------------
482// Diagnostics renderer
452// Diagnostics renderer
483// -----------------------------------------------------------------------------
453// -----------------------------------------------------------------------------
@@ -711,8 +681,6 @@ func prettifyToolName(name string) string {
711 return "View"
681 return "View"
712 case tools.WriteToolName:
682 case tools.WriteToolName:
713 return "Write"
683 return "Write"
714− case tools.PatchToolName:
715− return "Patch"
716 default:
684 default:
717 return name
685 return name
718 }
686 }
internal/tui/components/chat/sidebar/sidebar.go+306−7
@@ -1,11 +1,22 @@
1package sidebar
1package sidebar
2
2
3import (
3import (
4+ "context"
5+ "fmt"
4 "os"
6 "os"
7+ "sort"
5 "strings"
8 "strings"
9+ "sync"
6
10
7 tea "github.com/charmbracelet/bubbletea/v2"
11 tea "github.com/charmbracelet/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/config"
12 "github.com/charmbracelet/crush/internal/config"
13+ "github.com/charmbracelet/crush/internal/diff"
14+ "github.com/charmbracelet/crush/internal/fsext"
15+ "github.com/charmbracelet/crush/internal/history"
16+ "github.com/charmbracelet/crush/internal/llm/models"
17+ "github.com/charmbracelet/crush/internal/logging"
18+ "github.com/charmbracelet/crush/internal/lsp"
19+ "github.com/charmbracelet/crush/internal/lsp/protocol"
9 "github.com/charmbracelet/crush/internal/pubsub"
20 "github.com/charmbracelet/crush/internal/pubsub"
10 "github.com/charmbracelet/crush/internal/session"
21 "github.com/charmbracelet/crush/internal/session"
11 "github.com/charmbracelet/crush/internal/tui/components/chat"
22 "github.com/charmbracelet/crush/internal/tui/components/chat"
@@ -16,12 +27,28 @@ import (
16 "github.com/charmbracelet/crush/internal/tui/util"
27 "github.com/charmbracelet/crush/internal/tui/util"
17 "github.com/charmbracelet/crush/internal/version"
28 "github.com/charmbracelet/crush/internal/version"
18 "github.com/charmbracelet/lipgloss/v2"
29 "github.com/charmbracelet/lipgloss/v2"
30+ "github.com/charmbracelet/x/ansi"
19)
31)
20
32
21const (
33const (
22 logoBreakpoint = 65
34 logoBreakpoint = 65
23)
35)
24
36
37+type FileHistory struct {
38+ initialVersion history.File
39+ latestVersion history.File
40+}
41+
42+type SessionFile struct {
43+ History FileHistory
44+ FilePath string
45+ Additions int
46+ Deletions int
47+}
48+type SessionFilesMsg struct {
49+ Files []SessionFile
50+}
51+
25type Sidebar interface {
52type Sidebar interface {
26 util.Model
53 util.Model
27 layout.Sizeable
54 layout.Sizeable
@@ -32,10 +59,17 @@ type sidebarCmp struct {
32 session session.Session
59 session session.Session
33 logo string
60 logo string
34 cwd string
61 cwd string
62+ lspClients map[string]*lsp.Client
63+ history history.Service
64+ // Using a sync map here because we might receive file history events concurrently
65+ files sync.Map
35}
66}
36
67
37−func NewSidebarCmp() Sidebar {
68+func NewSidebarCmp(history history.Service, lspClients map[string]*lsp.Client) Sidebar {
38− return &sidebarCmp{}
69+ return &sidebarCmp{
70+ lspClients: lspClients,
71+ history: history,
72+ }
39}
73}
40
74
41func (m *sidebarCmp) Init() tea.Cmd {
75func (m *sidebarCmp) Init() tea.Cmd {
@@ -50,8 +84,19 @@ func (m *sidebarCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
50 if msg.ID != m.session.ID {
84 if msg.ID != m.session.ID {
51 m.session = msg
85 m.session = msg
52 }
86 }
87+ return m, m.loadSessionFiles
88+ case SessionFilesMsg:
89+ m.files = sync.Map{}
90+ for _, file := range msg.Files {
91+ m.files.Store(file.FilePath, file)
92+ }
93+ return m, nil
94+
53 case chat.SessionClearedMsg:
95 case chat.SessionClearedMsg:
54 m.session = session.Session{}
96 m.session = session.Session{}
97+ case pubsub.Event[history.File]:
98+ logging.Info("sidebar", "Received file history event", "file", msg.Payload.Path, "session", msg.Payload.SessionID)
99+ return m, m.handleFileHistoryEvent(msg)
55 case pubsub.Event[session.Session]:
100 case pubsub.Event[session.Session]:
56 if msg.Type == pubsub.UpdatedEvent {
101 if msg.Type == pubsub.UpdatedEvent {
57 if m.session.ID == msg.Payload.ID {
102 if m.session.ID == msg.Payload.ID {
@@ -74,6 +119,13 @@ func (m *sidebarCmp) View() tea.View {
74
119
75 parts = append(parts,
120 parts = append(parts,
76 m.cwd,
121 m.cwd,
122+ "",
123+ m.currentModelBlock(),
124+ )
125+ if m.session.ID != "" {
126+ parts = append(parts, "", m.filesBlock())
127+ }
128+ parts = append(parts,
77 "",
129 "",
78 m.lspBlock(),
130 m.lspBlock(),
79 "",
131 "",
@@ -85,6 +137,91 @@ func (m *sidebarCmp) View() tea.View {
85 )
137 )
86}
138}
87
139
140+func (m *sidebarCmp) handleFileHistoryEvent(event pubsub.Event[history.File]) tea.Cmd {
141+ return func() tea.Msg {
142+ file := event.Payload
143+ found := false
144+ m.files.Range(func(key, value any) bool {
145+ existing := value.(SessionFile)
146+ if existing.FilePath == file.Path {
147+ if existing.History.latestVersion.Version < file.Version {
148+ existing.History.latestVersion = file
149+ } else if file.Version == 0 {
150+ existing.History.initialVersion = file
151+ } else {
152+ // If the version is not greater than the latest, we ignore it
153+ return true
154+ }
155+ before := existing.History.initialVersion.Content
156+ after := existing.History.latestVersion.Content
157+ path := existing.History.initialVersion.Path
158+ _, additions, deletions := diff.GenerateDiff(before, after, path)
159+ existing.Additions = additions
160+ existing.Deletions = deletions
161+ m.files.Store(file.Path, existing)
162+ found = true
163+ return false
164+ }
165+ return true
166+ })
167+ if found {
168+ return nil
169+ }
170+ sf := SessionFile{
171+ History: FileHistory{
172+ initialVersion: file,
173+ latestVersion: file,
174+ },
175+ FilePath: file.Path,
176+ Additions: 0,
177+ Deletions: 0,
178+ }
179+ m.files.Store(file.Path, sf)
180+ return nil
181+ }
182+}
183+
184+func (m *sidebarCmp) loadSessionFiles() tea.Msg {
185+ files, err := m.history.ListBySession(context.Background(), m.session.ID)
186+ if err != nil {
187+ return util.InfoMsg{
188+ Type: util.InfoTypeError,
189+ Msg: err.Error(),
190+ }
191+ }
192+
193+ fileMap := make(map[string]FileHistory)
194+
195+ for _, file := range files {
196+ if existing, ok := fileMap[file.Path]; ok {
197+ // Update the latest version
198+ existing.latestVersion = file
199+ fileMap[file.Path] = existing
200+ } else {
201+ // Add the initial version
202+ fileMap[file.Path] = FileHistory{
203+ initialVersion: file,
204+ latestVersion: file,
205+ }
206+ }
207+ }
208+
209+ sessionFiles := make([]SessionFile, 0, len(fileMap))
210+ for path, fh := range fileMap {
211+ _, additions, deletions := diff.GenerateDiff(fh.initialVersion.Content, fh.latestVersion.Content, fh.initialVersion.Path)
212+ sessionFiles = append(sessionFiles, SessionFile{
213+ History: fh,
214+ FilePath: path,
215+ Additions: additions,
216+ Deletions: deletions,
217+ })
218+ }
219+
220+ return SessionFilesMsg{
221+ Files: sessionFiles,
222+ }
223+}
224+
88func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
225func (m *sidebarCmp) SetSize(width, height int) tea.Cmd {
89 if width < logoBreakpoint && m.width >= logoBreakpoint {
226 if width < logoBreakpoint && m.width >= logoBreakpoint {
90 m.logo = m.logoBlock(true)
227 m.logo = m.logoBlock(true)
@@ -112,11 +249,74 @@ func (m *sidebarCmp) logoBlock(compact bool) string {
112 })
249 })
113}
250}
114
251
252+func (m *sidebarCmp) filesBlock() string {
253+ maxWidth := min(m.width, 58)
254+ t := styles.CurrentTheme()
255+
256+ section := t.S().Subtle.Render(
257+ core.Section("Modified Files", maxWidth),
258+ )
259+
260+ files := make([]SessionFile, 0)
261+ m.files.Range(func(key, value any) bool {
262+ file := value.(SessionFile)
263+ files = append(files, file)
264+ return true // continue iterating
265+ })
266+ if len(files) == 0 {
267+ return lipgloss.JoinVertical(
268+ lipgloss.Left,
269+ section,
270+ "",
271+ t.S().Base.Foreground(t.Border).Render("None"),
272+ )
273+ }
274+
275+ fileList := []string{section, ""}
276+ // order files by the latest version's created time
277+ sort.Slice(files, func(i, j int) bool {
278+ return files[i].History.latestVersion.CreatedAt > files[j].History.latestVersion.CreatedAt
279+ })
280+
281+ for _, file := range files {
282+ // Extract just the filename from the path
283+
284+ // Create status indicators for additions/deletions
285+ var statusParts []string
286+ if file.Additions > 0 {
287+ statusParts = append(statusParts, t.S().Base.Foreground(t.Success).Render(fmt.Sprintf("+%d", file.Additions)))
288+ }
289+ if file.Deletions > 0 {
290+ statusParts = append(statusParts, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("-%d", file.Deletions)))
291+ }
292+
293+ extraContent := strings.Join(statusParts, " ")
294+ filePath := fsext.DirTrim(fsext.PrettyPath(file.FilePath), 2)
295+ filePath = ansi.Truncate(filePath, maxWidth-lipgloss.Width(extraContent)-2, "…")
296+ fileList = append(fileList,
297+ core.Status(
298+ core.StatusOpts{
299+ IconColor: t.FgMuted,
300+ NoIcon: true,
301+ Title: filePath,
302+ ExtraContent: extraContent,
303+ },
304+ m.width,
305+ ),
306+ )
307+ }
308+
309+ return lipgloss.JoinVertical(
310+ lipgloss.Left,
311+ fileList...,
312+ )
313+}
314+
115func (m *sidebarCmp) lspBlock() string {
315func (m *sidebarCmp) lspBlock() string {
116 maxWidth := min(m.width, 58)
316 maxWidth := min(m.width, 58)
117 t := styles.CurrentTheme()
317 t := styles.CurrentTheme()
118
318
119− section := t.S().Muted.Render(
319+ section := t.S().Subtle.Render(
120 core.Section("LSPs", maxWidth),
320 core.Section("LSPs", maxWidth),
121 )
321 )
122
322
@@ -137,12 +337,43 @@ func (m *sidebarCmp) lspBlock() string {
137 if l.Disabled {
337 if l.Disabled {
138 iconColor = t.FgMuted
338 iconColor = t.FgMuted
139 }
339 }
340+ lspErrs := map[protocol.DiagnosticSeverity]int{
341+ protocol.SeverityError: 0,
342+ protocol.SeverityWarning: 0,
343+ protocol.SeverityHint: 0,
344+ protocol.SeverityInformation: 0,
345+ }
346+ if client, ok := m.lspClients[n]; ok {
347+ for _, diagnostics := range client.GetDiagnostics() {
348+ for _, diagnostic := range diagnostics {
349+ if severity, ok := lspErrs[diagnostic.Severity]; ok {
350+ lspErrs[diagnostic.Severity] = severity + 1
351+ }
352+ }
353+ }
354+ }
355+
356+ errs := []string{}
357+ if lspErrs[protocol.SeverityError] > 0 {
358+ errs = append(errs, t.S().Base.Foreground(t.Error).Render(fmt.Sprintf("%s%d", styles.ErrorIcon, lspErrs[protocol.SeverityError])))
359+ }
360+ if lspErrs[protocol.SeverityWarning] > 0 {
361+ errs = append(errs, t.S().Base.Foreground(t.Warning).Render(fmt.Sprintf("%s%d", styles.WarningIcon, lspErrs[protocol.SeverityWarning])))
362+ }
363+ if lspErrs[protocol.SeverityHint] > 0 {
364+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.HintIcon, lspErrs[protocol.SeverityHint])))
365+ }
366+ if lspErrs[protocol.SeverityInformation] > 0 {
367+ errs = append(errs, t.S().Base.Foreground(t.FgHalfMuted).Render(fmt.Sprintf("%s%d", styles.InfoIcon, lspErrs[protocol.SeverityInformation])))
368+ }
369+
140 lspList = append(lspList,
370 lspList = append(lspList,
141 core.Status(
371 core.Status(
142 core.StatusOpts{
372 core.StatusOpts{
143− IconColor: iconColor,
373+ IconColor: iconColor,
144− Title: n,
374+ Title: n,
145− Description: l.Command,
375+ Description: l.Command,
376+ ExtraContent: strings.Join(errs, " "),
146 },
377 },
147 m.width,
378 m.width,
148 ),
379 ),
@@ -159,7 +390,7 @@ func (m *sidebarCmp) mcpBlock() string {
159 maxWidth := min(m.width, 58)
390 maxWidth := min(m.width, 58)
160 t := styles.CurrentTheme()
391 t := styles.CurrentTheme()
161
392
162− section := t.S().Muted.Render(
393+ section := t.S().Subtle.Render(
163 core.Section("MCPs", maxWidth),
394 core.Section("MCPs", maxWidth),
164 )
395 )
165
396
@@ -195,6 +426,74 @@ func (m *sidebarCmp) mcpBlock() string {
195 )
426 )
196}
427}
197
428
429+func formatTokensAndCost(tokens, contextWindow int64, cost float64) string {
430+ t := styles.CurrentTheme()
431+ // Format tokens in human-readable format (e.g., 110K, 1.2M)
432+ var formattedTokens string
433+ switch {
434+ case tokens >= 1_000_000:
435+ formattedTokens = fmt.Sprintf("%.1fM", float64(tokens)/1_000_000)
436+ case tokens >= 1_000:
437+ formattedTokens = fmt.Sprintf("%.1fK", float64(tokens)/1_000)
438+ default:
439+ formattedTokens = fmt.Sprintf("%d", tokens)
440+ }
441+
442+ // Remove .0 suffix if present
443+ if strings.HasSuffix(formattedTokens, ".0K") {
444+ formattedTokens = strings.Replace(formattedTokens, ".0K", "K", 1)
445+ }
446+ if strings.HasSuffix(formattedTokens, ".0M") {
447+ formattedTokens = strings.Replace(formattedTokens, ".0M", "M", 1)
448+ }
449+
450+ percentage := (float64(tokens) / float64(contextWindow)) * 100
451+
452+ baseStyle := t.S().Base
453+
454+ formattedCost := baseStyle.Foreground(t.FgMuted).Render(fmt.Sprintf("$%.2f", cost))
455+
456+ formattedTokens = baseStyle.Foreground(t.FgSubtle).Render(fmt.Sprintf("(%s)", formattedTokens))
457+ formattedPercentage := baseStyle.Foreground(t.FgMuted).Render(fmt.Sprintf("%d%%", int(percentage)))
458+ formattedTokens = fmt.Sprintf("%s %s", formattedPercentage, formattedTokens)
459+ if percentage > 80 {
460+ // add the warning icon
461+ formattedTokens = fmt.Sprintf("%s %s", styles.WarningIcon, formattedTokens)
462+ }
463+
464+ return fmt.Sprintf("%s %s", formattedTokens, formattedCost)
465+}
466+
467+func (s *sidebarCmp) currentModelBlock() string {
468+ cfg := config.Get()
469+ agentCfg := cfg.Agents[config.AgentCoder]
470+ selectedModelID := agentCfg.Model
471+ model := models.SupportedModels[selectedModelID]
472+
473+ t := styles.CurrentTheme()
474+
475+ modelIcon := t.S().Base.Foreground(t.FgSubtle).Render(styles.ModelIcon)
476+ modelName := t.S().Text.Render(model.Name)
477+ modelInfo := fmt.Sprintf("%s %s", modelIcon, modelName)
478+ parts := []string{
479+ modelInfo,
480+ }
481+ if s.session.ID != "" {
482+ parts = append(
483+ parts,
484+ " "+formatTokensAndCost(
485+ s.session.CompletionTokens+s.session.PromptTokens,
486+ model.ContextWindow,
487+ s.session.Cost,
488+ ),
489+ )
490+ }
491+ return lipgloss.JoinVertical(
492+ lipgloss.Left,
493+ parts...,
494+ )
495+}
496+
198func cwd() string {
497func cwd() string {
199 cwd := config.WorkingDirectory()
498 cwd := config.WorkingDirectory()
200 t := styles.CurrentTheme()
499 t := styles.CurrentTheme()
internal/tui/components/core/helpers.go+31−7
@@ -4,6 +4,8 @@ import (
4 "image/color"
4 "image/color"
5 "strings"
5 "strings"
6
6
7+ "github.com/alecthomas/chroma/v2"
8+ "github.com/charmbracelet/crush/internal/exp/diffview"
7 "github.com/charmbracelet/crush/internal/tui/styles"
9 "github.com/charmbracelet/crush/internal/tui/styles"
8 "github.com/charmbracelet/lipgloss/v2"
10 "github.com/charmbracelet/lipgloss/v2"
9 "github.com/charmbracelet/x/ansi"
11 "github.com/charmbracelet/x/ansi"
@@ -38,10 +40,12 @@ func Title(title string, width int) string {
38type StatusOpts struct {
40type StatusOpts struct {
39 Icon string
41 Icon string
40 IconColor color.Color
42 IconColor color.Color
43+ NoIcon bool // If true, no icon will be displayed
41 Title string
44 Title string
42 TitleColor color.Color
45 TitleColor color.Color
43 Description string
46 Description string
44 DescriptionColor color.Color
47 DescriptionColor color.Color
48+ ExtraContent string // Additional content to append after the description
45}
49}
46
50
47func Status(ops StatusOpts, width int) string {
51func Status(ops StatusOpts, width int) string {
@@ -50,6 +54,8 @@ func Status(ops StatusOpts, width int) string {
50 iconColor := t.Success
54 iconColor := t.Success
51 if ops.Icon != "" {
55 if ops.Icon != "" {
52 icon = ops.Icon
56 icon = ops.Icon
57+ } else if ops.NoIcon {
58+ icon = ""
53 }
59 }
54 if ops.IconColor != nil {
60 if ops.IconColor != nil {
55 iconColor = ops.IconColor
61 iconColor = ops.IconColor
@@ -64,17 +70,26 @@ func Status(ops StatusOpts, width int) string {
64 if ops.DescriptionColor != nil {
70 if ops.DescriptionColor != nil {
65 descriptionColor = ops.DescriptionColor
71 descriptionColor = ops.DescriptionColor
66 }
72 }
67− icon = t.S().Base.Foreground(iconColor).Render(icon)
68 title = t.S().Base.Foreground(titleColor).Render(title)
73 title = t.S().Base.Foreground(titleColor).Render(title)
69 if description != "" {
74 if description != "" {
75+ extraContent := len(ops.ExtraContent)
76+ if extraContent > 0 {
77+ extraContent += 1
78+ }
70− description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2, "…")
79+ description = ansi.Truncate(description, width-lipgloss.Width(icon)-lipgloss.Width(title)-2-extraContent, "…")
71 }
80 }
72 description = t.S().Base.Foreground(descriptionColor).Render(description)
81 description = t.S().Base.Foreground(descriptionColor).Render(description)
73− return strings.Join([]string{
82+
74− icon,
83+ content := []string{}
75− title,
84+ if icon != "" {
85+ content = append(content, t.S().Base.Foreground(iconColor).Render(icon))
86+ }
76− description,
87+ content = append(content, title, description)
77− }, " ")
88+ if ops.ExtraContent != "" {
89+ content = append(content, ops.ExtraContent)
90+ }
91+
92+ return strings.Join(content, " ")
78}
93}
79
94
80type ButtonOpts struct {
95type ButtonOpts struct {
@@ -131,3 +146,12 @@ func SelectableButtons(buttons []ButtonOpts, spacing string) string {
131
146
132 return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
147 return lipgloss.JoinHorizontal(lipgloss.Left, parts...)
133}
148}
149+
150+func DiffFormatter() *diffview.DiffView {
151+ formatDiff := diffview.New()
152+ style := chroma.MustNewStyle("crush", styles.GetChromaTheme())
153+ diff := formatDiff.
154+ SyntaxHightlight(true).
155+ ChromaStyle(style)
156+ return diff
157+}
internal/tui/components/dialogs/commands/commands.go+2−1
@@ -239,7 +239,8 @@ func (c *commandDialogCmp) defaultCommands() []Command {
239
239
240 The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
240 The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
241 If there's already a CRUSH.md, improve it.
241 If there's already a CRUSH.md, improve it.
242− If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
242+ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
243+ Add the .crush directory to the .gitignore file if it's not already there.`
243 return util.CmdHandler(chat.SendMsg{
244 return util.CmdHandler(chat.SendMsg{
244 Text: prompt,
245 Text: prompt,
245 })
246 })
internal/tui/components/dialogs/init/init.go+2−1
@@ -180,7 +180,8 @@ func (m *initDialogCmp) handleInitialization(initialize bool) tea.Cmd {
180
180
181The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
181The file you create will be given to agentic coding agents (such as yourself) that operate in this repository. Make it about 20 lines long.
182If there's already a CRUSH.md, improve it.
182If there's already a CRUSH.md, improve it.
183−If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.`
183+If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (in .github/copilot-instructions.md), make sure to include them.
184+Add the .crush directory to the .gitignore file if it's not already there.`
184
185
185 // Mark the project as initialized
186 // Mark the project as initialized
186 if err := config.MarkProjectInitialized(); err != nil {
187 if err := config.MarkProjectInitialized(); err != nil {
internal/tui/components/dialogs/permissions/permissions.go+10−23
@@ -8,7 +8,7 @@ import (
8 "github.com/charmbracelet/bubbles/v2/viewport"
8 "github.com/charmbracelet/bubbles/v2/viewport"
9 tea "github.com/charmbracelet/bubbletea/v2"
9 tea "github.com/charmbracelet/bubbletea/v2"
10 "github.com/charmbracelet/crush/internal/diff"
10 "github.com/charmbracelet/crush/internal/diff"
11− "github.com/charmbracelet/crush/internal/fileutil"
11+ "github.com/charmbracelet/crush/internal/fsext"
12 "github.com/charmbracelet/crush/internal/llm/tools"
12 "github.com/charmbracelet/crush/internal/llm/tools"
13 "github.com/charmbracelet/crush/internal/permission"
13 "github.com/charmbracelet/crush/internal/permission"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
14 "github.com/charmbracelet/crush/internal/tui/components/core"
@@ -170,7 +170,7 @@ func (p *permissionDialogCmp) renderHeader() string {
170 pathKey := t.S().Muted.Render("Path")
170 pathKey := t.S().Muted.Render("Path")
171 pathValue := t.S().Text.
171 pathValue := t.S().Text.
172 Width(p.width - lipgloss.Width(pathKey)).
172 Width(p.width - lipgloss.Width(pathKey)).
173− Render(fmt.Sprintf(" %s", fileutil.PrettyPath(p.permission.Path)))
173+ Render(fmt.Sprintf(" %s", fsext.PrettyPath(p.permission.Path)))
174
174
175 headerParts := []string{
175 headerParts := []string{
176 lipgloss.JoinHorizontal(
176 lipgloss.JoinHorizontal(
@@ -196,7 +196,7 @@ func (p *permissionDialogCmp) renderHeader() string {
196 fileKey := t.S().Muted.Render("File")
196 fileKey := t.S().Muted.Render("File")
197 filePath := t.S().Text.
197 filePath := t.S().Text.
198 Width(p.width - lipgloss.Width(fileKey)).
198 Width(p.width - lipgloss.Width(fileKey)).
199− Render(fmt.Sprintf(" %s", fileutil.PrettyPath(params.FilePath)))
199+ Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
200 headerParts = append(headerParts,
200 headerParts = append(headerParts,
201 lipgloss.JoinHorizontal(
201 lipgloss.JoinHorizontal(
202 lipgloss.Left,
202 lipgloss.Left,
@@ -211,7 +211,7 @@ func (p *permissionDialogCmp) renderHeader() string {
211 fileKey := t.S().Muted.Render("File")
211 fileKey := t.S().Muted.Render("File")
212 filePath := t.S().Text.
212 filePath := t.S().Text.
213 Width(p.width - lipgloss.Width(fileKey)).
213 Width(p.width - lipgloss.Width(fileKey)).
214− Render(fmt.Sprintf(" %s", fileutil.PrettyPath(params.FilePath)))
214+ Render(fmt.Sprintf(" %s", fsext.PrettyPath(params.FilePath)))
215 headerParts = append(headerParts,
215 headerParts = append(headerParts,
216 lipgloss.JoinHorizontal(
216 lipgloss.JoinHorizontal(
217 lipgloss.Left,
217 lipgloss.Left,
@@ -267,24 +267,13 @@ func (p *permissionDialogCmp) renderBashContent() string {
267
267
268func (p *permissionDialogCmp) renderEditContent() string {
268func (p *permissionDialogCmp) renderEditContent() string {
269 if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
269 if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
270− diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
270+ formatter := core.DiffFormatter().
271− return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width()))
271+ Before(fsext.PrettyPath(pr.FilePath), pr.OldContent).
272− })
272+ After(fsext.PrettyPath(pr.FilePath), pr.NewContent).
273−
274− contentHeight := min(p.height-9, lipgloss.Height(diff))
275− p.contentViewPort.SetHeight(contentHeight)
276− p.contentViewPort.SetContent(diff)
277− return p.styleViewport()
278− }
279− return ""
280−}
281−
282−func (p *permissionDialogCmp) renderPatchContent() string {
283− if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
284− diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
285− return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width()))
273+ Width(p.contentViewPort.Width()).
286− })
274+ Split()
287
275
276+ diff := formatter.String()
288 contentHeight := min(p.height-9, lipgloss.Height(diff))
277 contentHeight := min(p.height-9, lipgloss.Height(diff))
289 p.contentViewPort.SetHeight(contentHeight)
278 p.contentViewPort.SetHeight(contentHeight)
290 p.contentViewPort.SetContent(diff)
279 p.contentViewPort.SetContent(diff)
@@ -381,8 +370,6 @@ func (p *permissionDialogCmp) render() string {
381 contentFinal = p.renderBashContent()
370 contentFinal = p.renderBashContent()
382 case tools.EditToolName:
371 case tools.EditToolName:
383 contentFinal = p.renderEditContent()
372 contentFinal = p.renderEditContent()
384− case tools.PatchToolName:
385− contentFinal = p.renderPatchContent()
386 case tools.WriteToolName:
373 case tools.WriteToolName:
387 contentFinal = p.renderWriteContent()
374 contentFinal = p.renderWriteContent()
388 case tools.FetchToolName:
375 case tools.FetchToolName:
internal/tui/page/chat/chat.go+1−1
@@ -209,7 +209,7 @@ func (p *chatPage) Bindings() []key.Binding {
209
209
210func NewChatPage(app *app.App) ChatPage {
210func NewChatPage(app *app.App) ChatPage {
211 sidebarContainer := layout.NewContainer(
211 sidebarContainer := layout.NewContainer(
212− sidebar.NewSidebarCmp(),
212+ sidebar.NewSidebarCmp(app.History, app.LSPClients),
213 layout.WithPadding(1, 1, 1, 1),
213 layout.WithPadding(1, 1, 1, 1),
214 )
214 )
215 editorContainer := layout.NewContainer(
215 editorContainer := layout.NewContainer(
internal/tui/styles/crush.go+1−1
@@ -32,7 +32,7 @@ func NewCrushTheme() *Theme {
32 // Status
32 // Status
33 Success: charmtone.Guac,
33 Success: charmtone.Guac,
34 Error: charmtone.Sriracha,
34 Error: charmtone.Sriracha,
35− Warning: charmtone.Uni,
35+ Warning: charmtone.Zest,
36 Info: charmtone.Malibu,
36 Info: charmtone.Malibu,
37
37
38 // Colors
38 // Colors
internal/tui/styles/icons.go+3−2
@@ -4,11 +4,12 @@ const (
4 CheckIcon string = "✓"
4 CheckIcon string = "✓"
5 ErrorIcon string = "×"
5 ErrorIcon string = "×"
6 WarningIcon string = "⚠"
6 WarningIcon string = "⚠"
7− InfoIcon string = ""
7+ InfoIcon string = "ⓘ"
8− HintIcon string = "i"
8+ HintIcon string = "∵"
9 SpinnerIcon string = "..."
9 SpinnerIcon string = "..."
10 LoadingIcon string = "⟳"
10 LoadingIcon string = "⟳"
11 DocumentIcon string = "🖼"
11 DocumentIcon string = "🖼"
12+ ModelIcon string = "⬡"
12
13
13 // Tool call icons
14 // Tool call icons
14 ToolPending string = "●"
15 ToolPending string = "●"
internal/version/version.go+1−1
@@ -5,7 +5,7 @@ import "runtime/debug"
5// Build-time parameters set via -ldflags
5// Build-time parameters set via -ldflags
6var Version = "unknown"
6var Version = "unknown"
7
7
8−// A user may install pug using `go install github.com/charmbracelet/crush@latest`.
8+// A user may install crush using `go install github.com/charmbracelet/crush@latest`.
9// without -ldflags, in which case the version above is unset. As a workaround
9// without -ldflags, in which case the version above is unset. As a workaround
10// we use the embedded build version that *is* set when using `go install` (and
10// we use the embedded build version that *is* set when using `go install` (and
11// is only set for `go install` and not for `go build`).
11// is only set for `go install` and not for `go build`).
todos.md+8−2
@@ -3,8 +3,11 @@
3- [x] Implement help
3- [x] Implement help
4 - [x] Show full help
4 - [x] Show full help
5 - [x] Make help dependent on the focused pane and page
5 - [x] Make help dependent on the focused pane and page
6−- [ ] Implement current model in the sidebar
6+- [x] Implement current model in the sidebar
7+- [x] Implement LSP errors
7−- [ ] Implement changed files
8+- [x] Implement changed files
9+ - [x] Implement initial load
10+ - [x] Implement realtime file changes
8- [ ] Events when tool error
11- [ ] Events when tool error
9- [ ] Support bash commands
12- [ ] Support bash commands
10- [ ] Editor attachments fixes
13- [ ] Editor attachments fixes
@@ -23,8 +26,10 @@
23 - [ ] Address UX issues
26 - [ ] Address UX issues
24 - [ ] Fix issue with numbers (padding) view tool
27 - [ ] Fix issue with numbers (padding) view tool
25- [ ] Implement responsive mode
28- [ ] Implement responsive mode
29+- [ ] Update interactive mode to use the spinner
26- [ ] Revisit the core list component
30- [ ] Revisit the core list component
27 - [ ] This component has become super complex we might need to fix this.
31 - [ ] This component has become super complex we might need to fix this.
32+- [ ] Handle correct LSP and MCP status icon
28- [x] Investigate ways to make the spinner less CPU intensive
33- [x] Investigate ways to make the spinner less CPU intensive
29- [ ] General cleanup and documentation
34- [ ] General cleanup and documentation
30- [ ] Update the readme
35- [ ] Update the readme
@@ -34,3 +39,4 @@
34- [ ] Revisit the provider/model/configs
39- [ ] Revisit the provider/model/configs
35- [ ] Implement correct persistent shell
40- [ ] Implement correct persistent shell
36- [ ] Store file read/write time somewhere so that the we can make sure that even if we restart we do not need to re-read the same file
41- [ ] Store file read/write time somewhere so that the we can make sure that even if we restart we do not need to re-read the same file
42+- [ ] Send updates to the UI when new LSP diagnostics are available