diff --git a/cspell.json b/cspell.json index 266001569d7d8dfb6713c634286f406ae04b03b1..5e62368ec8ffecaaebb2fdc1c4ef23bc42cc0e0f 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +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"]} \ No newline at end of file +{"flagWords":[],"words":["crush","charmbracelet","lipgloss","bubbletea","textinput","Focusable","lsps","Sourcegraph","filepicker","imageorient","rasterx","oksvg","termenv","trashhalo","lucasb","nfnt","srwiley","Lanczos","fsext","GROQ","alecthomas","Preproc","Emph","charmtone","Charple","Guac","diffview","Strikethrough","Unticked","uniseg","rivo"],"version":"0.2","language":"en"} \ No newline at end of file diff --git a/internal/app/app.go b/internal/app/app.go index 29c77308111e09f8174ea7f7ceddd30948db8cf1..e7472059a9f3fad360172c353f5d9a188529d177 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -163,4 +163,5 @@ func (app *App) Shutdown() { } cancel() } + app.CoderAgent.CancelAll() } diff --git a/internal/exp/diffview/Taskfile.yaml b/internal/exp/diffview/Taskfile.yaml index f27d093565d7e0e00fb265e21e3ccb2ce48c65b1..909837e10fff38c0309e55dc2c4b90d996b04d6e 100644 --- a/internal/exp/diffview/Taskfile.yaml +++ b/internal/exp/diffview/Taskfile.yaml @@ -100,3 +100,23 @@ tasks: - for: sources cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}} silent: true + + test:print:yoffset:unified:infinite: + desc: Print golden files for debugging + method: none + sources: + - ./testdata/TestDiffViewYOffsetInfinite/Unified/*.golden + cmds: + - for: sources + cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}} + silent: true + + test:print:yoffset:split:infinite: + desc: Print golden files for debugging + method: none + sources: + - ./testdata/TestDiffViewYOffsetInfinite/Split/*.golden + cmds: + - for: sources + cmd: echo && echo "------- {{.ITEM}} -------" && echo && cat {{.ITEM}} + silent: true diff --git a/internal/exp/diffview/diffview.go b/internal/exp/diffview/diffview.go index 3802b473c4da01f5e6a314b477deae65abde60bc..bb51a7e505666e67fd9e914a135a0dd7632bb184 100644 --- a/internal/exp/diffview/diffview.go +++ b/internal/exp/diffview/diffview.go @@ -33,18 +33,19 @@ const ( // DiffView represents a view for displaying differences between two files. type DiffView struct { - layout layout - before file - after file - contextLines int - lineNumbers bool - height int - width int - xOffset int - yOffset int - style Style - tabWidth int - chromaStyle *chroma.Style + layout layout + before file + after file + contextLines int + lineNumbers bool + height int + width int + xOffset int + yOffset int + infiniteYScroll bool + style Style + tabWidth int + chromaStyle *chroma.Style isComputed bool err error @@ -53,6 +54,7 @@ type DiffView struct { splitHunks []splitHunk + totalLines int codeWidth int fullCodeWidth int // with leading symbols extraColOnAfter bool // add extra column on after panel @@ -138,6 +140,12 @@ func (dv *DiffView) YOffset(yOffset int) *DiffView { return dv } +// InfiniteYScroll allows the YOffset to scroll beyond the last line. +func (dv *DiffView) InfiniteYScroll(infiniteYScroll bool) *DiffView { + dv.infiniteYScroll = infiniteYScroll + return dv +} + // TabWidth sets the tab width. Only relevant for code that contains tabs, like // Go code. func (dv *DiffView) TabWidth(tabWidth int) *DiffView { @@ -161,6 +169,8 @@ func (dv *DiffView) String() string { dv.convertDiffToSplit() dv.adjustStyles() dv.detectNumDigits() + dv.detectTotalLines() + dv.preventInfiniteYScroll() if dv.width <= 0 { dv.detectCodeWidth() @@ -251,6 +261,37 @@ func (dv *DiffView) detectNumDigits() { } } +func (dv *DiffView) detectTotalLines() { + dv.totalLines = 0 + + switch dv.layout { + case layoutUnified: + for _, h := range dv.unified.Hunks { + dv.totalLines += 1 + len(h.Lines) + } + case layoutSplit: + for _, h := range dv.splitHunks { + dv.totalLines += 1 + len(h.lines) + } + } +} + +func (dv *DiffView) preventInfiniteYScroll() { + if dv.infiniteYScroll { + return + } + + // clamp yOffset to prevent scrolling beyond the last line + if dv.height > 0 { + maxYOffset := max(0, dv.totalLines-dv.height) + dv.yOffset = min(dv.yOffset, maxYOffset) + } else { + // if no height limit, ensure yOffset doesn't exceed total lines + dv.yOffset = min(dv.yOffset, max(0, dv.totalLines-1)) + } + dv.yOffset = max(0, dv.yOffset) // ensure yOffset is not negative +} + // detectCodeWidth calculates the maximum width of code lines in the diff view. func (dv *DiffView) detectCodeWidth() { switch dv.layout { @@ -321,23 +362,29 @@ func (dv *DiffView) renderUnified() string { fullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth) printedLines := -dv.yOffset - - write := func(s string) { - if printedLines >= 0 { - b.WriteString(s) - } + shouldWrite := func() bool { return printedLines >= 0 } + + getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) { + content = strings.TrimSuffix(in, "\n") + content = dv.hightlightCode(content, ls.Code.GetBackground()) + content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content)) + content = ansi.Truncate(content, dv.codeWidth, "…") + leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != "" + return } outer: for i, h := range dv.unified.Hunks { - ls := dv.style.DividerLine - if dv.lineNumbers { - write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) - write(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.DividerLine + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) + b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) + } + content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…") + b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content)) + b.WriteString("\n") } - content := ansi.Truncate(dv.hunkLineFor(h), dv.fullCodeWidth, "…") - write(ls.Code.Width(dv.fullCodeWidth).Render(content)) - write("\n") printedLines++ beforeLine := h.FromLine @@ -349,80 +396,82 @@ outer: isLastHunk := i+1 == len(dv.unified.Hunks) isLastLine := j+1 == len(h.Lines) if hasReachedHeight && (!isLastHunk || !isLastLine) { - ls := dv.lineStyleForType(l.Kind) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) - write(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) + if shouldWrite() { + ls := dv.lineStyleForType(l.Kind) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) + b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) + } + b.WriteString(fullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth).Render(" …"), + )) + b.WriteRune('\n') } - write(fullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth).Render(" …"), - )) - write("\n") break outer } - getContent := func(ls LineStyle) string { - content := strings.TrimSuffix(l.Content, "\n") - content = dv.hightlightCode(content, ls.Code.GetBackground()) - content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content)) - content = ansi.Truncate(content, dv.codeWidth, "…") - return content - } - - leadingEllipsis := dv.xOffset > 0 && strings.TrimSpace(content) != "" - switch l.Kind { case udiff.Equal: - ls := dv.style.EqualLine - content := getContent(ls) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) - write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.EqualLine + content, leadingEllipsis := getContent(l.Content, ls) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) + b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + } + b.WriteString(fullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content), + )) } - write(fullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content), - )) beforeLine++ afterLine++ case udiff.Insert: - ls := dv.style.InsertLine - content := getContent(ls) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) - write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.InsertLine + content, leadingEllipsis := getContent(l.Content, ls) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) + b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + } + b.WriteString(fullContentStyle.Render( + ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) + + ls.Code.Width(dv.codeWidth).Render(content), + )) } - write(fullContentStyle.Render( - ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) + - ls.Code.Width(dv.codeWidth).Render(content), - )) afterLine++ case udiff.Delete: - ls := dv.style.DeleteLine - content := getContent(ls) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) - write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.DeleteLine + content, leadingEllipsis := getContent(l.Content, ls) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) + b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + } + b.WriteString(fullContentStyle.Render( + ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) + + ls.Code.Width(dv.codeWidth).Render(content), + )) } - write(fullContentStyle.Render( - ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) + - ls.Code.Width(dv.codeWidth).Render(content), - )) beforeLine++ } - write("\n") + if shouldWrite() { + b.WriteRune('\n') + } printedLines++ } } for printedLines < dv.height { - ls := dv.style.MissingLine - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) - write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.MissingLine + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) + b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + } + b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" ")) + b.WriteRune('\n') } - write(ls.Code.Width(dv.fullCodeWidth).Render(" ")) - write("\n") printedLines++ } @@ -436,26 +485,32 @@ func (dv *DiffView) renderSplit() string { beforeFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth) afterFullContentStyle := lipgloss.NewStyle().MaxWidth(dv.fullCodeWidth + btoi(dv.extraColOnAfter)) printedLines := -dv.yOffset - - write := func(s string) { - if printedLines >= 0 { - b.WriteString(s) - } + shouldWrite := func() bool { return printedLines >= 0 } + + getContent := func(in string, ls LineStyle) (content string, leadingEllipsis bool) { + content = strings.TrimSuffix(in, "\n") + content = dv.hightlightCode(content, ls.Code.GetBackground()) + content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content)) + content = ansi.Truncate(content, dv.codeWidth, "…") + leadingEllipsis = dv.xOffset > 0 && strings.TrimSpace(content) != "" + return } outer: for i, h := range dv.splitHunks { - ls := dv.style.DividerLine - if dv.lineNumbers { - write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) - } - content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…") - write(ls.Code.Width(dv.fullCodeWidth).Render(content)) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.DividerLine + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) + } + content := ansi.Truncate(dv.hunkLineFor(dv.unified.Hunks[i]), dv.fullCodeWidth, "…") + b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(content)) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) + } + b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" ")) + b.WriteRune('\n') } - write(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" ")) - write("\n") printedLines++ beforeLine := h.fromLine @@ -467,126 +522,129 @@ outer: isLastHunk := i+1 == len(dv.unified.Hunks) isLastLine := j+1 == len(h.lines) if hasReachedHeight && (!isLastHunk || !isLastLine) { - ls := dv.style.MissingLine - if l.before != nil { - ls = dv.lineStyleForType(l.before.Kind) - } - if dv.lineNumbers { - write(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) - } - write(beforeFullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth).Render(" …"), - )) - ls = dv.style.MissingLine - if l.after != nil { - ls = dv.lineStyleForType(l.after.Kind) + if shouldWrite() { + ls := dv.style.MissingLine + if l.before != nil { + ls = dv.lineStyleForType(l.before.Kind) + } + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad("…", dv.beforeNumDigits))) + } + b.WriteString(beforeFullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth).Render(" …"), + )) + ls = dv.style.MissingLine + if l.after != nil { + ls = dv.lineStyleForType(l.after.Kind) + } + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) + } + b.WriteString(afterFullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth).Render(" …"), + )) + b.WriteRune('\n') } - if dv.lineNumbers { - write(ls.LineNumber.Render(pad("…", dv.afterNumDigits))) - } - write(afterFullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth).Render(" …"), - )) - write("\n") break outer } - getContent := func(content string, ls LineStyle) string { - content = strings.TrimSuffix(content, "\n") - content = dv.hightlightCode(content, ls.Code.GetBackground()) - content = ansi.GraphemeWidth.Cut(content, dv.xOffset, len(content)) - content = ansi.Truncate(content, dv.codeWidth, "…") - return content - } - getLeadingEllipsis := func(content string) bool { - return dv.xOffset > 0 && strings.TrimSpace(content) != "" - } - switch { case l.before == nil: - ls := dv.style.MissingLine - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) + if shouldWrite() { + ls := dv.style.MissingLine + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) + } + b.WriteString(beforeFullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth).Render(" "), + )) } - write(beforeFullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth).Render(" "), - )) case l.before.Kind == udiff.Equal: - ls := dv.style.EqualLine - content := getContent(l.before.Content, ls) - leadingEllipsis := getLeadingEllipsis(content) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) + if shouldWrite() { + ls := dv.style.EqualLine + content, leadingEllipsis := getContent(l.before.Content, ls) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) + } + b.WriteString(beforeFullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content), + )) } - write(beforeFullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth).Render(ternary(leadingEllipsis, " …", " ") + content), - )) beforeLine++ case l.before.Kind == udiff.Delete: - ls := dv.style.DeleteLine - content := getContent(l.before.Content, ls) - leadingEllipsis := getLeadingEllipsis(content) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) + if shouldWrite() { + ls := dv.style.DeleteLine + content, leadingEllipsis := getContent(l.before.Content, ls) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(beforeLine, dv.beforeNumDigits))) + } + b.WriteString(beforeFullContentStyle.Render( + ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) + + ls.Code.Width(dv.codeWidth).Render(content), + )) } - write(beforeFullContentStyle.Render( - ls.Symbol.Render(ternary(leadingEllipsis, "-…", "- ")) + - ls.Code.Width(dv.codeWidth).Render(content), - )) beforeLine++ } switch { case l.after == nil: - ls := dv.style.MissingLine - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.MissingLine + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + } + b.WriteString(afterFullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "), + )) } - write(afterFullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" "), - )) case l.after.Kind == udiff.Equal: - ls := dv.style.EqualLine - content := getContent(l.after.Content, ls) - leadingEllipsis := getLeadingEllipsis(content) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.EqualLine + content, leadingEllipsis := getContent(l.after.Content, ls) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + } + b.WriteString(afterFullContentStyle.Render( + ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content), + )) } - write(afterFullContentStyle.Render( - ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(ternary(leadingEllipsis, " …", " ") + content), - )) afterLine++ case l.after.Kind == udiff.Insert: - ls := dv.style.InsertLine - content := getContent(l.after.Content, ls) - leadingEllipsis := getLeadingEllipsis(content) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.InsertLine + content, leadingEllipsis := getContent(l.after.Content, ls) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(afterLine, dv.afterNumDigits))) + } + b.WriteString(afterFullContentStyle.Render( + ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) + + ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content), + )) } - write(afterFullContentStyle.Render( - ls.Symbol.Render(ternary(leadingEllipsis, "+…", "+ ")) + - ls.Code.Width(dv.codeWidth+btoi(dv.extraColOnAfter)).Render(content), - )) afterLine++ } - write("\n") + if shouldWrite() { + b.WriteRune('\n') + } printedLines++ } } for printedLines < dv.height { - ls := dv.style.MissingLine - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) - } - write(ls.Code.Width(dv.fullCodeWidth).Render(" ")) - if dv.lineNumbers { - write(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + if shouldWrite() { + ls := dv.style.MissingLine + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(" ", dv.beforeNumDigits))) + } + b.WriteString(ls.Code.Width(dv.fullCodeWidth).Render(" ")) + if dv.lineNumbers { + b.WriteString(ls.LineNumber.Render(pad(" ", dv.afterNumDigits))) + } + b.WriteString(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" ")) + b.WriteRune('\n') } - write(ls.Code.Width(dv.fullCodeWidth + btoi(dv.extraColOnAfter)).Render(" ")) - write("\n") printedLines++ } diff --git a/internal/exp/diffview/diffview_test.go b/internal/exp/diffview/diffview_test.go index ed868b7574009e0275c43890d13c66e62174e9e5..d663ec6c04c3012dd08a60fe9c35cd295912b1bf 100644 --- a/internal/exp/diffview/diffview_test.go +++ b/internal/exp/diffview/diffview_test.go @@ -283,6 +283,33 @@ func TestDiffViewYOffset(t *testing.T) { } } +func TestDiffViewYOffsetInfinite(t *testing.T) { + for layoutName, layoutFunc := range LayoutFuncs { + t.Run(layoutName, func(t *testing.T) { + for yOffset := range 17 { + t.Run(fmt.Sprintf("YOffsetOf%02d", yOffset), func(t *testing.T) { + t.Parallel() + + dv := diffview.New(). + Before("main.go", TestMultipleHunksBefore). + After("main.go", TestMultipleHunksAfter). + Style(diffview.DefaultLightStyle()). + ChromaStyle(styles.Get("catppuccin-latte")). + Height(5). + YOffset(yOffset). + InfiniteYScroll(true) + dv = layoutFunc(dv) + + output := dv.String() + golden.RequireEqual(t, []byte(output)) + + assertHeight(t, 5, output) + }) + } + }) + } +} + func assertLineWidth(t *testing.T, expected int, output string) { var lineWidth int for line := range strings.SplitSeq(output, "\n") { diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.golden index c9cf2ba5c67d9a207c92d303d464007dde1befb4..82e128dc76e583c6012e6bc43d81ed2142d2d98c 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf01.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …unc main() {    8    -… fmt.Println("Hello, world!")       8 +… content := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.golden index 83de454cb48931a7515d520410cfd3c834e3c1f7..69333d10986b5dfdf9a180e6a401c30944ec1686 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf02.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …nc main() {    8    -… fmt.Println("Hello, world!")       8 +… content := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.golden index 953b5d6e18c97bfab677573107546b9c406714d9..d57a93823a4a2927f8b583bee6f5a64b50ea427f 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf03.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …c main() {    8    -… fmt.Println("Hello, world!")       8 +… content := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.golden index 30ef912446ba3f5d31a86199f77319a756078f91..83b84378f67a116b64104064d37c9eeafdfc5184 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf04.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  … main() {    8    -…fmt.Println("Hello, world!")       8 +…content := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.golden index e3068f96ee77bb62ddf13203d6d90c0689f80428..39923d1ad17cc9d079cb7e9ca069b666717f90e9 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf05.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …main() {    8    -…mt.Println("Hello, world!")       8 +…ontent := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.golden index 2112d73c61e72d0d027815ec85a54e9f60ca3358..23bbc359dc2713c5e4efe289d49db739dc7febee 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf06.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …ain() {    8    -…t.Println("Hello, world!")       8 +…ntent := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.golden index 54b71be135d454289d77e7a02657d1555b8ecd33..6d0051d415fe34d156c1a2e214c73b5b82f51fb4 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf07.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …in() {    8    -….Println("Hello, world!")       8 +…tent := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.golden index 722115cc24309d4cf633ea5a6e566073047f30d0..f0cbcccd3fac81c5887ff2cbb4c94391f81b4eef 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf08.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …n() {    8    -…Println("Hello, world!")       8 +…ent := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.golden index 5f7bdbdcdd5be4cd11a389c0c783a78183b7bdf8..d129d6ea063bc43ca9af819f3c7f07abfca3bee5 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf09.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …() {    8    -…rintln("Hello, world!")       8 +…nt := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.golden index 45559be79cdb9ededcc648e270de7cd35e7d2568..61db3004e0a95b2862003b589a55a757aed32d51 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf10.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …) {    8    -…intln("Hello, world!")       8 +…t := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.golden index e43d883ca53c33655ef3cba3fcfb8c198f6ba08b..c8e55835730a1cb2b483f4ccf3b28af8eb18acaa 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf11.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  … {    8    -…ntln("Hello, world!")       8 +… := "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.golden index f869ba9a4296fea7bc075dee39d803bfb593571f..76c7f8169230514bbc56d1160ce2e0d6efc8bf79 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf12.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …{    8    -…tln("Hello, world!")       8 +…:= "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.golden index 0c6ceea7ffb1bc8538ecfa791d93ddafbda184ab..c4d1b3d033cb1107489f506b496ce550b124508a 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf13.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…ln("Hello, world!")       8 +…= "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.golden index e5189e842557d9fdb3c1bbcb64bcb88022b5e3fb..d203c18a305f714131a6bc021db50c4d24fd3571 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf14.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…n("Hello, world!")       8 +… "Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.golden index 5ae21ff9d9614a3a938886f54621c38d479f6aaf..8e1488eec270882434c254a3bab5208809affce9 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf15.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…("Hello, world!")       8 +…"Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.golden index 0fada9a25ff6e1292855cecb452b84ae446214ca..6e4045af203a729eca49b755654474a7b0c5929f 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf16.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…"Hello, world!")       8 +…Hello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.golden index 77edd36e83f048673a1a1962ff0839bbd291a2dc..1b6cc59882f6da641e3087576a5c9233e04c181e 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf17.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…Hello, world!")       8 +…ello, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.golden index 84a830655e2f83046e03421608ce2f767653e28f..33ee3c98182045eb4c38d9c1c6aae6c8757288ef 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf18.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…ello, world!")       8 +…llo, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.golden index 9fb8b3b88efda9d50ad6cde9fe1d57305dd7fea6..dede5300cd84025b5bd8fcaf9bef7b6bc9477f70 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf19.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…llo, world!")       8 +…lo, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.golden b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.golden index 410d365f438ae62a002ef06b8e9fc2867545119c..2df1d43dd2dff9599f5e2adc0006941c98ae52c4 100644 --- a/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.golden +++ b/internal/exp/diffview/testdata/TestDiffViewXOffset/Unified/XOffsetOf20.golden @@ -1,6 +1,6 @@   …   …  @@ -5,5 +5,6 @@     5   5  …  -  6   6  …  +  6   6      7   7  …    8    -…lo, world!")       8 +…o, world!"  diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.golden index fca02b528feae2e2620f53d77bdbdc55e06096f0..09c4fa2b1b7eb1941caf79650cd494e9b56727d0 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf11.golden @@ -1,5 +1,5 @@ + 10    11     11  func getContent() string {  12  func getContent() string {   12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")        14 +  return content  - 13  }  15  }  -           \ No newline at end of file + 13  }  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.golden index 5e347b97b1af201404fc830d47501dac3d6b8b36..09c4fa2b1b7eb1941caf79650cd494e9b56727d0 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf12.golden @@ -1,5 +1,5 @@ + 10    11    + 11  func getContent() string {  12  func getContent() string {   12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")        14 +  return content  - 13  }  15  }  -           -           \ No newline at end of file + 13  }  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.golden index 1496bd70374c1879a7b5a628ab923124242508c1..09c4fa2b1b7eb1941caf79650cd494e9b56727d0 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf13.golden @@ -1,5 +1,5 @@ + 10    11    + 11  func getContent() string {  12  func getContent() string {  + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")        14 +  return content  - 13  }  15  }  -           -           -           \ No newline at end of file + 13  }  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.golden index 80b0d239540b60cb3e84a3323a8085b86ad428fa..09c4fa2b1b7eb1941caf79650cd494e9b56727d0 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf14.golden @@ -1,5 +1,5 @@ - 13  }  15  }  -           -           -           -           \ No newline at end of file + 10    11    + 11  func getContent() string {  12  func getContent() string {  + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")  +      14 +  return content  + 13  }  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.golden index c989070ae313dcd1212208115acf3603671ceab6..09c4fa2b1b7eb1941caf79650cd494e9b56727d0 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf15.golden @@ -1,5 +1,5 @@ -           -           -           -           -           \ No newline at end of file + 10    11    + 11  func getContent() string {  12  func getContent() string {  + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")  +      14 +  return content  + 13  }  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.golden index c989070ae313dcd1212208115acf3603671ceab6..09c4fa2b1b7eb1941caf79650cd494e9b56727d0 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Split/YOffsetOf16.golden @@ -1,5 +1,5 @@ -           -           -           -           -           \ No newline at end of file + 10    11    + 11  func getContent() string {  12  func getContent() string {  + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")  +      14 +  return content  + 13  }  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.golden index 1d8d95395c0e5900b270bb4d6f03ddef56f9bbe3..5f686c57c49ca7f7db94c766fff568f55725fa36 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf12.golden @@ -1,5 +1,5 @@ + 11  12  func getContent() string {   12    -  return "Hello, world!"      13 +  content := strings.ToUpper("Hello, World!")      14 +  return content  - 13  15  }  -         \ No newline at end of file + 13  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.golden index 469accfc86b39051e1f0d22944328f5ae0ddb911..5f686c57c49ca7f7db94c766fff568f55725fa36 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf13.golden @@ -1,5 +1,5 @@ + 11  12  func getContent() string {  + 12    -  return "Hello, world!"      13 +  content := strings.ToUpper("Hello, World!")      14 +  return content  - 13  15  }  -         -         \ No newline at end of file + 13  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.golden index 547e576181a8e1abe5d24f3fce8f5dce2988bb3c..5f686c57c49ca7f7db94c766fff568f55725fa36 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf14.golden @@ -1,5 +1,5 @@ + 11  12  func getContent() string {  + 12    -  return "Hello, world!"  +    13 +  content := strings.ToUpper("Hello, World!")      14 +  return content  - 13  15  }  -         -         -         \ No newline at end of file + 13  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.golden index 005a5ea98096a1a703fb31ccd9b6e94ee0fe874a..5f686c57c49ca7f7db94c766fff568f55725fa36 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf15.golden @@ -1,5 +1,5 @@ - 13  15  }  -         -         -         -         \ No newline at end of file + 11  12  func getContent() string {  + 12    -  return "Hello, world!"  +    13 +  content := strings.ToUpper("Hello, World!")  +    14 +  return content  + 13  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.golden b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.golden index 933f56b9d1fc148d6d394cb271299f4eddc0a739..5f686c57c49ca7f7db94c766fff568f55725fa36 100644 --- a/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.golden +++ b/internal/exp/diffview/testdata/TestDiffViewYOffset/Unified/YOffsetOf16.golden @@ -1,5 +1,5 @@ -         -         -         -         -         \ No newline at end of file + 11  12  func getContent() string {  + 12    -  return "Hello, world!"  +    13 +  content := strings.ToUpper("Hello, World!")  +    14 +  return content  + 13  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf00.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf00.golden new file mode 100644 index 0000000000000000000000000000000000000000..8d4a293fdd17ce5cbd8d709656573105a8cc7b09 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf00.golden @@ -0,0 +1,5 @@ +  …  @@ -2,6 +2,7 @@    …    +  2     2    +  3  import (   3  import (  +  4   "fmt"   4   "fmt"  +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf01.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf01.golden new file mode 100644 index 0000000000000000000000000000000000000000..59b706f531489a560427504d00b4ed513a2f0429 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf01.golden @@ -0,0 +1,5 @@ +  2     2    +  3  import (   3  import (  +  4   "fmt"   4   "fmt"  +       5 +  "strings"  +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf02.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf02.golden new file mode 100644 index 0000000000000000000000000000000000000000..a3e47b440f42bcadf6644a9ce09b4a3eced04193 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf02.golden @@ -0,0 +1,5 @@ +  3  import (   3  import (  +  4   "fmt"   4   "fmt"  +       5 +  "strings"  +  5  )   6  )  +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf03.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf03.golden new file mode 100644 index 0000000000000000000000000000000000000000..823ceecf70c4ba985ab52af71e5d73b502e486ef --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf03.golden @@ -0,0 +1,5 @@ +  4   "fmt"   4   "fmt"  +       5 +  "strings"  +  5  )   6  )  +  6     7    +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf04.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf04.golden new file mode 100644 index 0000000000000000000000000000000000000000..51cc05362f49e9733efb56c02af96d584ed3b507 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf04.golden @@ -0,0 +1,5 @@ +       5 +  "strings"  +  5  )   6  )  +  6     7    +  7  func main() {   8  func main() {  +  …  @@ -9,5 +10,6 @@    …    \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf05.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf05.golden new file mode 100644 index 0000000000000000000000000000000000000000..f8052af3a03a3e0d71c7e6914ae61e0cfe1cd208 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf05.golden @@ -0,0 +1,5 @@ +  5  )   6  )  +  6     7    +  7  func main() {   8  func main() {  +  …  @@ -9,5 +10,6 @@    …    +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf06.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf06.golden new file mode 100644 index 0000000000000000000000000000000000000000..00209b6b75b82c568e0f04349dd366a8c303b9e0 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf06.golden @@ -0,0 +1,5 @@ +  6     7    +  7  func main() {   8  func main() {  +  …  @@ -9,5 +10,6 @@    …    +  9  }  10  }  +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf07.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf07.golden new file mode 100644 index 0000000000000000000000000000000000000000..49f92c490c17a8d125f1a7ca6854573e72ef3e65 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf07.golden @@ -0,0 +1,5 @@ +  7  func main() {   8  func main() {  +  …  @@ -9,5 +10,6 @@    …    +  9  }  10  }  + 10    11    +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf08.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf08.golden new file mode 100644 index 0000000000000000000000000000000000000000..6f01089b12f29dff3c4a31bff325a543dc660355 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf08.golden @@ -0,0 +1,5 @@ +  …  @@ -9,5 +10,6 @@    …    +  9  }  10  }  + 10    11    + 11  func getContent() string {  12  func getContent() string {  +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf09.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf09.golden new file mode 100644 index 0000000000000000000000000000000000000000..d368d1e85520195ead3015f57012ffefa0502d0d --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf09.golden @@ -0,0 +1,5 @@ +  9  }  10  }  + 10    11    + 11  func getContent() string {  12  func getContent() string {  + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")  +  …  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf10.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf10.golden new file mode 100644 index 0000000000000000000000000000000000000000..09c4fa2b1b7eb1941caf79650cd494e9b56727d0 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf10.golden @@ -0,0 +1,5 @@ + 10    11    + 11  func getContent() string {  12  func getContent() string {  + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")  +      14 +  return content  + 13  }  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf11.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf11.golden new file mode 100644 index 0000000000000000000000000000000000000000..fca02b528feae2e2620f53d77bdbdc55e06096f0 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf11.golden @@ -0,0 +1,5 @@ + 11  func getContent() string {  12  func getContent() string {  + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")  +      14 +  return content  + 13  }  15  }  +           \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf12.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf12.golden new file mode 100644 index 0000000000000000000000000000000000000000..5e347b97b1af201404fc830d47501dac3d6b8b36 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf12.golden @@ -0,0 +1,5 @@ + 12 -  return "Hello, world!"  13 +  content := strings.ToUpper("Hello, World!")  +      14 +  return content  + 13  }  15  }  +           +           \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf13.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf13.golden new file mode 100644 index 0000000000000000000000000000000000000000..1496bd70374c1879a7b5a628ab923124242508c1 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf13.golden @@ -0,0 +1,5 @@ +      14 +  return content  + 13  }  15  }  +           +           +           \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf14.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf14.golden new file mode 100644 index 0000000000000000000000000000000000000000..80b0d239540b60cb3e84a3323a8085b86ad428fa --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf14.golden @@ -0,0 +1,5 @@ + 13  }  15  }  +           +           +           +           \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf15.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf15.golden new file mode 100644 index 0000000000000000000000000000000000000000..c989070ae313dcd1212208115acf3603671ceab6 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf15.golden @@ -0,0 +1,5 @@ +           +           +           +           +           \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf16.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf16.golden new file mode 100644 index 0000000000000000000000000000000000000000..c989070ae313dcd1212208115acf3603671ceab6 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Split/YOffsetOf16.golden @@ -0,0 +1,5 @@ +           +           +           +           +           \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf00.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf00.golden new file mode 100644 index 0000000000000000000000000000000000000000..e11e6df667cfa9dc3d909f30fef8895c16dc85c2 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf00.golden @@ -0,0 +1,5 @@ +  …   …  @@ -2,6 +2,7 @@   +  2   2    +  3   3  import (  +  4   4   "fmt"  +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf01.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf01.golden new file mode 100644 index 0000000000000000000000000000000000000000..52c1ff0ee9ddec7f746590d4a5efd7630eb97fd4 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf01.golden @@ -0,0 +1,5 @@ +  2   2    +  3   3  import (  +  4   4   "fmt"  +     5 +  "strings"  +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf02.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf02.golden new file mode 100644 index 0000000000000000000000000000000000000000..0d3ef94eccf5119a182afaecfdf5ad85880d3271 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf02.golden @@ -0,0 +1,5 @@ +  3   3  import (  +  4   4   "fmt"  +     5 +  "strings"  +  5   6  )  +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf03.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf03.golden new file mode 100644 index 0000000000000000000000000000000000000000..37d7ae5851327dd0d05ecb6cfc8d1dcc447c0be7 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf03.golden @@ -0,0 +1,5 @@ +  4   4   "fmt"  +     5 +  "strings"  +  5   6  )  +  6   7    +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf04.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf04.golden new file mode 100644 index 0000000000000000000000000000000000000000..cdc84e4407a0ac070af67944cbf2b470182a865d --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf04.golden @@ -0,0 +1,5 @@ +     5 +  "strings"  +  5   6  )  +  6   7    +  7   8  func main() {  +  …   …  @@ -9,5 +10,6 @@   \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf05.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf05.golden new file mode 100644 index 0000000000000000000000000000000000000000..7d36fccbe412a8d49e3c1bc78ae91d2f5433e4de --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf05.golden @@ -0,0 +1,5 @@ +  5   6  )  +  6   7    +  7   8  func main() {  +  …   …  @@ -9,5 +10,6 @@   +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf06.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf06.golden new file mode 100644 index 0000000000000000000000000000000000000000..32c230ed26134c7427738a89829c8c511ea97922 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf06.golden @@ -0,0 +1,5 @@ +  6   7    +  7   8  func main() {  +  …   …  @@ -9,5 +10,6 @@   +  9  10  }  +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf07.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf07.golden new file mode 100644 index 0000000000000000000000000000000000000000..95f4c23477af7c7b87731b4170b96ce3bf94e755 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf07.golden @@ -0,0 +1,5 @@ +  7   8  func main() {  +  …   …  @@ -9,5 +10,6 @@   +  9  10  }  + 10  11    +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf08.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf08.golden new file mode 100644 index 0000000000000000000000000000000000000000..bf5e674b322acebb23134ff841fbbbfa27eeb752 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf08.golden @@ -0,0 +1,5 @@ +  …   …  @@ -9,5 +10,6 @@   +  9  10  }  + 10  11    + 11  12  func getContent() string {  +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf09.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf09.golden new file mode 100644 index 0000000000000000000000000000000000000000..ad0bfca4be7b761b73d833849de6b0c369d92586 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf09.golden @@ -0,0 +1,5 @@ +  9  10  }  + 10  11    + 11  12  func getContent() string {  + 12    -  return "Hello, world!"  +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf10.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf10.golden new file mode 100644 index 0000000000000000000000000000000000000000..5998ba6ec7df46f91fe969858c19559fe873c330 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf10.golden @@ -0,0 +1,5 @@ + 10  11    + 11  12  func getContent() string {  + 12    -  return "Hello, world!"  +    13 +  content := strings.ToUpper("Hello, World!")  +  …   …  …  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf11.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf11.golden new file mode 100644 index 0000000000000000000000000000000000000000..5f686c57c49ca7f7db94c766fff568f55725fa36 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf11.golden @@ -0,0 +1,5 @@ + 11  12  func getContent() string {  + 12    -  return "Hello, world!"  +    13 +  content := strings.ToUpper("Hello, World!")  +    14 +  return content  + 13  15  }  \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf12.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf12.golden new file mode 100644 index 0000000000000000000000000000000000000000..1d8d95395c0e5900b270bb4d6f03ddef56f9bbe3 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf12.golden @@ -0,0 +1,5 @@ + 12    -  return "Hello, world!"  +    13 +  content := strings.ToUpper("Hello, World!")  +    14 +  return content  + 13  15  }  +         \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf13.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf13.golden new file mode 100644 index 0000000000000000000000000000000000000000..469accfc86b39051e1f0d22944328f5ae0ddb911 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf13.golden @@ -0,0 +1,5 @@ +    13 +  content := strings.ToUpper("Hello, World!")  +    14 +  return content  + 13  15  }  +         +         \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf14.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf14.golden new file mode 100644 index 0000000000000000000000000000000000000000..547e576181a8e1abe5d24f3fce8f5dce2988bb3c --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf14.golden @@ -0,0 +1,5 @@ +    14 +  return content  + 13  15  }  +         +         +         \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf15.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf15.golden new file mode 100644 index 0000000000000000000000000000000000000000..005a5ea98096a1a703fb31ccd9b6e94ee0fe874a --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf15.golden @@ -0,0 +1,5 @@ + 13  15  }  +         +         +         +         \ No newline at end of file diff --git a/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf16.golden b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf16.golden new file mode 100644 index 0000000000000000000000000000000000000000..933f56b9d1fc148d6d394cb271299f4eddc0a739 --- /dev/null +++ b/internal/exp/diffview/testdata/TestDiffViewYOffsetInfinite/Unified/YOffsetOf16.golden @@ -0,0 +1,5 @@ +         +         +         +         +         \ No newline at end of file diff --git a/internal/llm/agent/agent.go b/internal/llm/agent/agent.go index 2659e2bcad17756986cbc69a203c05ce7a688c9f..4c8bae171118b4550c9d2a28cb5df0456099530c 100644 --- a/internal/llm/agent/agent.go +++ b/internal/llm/agent/agent.go @@ -50,6 +50,7 @@ type Service interface { Model() models.Model Run(ctx context.Context, sessionID string, content string, attachments ...message.Attachment) (<-chan AgentEvent, error) Cancel(sessionID string) + CancelAll() IsSessionBusy(sessionID string) bool IsBusy() bool Update(agentName config.AgentName, modelID models.ModelID) (models.Model, error) @@ -698,6 +699,13 @@ func (a *agent) Summarize(ctx context.Context, sessionID string) error { return nil } +func (a *agent) CancelAll() { + a.activeRequests.Range(func(key, value any) bool { + a.Cancel(key.(string)) // key is sessionID + return true + }) +} + func createAgentProvider(agentName config.AgentName) (provider.Provider, error) { cfg := config.Get() agentConfig, ok := cfg.Agents[agentName] diff --git a/internal/llm/prompt/coder.go b/internal/llm/prompt/coder.go index d994208dae04cd1cec63d0fc9b3fa7d606a2b0af..ea31bfa0297c1ce207e188a7f162e26831927636 100644 --- a/internal/llm/prompt/coder.go +++ b/internal/llm/prompt/coder.go @@ -30,10 +30,6 @@ You are operating as and within the Crush CLI, a terminal-based agentic coding a You can: - Receive user prompts, project context, and files. - Stream responses and emit function calls (e.g., shell commands, code edits). -- Apply patches, run commands, and manage user approvals based on policy. -- Work inside a sandboxed, git-backed workspace with rollback support. -- Log telemetry so sessions can be replayed or inspected later. -- More details on your functionality are available at "crush --help" You are an agent - please keep going until the user's query is completely resolved, before ending your turn and yielding back to the user. Only terminate your turn when you are sure that the problem is solved. If you are not sure about file content or codebase structure pertaining to the user's request, use your tools to read files and gather the relevant information: do NOT guess or make up an answer. @@ -64,17 +60,19 @@ You MUST adhere to the following criteria when executing the task: - If completing the user's task DOES NOT require writing or modifying files (e.g., the user asks a question about the code base): - Respond in a friendly tune as a remote teammate, who is knowledgeable, capable and eager to help with coding. - When your task involves writing or modifying files: - - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using "apply_patch". Instead, reference the file as already saved. + - Do NOT tell the user to "save the file" or "copy the code into a file" if you already created or modified the file using "edit/write". Instead, reference the file as already saved. - Do NOT show the full contents of large files you have already written, unless the user explicitly asks for them. - When doing things with paths, always use use the full path, if the working directory is /abc/xyz and you want to edit the file abc.go in the working dir refer to it as /abc/xyz/abc.go. - If you send a path not including the working dir, the working dir will be prepended to it. - Remember the user does not see the full output of tools +- NEVER use emojis in your responses ` const baseAnthropicCoderPrompt = `You are Crush, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user. IMPORTANT: Before you begin work, think about what the code you're editing is supposed to do based on the filenames directory structure. + # Memory If the current working directory contains a file called CRUSH.md, it will be automatically added to your context. This file serves multiple purposes: 1. Storing frequently used bash commands (build, test, lint, etc.) so you can use them without searching each time @@ -131,7 +129,7 @@ assistant: src/foo.c user: write tests for new feature -assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit/patch file tool to write new tests] +assistant: [uses grep and glob search tools to find where similar tests are defined, uses concurrent read file tool use blocks in one tool call to read relevant files at the same time, uses edit file tool to write new tests] # Proactiveness @@ -165,6 +163,8 @@ NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTAN - If you intend to call multiple tools and there are no dependencies between the calls, make all of the independent calls in parallel. - IMPORTANT: The user does not see the full output of the tool responses, so if you need the output of the tool for the response make sure to summarize it for the user. +VERY IMPORTANT NEVER use emojis in your responses. + You MUST answer concisely with fewer than 4 lines of text (not including tool use or code generation), unless user asks for detail.` func getEnvironmentInfo() string { diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index 5228e432d25f6f4dad56fac415e1d1023fce7173..da47526a5b252af7166562cb61165ed308a2b348 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -36,6 +36,7 @@ const ( DefaultTimeout = 1 * 60 * 1000 // 1 minutes in milliseconds MaxTimeout = 10 * 60 * 1000 // 10 minutes in milliseconds MaxOutputLength = 30000 + BashNoOutput = "no output" ) var bannedCommands = []string{ @@ -321,7 +322,7 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) EndTime: time.Now().UnixMilli(), } if stdout == "" { - return WithResponseMetadata(NewTextResponse("no output"), metadata), nil + return WithResponseMetadata(NewTextResponse(BashNoOutput), metadata), nil } return WithResponseMetadata(NewTextResponse(stdout), metadata), nil } diff --git a/internal/tui/components/chat/chat.go b/internal/tui/components/chat/chat.go index 332d0ff5070290377342e75b0af6c2f4a59d70e5..95c9ad2d2831ab39da9ddd524fec2932ad9ddc73 100644 --- a/internal/tui/components/chat/chat.go +++ b/internal/tui/components/chat/chat.go @@ -272,7 +272,7 @@ func (m *messageListCmp) findAssistantMessageAndToolCalls(items []util.Model, me assistantIndex = i } } else if tc, ok := item.(messages.ToolCallCmp); ok { - if tc.ParentMessageId() == messageID { + if tc.ParentMessageID() == messageID { toolCalls[i] = tc } } @@ -295,9 +295,17 @@ func (m *messageListCmp) updateAssistantMessageContent(msg message.Message, assi assistantIndex, messages.NewMessageCmp( msg, - messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)), ), ) + + if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn { + m.listCmp.AppendItem( + messages.NewAssistantSection( + msg, + time.Unix(m.lastUserMessageTime, 0), + ), + ) + } } else if hasToolCallsOnly { m.listCmp.DeleteItem(assistantIndex) } @@ -347,7 +355,6 @@ func (m *messageListCmp) handleNewAssistantMessage(msg message.Message) tea.Cmd cmd := m.listCmp.AppendItem( messages.NewMessageCmp( msg, - messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)), ), ) cmds = append(cmds, cmd) @@ -412,6 +419,9 @@ func (m *messageListCmp) convertMessagesToUI(sessionMessages []message.Message, uiMessages = append(uiMessages, messages.NewMessageCmp(msg)) case message.Assistant: uiMessages = append(uiMessages, m.convertAssistantMessage(msg, toolResultMap)...) + if msg.FinishPart() != nil && msg.FinishPart().Reason == message.FinishReasonEndTurn { + uiMessages = append(uiMessages, messages.NewAssistantSection(msg, time.Unix(m.lastUserMessageTime, 0))) + } } } @@ -428,7 +438,6 @@ func (m *messageListCmp) convertAssistantMessage(msg message.Message, toolResult uiMessages, messages.NewMessageCmp( msg, - messages.WithLastUserMessageTime(time.Unix(m.lastUserMessageTime, 0)), ), ) } diff --git a/internal/tui/components/chat/messages/messages.go b/internal/tui/components/chat/messages/messages.go index 51901308b6a20b65bbaa0779d8e3340675d2b1e5..52ca288b9aa5a140f2abaa9ee64ae8775e78bfa6 100644 --- a/internal/tui/components/chat/messages/messages.go +++ b/internal/tui/components/chat/messages/messages.go @@ -8,13 +8,14 @@ import ( "github.com/charmbracelet/bubbles/v2/spinner" tea "github.com/charmbracelet/bubbletea/v2" - "github.com/charmbracelet/crush/internal/llm/models" "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/crush/internal/llm/models" "github.com/charmbracelet/crush/internal/message" "github.com/charmbracelet/crush/internal/tui/components/anim" "github.com/charmbracelet/crush/internal/tui/components/core" "github.com/charmbracelet/crush/internal/tui/components/core/layout" + "github.com/charmbracelet/crush/internal/tui/components/core/list" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" ) @@ -37,32 +38,17 @@ type messageCmp struct { focused bool // Focus state for border styling // Core message data and state - message message.Message // The underlying message content - spinning bool // Whether to show loading animation - anim util.Model // Animation component for loading states - lastUserMessageTime time.Time // Used for calculating response duration -} - -// MessageOption provides functional options for configuring message components -type MessageOption func(*messageCmp) - -// WithLastUserMessageTime sets the timestamp of the last user message -// for calculating assistant response duration -func WithLastUserMessageTime(t time.Time) MessageOption { - return func(m *messageCmp) { - m.lastUserMessageTime = t - } + message message.Message // The underlying message content + spinning bool // Whether to show loading animation + anim util.Model // Animation component for loading states } // NewMessageCmp creates a new message component with the given message and options -func NewMessageCmp(msg message.Message, opts ...MessageOption) MessageCmp { +func NewMessageCmp(msg message.Message) MessageCmp { m := &messageCmp{ message: msg, anim: anim.New(15, ""), } - for _, opt := range opts { - opt(m) - } return m } @@ -145,32 +131,10 @@ func (msg *messageCmp) style() lipgloss.Style { // renderAssistantMessage renders assistant messages with optional footer information. // Shows model name, response time, and finish reason when the message is complete. func (m *messageCmp) renderAssistantMessage() string { - t := styles.CurrentTheme() parts := []string{ m.markdownContent(), } - finished := m.message.IsFinished() - finishData := m.message.FinishPart() - // Only show the footer if the message is not a tool call - if finished && finishData.Reason != message.FinishReasonToolUse { - infoMsg := "" - switch finishData.Reason { - case message.FinishReasonEndTurn: - finishTime := time.Unix(finishData.Time, 0) - duration := finishTime.Sub(m.lastUserMessageTime) - infoMsg = duration.String() - case message.FinishReasonCanceled: - infoMsg = "canceled" - case message.FinishReasonError: - infoMsg = "error" - case message.FinishReasonPermissionDenied: - infoMsg = "permission denied" - } - assistant := t.S().Muted.Render(fmt.Sprintf("%s %s (%s)", styles.ModelIcon, models.SupportedModels[m.message.Model].Name, infoMsg)) - parts = append(parts, core.Section(assistant, m.textWidth())) - } - joined := lipgloss.JoinVertical(lipgloss.Left, parts...) return m.style().Render(joined) } @@ -200,7 +164,7 @@ func (m *messageCmp) renderUserMessage() string { parts = append(parts, "", strings.Join(attachments, "")) } joined := lipgloss.JoinVertical(lipgloss.Left, parts...) - return m.style().MarginBottom(1).Render(joined) + return m.style().Render(joined) } // toMarkdown converts text content to rendered markdown using the configured renderer @@ -225,7 +189,7 @@ func (m *messageCmp) markdownContent() string { } else if finished && content == "" && finishedData.Reason == message.FinishReasonEndTurn { // Sometimes the LLMs respond with no content when they think the previous tool result // provides the requested question - content = "*Finished without output*" + content = "" } else if finished && content == "" && finishedData.Reason == message.FinishReasonCanceled { content = "*Canceled*" } @@ -287,3 +251,59 @@ func (m *messageCmp) SetSize(width int, height int) tea.Cmd { func (m *messageCmp) Spinning() bool { return m.spinning } + +type AssistantSection interface { + util.Model + layout.Sizeable + list.SectionHeader +} +type assistantSectionModel struct { + width int + message message.Message + lastUserMessageTime time.Time +} + +func NewAssistantSection(message message.Message, lastUserMessageTime time.Time) AssistantSection { + return &assistantSectionModel{ + width: 0, + message: message, + lastUserMessageTime: lastUserMessageTime, + } +} + +func (m *assistantSectionModel) Init() tea.Cmd { + return nil +} + +func (m *assistantSectionModel) Update(tea.Msg) (tea.Model, tea.Cmd) { + return m, nil +} + +func (m *assistantSectionModel) View() tea.View { + t := styles.CurrentTheme() + finishData := m.message.FinishPart() + finishTime := time.Unix(finishData.Time, 0) + duration := finishTime.Sub(m.lastUserMessageTime) + infoMsg := t.S().Subtle.Render(duration.String()) + icon := t.S().Subtle.Render(styles.ModelIcon) + model := t.S().Muted.Render(models.SupportedModels[m.message.Model].Name) + assistant := fmt.Sprintf("%s %s %s", icon, model, infoMsg) + return tea.NewView( + t.S().Base.PaddingLeft(2).Render( + core.Section(assistant, m.width-2), + ), + ) +} + +func (m *assistantSectionModel) GetSize() (int, int) { + return m.width, 1 +} + +func (m *assistantSectionModel) SetSize(width int, height int) tea.Cmd { + m.width = width + return nil +} + +func (m *assistantSectionModel) IsSectionHeader() bool { + return true +} diff --git a/internal/tui/components/chat/messages/renderer.go b/internal/tui/components/chat/messages/renderer.go index 32322ddefe4ebdd42a949263f9e59b752a6a3b3c..1bc586de56eaa9aa13d029c9a87381524c43e1fb 100644 --- a/internal/tui/components/chat/messages/renderer.go +++ b/internal/tui/components/chat/messages/renderer.go @@ -111,8 +111,18 @@ func (br baseRenderer) unmarshalParams(input string, target any) error { return json.Unmarshal([]byte(input), target) } +// makeHeader builds the tool call header with status icon and parameters for a nested tool call. +func (br baseRenderer) makeNestedHeader(v *toolCallCmp, tool string, width int, params ...string) string { + t := styles.CurrentTheme() + tool = t.S().Base.Foreground(t.FgHalfMuted).Render(tool) + " " + return tool + renderParamList(true, width-lipgloss.Width(tool), params...) +} + // makeHeader builds ": param (key=value)" and truncates as needed. func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params ...string) string { + if v.isNested { + return br.makeNestedHeader(v, tool, width, params...) + } t := styles.CurrentTheme() icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending) if v.result.ToolCallID != "" { @@ -125,16 +135,17 @@ func (br baseRenderer) makeHeader(v *toolCallCmp, tool string, width int, params icon = t.S().Muted.Render(styles.ToolPending) } tool = t.S().Base.Foreground(t.Blue).Render(tool) - prefix := fmt.Sprintf("%s %s: ", icon, tool) - return prefix + renderParamList(width-lipgloss.Width(prefix), params...) + prefix := fmt.Sprintf("%s %s ", icon, tool) + return prefix + renderParamList(false, width-lipgloss.Width(prefix), params...) } // renderError provides consistent error rendering func (br baseRenderer) renderError(v *toolCallCmp, message string) string { t := styles.CurrentTheme() header := br.makeHeader(v, prettifyToolName(v.call.Name), v.textWidth(), "") - message = t.S().Error.Render(v.fit(message, v.textWidth()-2)) // -2 for padding - return joinHeaderBody(header, message) + errorTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR") + message = t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(message, v.textWidth()-3-lipgloss.Width(errorTag))) // -2 for padding and space + return joinHeaderBody(header, errorTag+" "+message) } // Register tool renderers @@ -188,6 +199,9 @@ func (br bashRenderer) Render(v *toolCallCmp) string { args := newParamBuilder().addMain(cmd).build() return br.renderWithParams(v, "Bash", args, func() string { + if v.result.Content == tools.BashNoOutput { + return "" + } return renderPlainContent(v, v.result.Content) }) } @@ -244,13 +258,12 @@ type editRenderer struct { // Render displays the edited file with a formatted diff of changes func (er editRenderer) Render(v *toolCallCmp) string { var params tools.EditParams - if err := er.unmarshalParams(v.call.Input, ¶ms); err != nil { - return er.renderError(v, "Invalid edit parameters") + var args []string + if err := er.unmarshalParams(v.call.Input, ¶ms); err == nil { + file := fsext.PrettyPath(params.FilePath) + args = newParamBuilder().addMain(file).build() } - file := fsext.PrettyPath(params.FilePath) - args := newParamBuilder().addMain(file).build() - return er.renderWithParams(v, "Edit", args, func() string { var meta tools.EditResponseMetadata if err := er.unmarshalParams(v.result.Metadata, &meta); err != nil { @@ -260,8 +273,10 @@ func (er editRenderer) Render(v *toolCallCmp) string { formatter := core.DiffFormatter(). Before(fsext.PrettyPath(params.FilePath), meta.OldContent). After(fsext.PrettyPath(params.FilePath), meta.NewContent). - Split(). Width(v.textWidth() - 2) // -2 for padding + if v.textWidth() > 120 { + formatter = formatter.Split() + } return formatter.String() }) } @@ -278,13 +293,13 @@ type writeRenderer struct { // Render displays the file being written with syntax highlighting func (wr writeRenderer) Render(v *toolCallCmp) string { var params tools.WriteParams - if err := wr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return wr.renderError(v, "Invalid write parameters") + var args []string + var file string + if err := wr.unmarshalParams(v.call.Input, ¶ms); err == nil { + file = fsext.PrettyPath(params.FilePath) + args = newParamBuilder().addMain(file).build() } - file := fsext.PrettyPath(params.FilePath) - args := newParamBuilder().addMain(file).build() - return wr.renderWithParams(v, "Write", args, func() string { return renderCodeContent(v, file, params.Content, 0) }) @@ -302,16 +317,15 @@ type fetchRenderer struct { // Render displays the fetched URL with format and timeout parameters func (fr fetchRenderer) Render(v *toolCallCmp) string { var params tools.FetchParams - if err := fr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return fr.renderError(v, "Invalid fetch parameters") + var args []string + if err := fr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.URL). + addKeyValue("format", params.Format). + addKeyValue("timeout", formatTimeout(params.Timeout)). + build() } - args := newParamBuilder(). - addMain(params.URL). - addKeyValue("format", params.Format). - addKeyValue("timeout", formatTimeout(params.Timeout)). - build() - return fr.renderWithParams(v, "Fetch", args, func() string { file := fr.getFileExtension(params.Format) return renderCodeContent(v, file, v.result.Content, 0) @@ -350,15 +364,14 @@ type globRenderer struct { // Render displays the glob pattern with optional path parameter func (gr globRenderer) Render(v *toolCallCmp) string { var params tools.GlobParams - if err := gr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return gr.renderError(v, "Invalid glob parameters") + var args []string + if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.Pattern). + addKeyValue("path", params.Path). + build() } - args := newParamBuilder(). - addMain(params.Pattern). - addKeyValue("path", params.Path). - build() - return gr.renderWithParams(v, "Glob", args, func() string { return renderPlainContent(v, v.result.Content) }) @@ -376,17 +389,16 @@ type grepRenderer struct { // Render displays the search pattern with path, include, and literal text options func (gr grepRenderer) Render(v *toolCallCmp) string { var params tools.GrepParams - if err := gr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return gr.renderError(v, "Invalid grep parameters") + var args []string + if err := gr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.Pattern). + addKeyValue("path", params.Path). + addKeyValue("include", params.Include). + addFlag("literal", params.LiteralText). + build() } - args := newParamBuilder(). - addMain(params.Pattern). - addKeyValue("path", params.Path). - addKeyValue("include", params.Include). - addFlag("literal", params.LiteralText). - build() - return gr.renderWithParams(v, "Grep", args, func() string { return renderPlainContent(v, v.result.Content) }) @@ -404,17 +416,16 @@ type lsRenderer struct { // Render displays the directory path, defaulting to current directory func (lr lsRenderer) Render(v *toolCallCmp) string { var params tools.LSParams - if err := lr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return lr.renderError(v, "Invalid ls parameters") - } + var args []string + if err := lr.unmarshalParams(v.call.Input, ¶ms); err == nil { + path := params.Path + if path == "" { + path = "." + } + path = fsext.PrettyPath(path) - path := params.Path - if path == "" { - path = "." + args = newParamBuilder().addMain(path).build() } - path = fsext.PrettyPath(path) - - args := newParamBuilder().addMain(path).build() return lr.renderWithParams(v, "List", args, func() string { return renderPlainContent(v, v.result.Content) @@ -433,16 +444,15 @@ type sourcegraphRenderer struct { // Render displays the search query with optional count and context window parameters func (sr sourcegraphRenderer) Render(v *toolCallCmp) string { var params tools.SourcegraphParams - if err := sr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return sr.renderError(v, "Invalid sourcegraph parameters") + var args []string + if err := sr.unmarshalParams(v.call.Input, ¶ms); err == nil { + args = newParamBuilder(). + addMain(params.Query). + addKeyValue("count", formatNonZero(params.Count)). + addKeyValue("context", formatNonZero(params.ContextWindow)). + build() } - args := newParamBuilder(). - addMain(params.Query). - addKeyValue("count", formatNonZero(params.Count)). - addKeyValue("context", formatNonZero(params.ContextWindow)). - build() - return sr.renderWithParams(v, "Sourcegraph", args, func() string { return renderPlainContent(v, v.result.Content) }) @@ -475,25 +485,47 @@ type agentRenderer struct { baseRenderer } +func RoundedEnumerator(children tree.Children, index int) string { + if children.Length()-1 == index { + return " ╰──" + } + return " ├──" +} + // Render displays agent task parameters and result content func (tr agentRenderer) Render(v *toolCallCmp) string { + t := styles.CurrentTheme() var params agent.AgentParams - if err := tr.unmarshalParams(v.call.Input, ¶ms); err != nil { - return tr.renderError(v, "Invalid task parameters") - } + tr.unmarshalParams(v.call.Input, ¶ms) + prompt := params.Prompt prompt = strings.ReplaceAll(prompt, "\n", " ") - args := newParamBuilder().addMain(prompt).build() - header := tr.makeHeader(v, "Task", v.textWidth(), args...) - t := tree.Root(header) + header := tr.makeHeader(v, "Agent", v.textWidth()) + if res, done := earlyState(header, v); done { + return res + } + taskTag := t.S().Base.Padding(0, 1).MarginLeft(1).Background(t.BlueLight).Foreground(t.White).Render("Task") + remainingWidth := v.textWidth() - lipgloss.Width(header) - lipgloss.Width(taskTag) - 2 // -2 for padding + prompt = t.S().Muted.Width(remainingWidth).Render(prompt) + header = lipgloss.JoinVertical( + lipgloss.Left, + header, + "", + lipgloss.JoinHorizontal( + lipgloss.Left, + taskTag, + " ", + prompt, + ), + ) + childTools := tree.Root(header) for _, call := range v.nestedToolCalls { - t.Child(call.View()) + childTools.Child(call.View()) } - parts := []string{ - t.Enumerator(tree.RoundedEnumerator).String(), + childTools.Enumerator(RoundedEnumerator).String(), } if v.result.ToolCallID == "" { v.spinning = true @@ -516,7 +548,8 @@ func (tr agentRenderer) Render(v *toolCallCmp) string { } // renderParamList renders params, params[0] (params[1]=params[2] ....) -func renderParamList(paramsWidth int, params ...string) string { +func renderParamList(nested bool, paramsWidth int, params ...string) string { + t := styles.CurrentTheme() if len(params) == 0 { return "" } @@ -526,7 +559,10 @@ func renderParamList(paramsWidth int, params ...string) string { } if len(params) == 1 { - return mainParam + if nested { + return t.S().Muted.Render(mainParam) + } + return t.S().Subtle.Render(mainParam) } otherParams := params[1:] // create pairs of key/value @@ -547,15 +583,21 @@ func renderParamList(paramsWidth int, params ...string) string { partsRendered := strings.Join(parts, ", ") remainingWidth := paramsWidth - lipgloss.Width(partsRendered) - 3 // count for " ()" if remainingWidth < 30 { + if nested { + return t.S().Muted.Render(mainParam) + } // No space for the params, just show the main - return mainParam + return t.S().Subtle.Render(mainParam) } if len(parts) > 0 { mainParam = fmt.Sprintf("%s (%s)", mainParam, strings.Join(parts, ", ")) } - return ansi.Truncate(mainParam, paramsWidth, "...") + if nested { + return t.S().Muted.Render(ansi.Truncate(mainParam, paramsWidth, "...")) + } + return t.S().Subtle.Render(ansi.Truncate(mainParam, paramsWidth, "...")) } // earlyState returns immediately‑rendered error/cancelled/ongoing states. @@ -566,21 +608,21 @@ func earlyState(header string, v *toolCallCmp) (string, bool) { case v.result.IsError: message = v.renderToolError() case v.cancelled: - message = "Cancelled" + message = t.S().Base.Padding(0, 1).Background(t.Border).Render("Cancelled") case v.result.ToolCallID == "": - message = "Waiting for tool to start..." + message = t.S().Base.Padding(0, 1).Background(t.Accent).Foreground(t.FgSubtle).Render("Waiting for tool to start...") default: return "", false } message = t.S().Base.PaddingLeft(2).Render(message) - return lipgloss.JoinVertical(lipgloss.Left, header, message), true + return lipgloss.JoinVertical(lipgloss.Left, header, "", message), true } func joinHeaderBody(header, body string) string { t := styles.CurrentTheme() body = t.S().Base.PaddingLeft(2).Render(body) - return lipgloss.JoinVertical(lipgloss.Left, header, body, "") + return lipgloss.JoinVertical(lipgloss.Left, header, "", body, "") } func renderPlainContent(v *toolCallCmp, content string) string { @@ -600,46 +642,56 @@ func renderPlainContent(v *toolCallCmp, content string) string { } out = append(out, t.S().Muted. Width(width). - Background(t.BgSubtle). + Background(t.BgBaseLighter). Render(ln)) } if len(lines) > responseContextHeight { out = append(out, t.S().Muted. - Background(t.BgSubtle). + Background(t.BgBaseLighter). Width(width). Render(fmt.Sprintf("... (%d lines)", len(lines)-responseContextHeight))) } return strings.Join(out, "\n") } +func pad(v any, width int) string { + s := fmt.Sprintf("%v", v) + w := ansi.StringWidth(s) + if w >= width { + return s + } + return strings.Repeat(" ", width-w) + s +} + func renderCodeContent(v *toolCallCmp, path, content string, offset int) string { t := styles.CurrentTheme() truncated := truncateHeight(content, responseContextHeight) - highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgSubtle) + highlighted, _ := highlight.SyntaxHighlight(truncated, path, t.BgBase) lines := strings.Split(highlighted, "\n") if len(strings.Split(content, "\n")) > responseContextHeight { lines = append(lines, t.S().Muted. - Background(t.BgSubtle). - Width(v.textWidth()-2). - Render(fmt.Sprintf("... (%d lines)", len(strings.Split(content, "\n"))-responseContextHeight))) + Background(t.BgBase). + Render(fmt.Sprintf(" …(%d lines)", len(strings.Split(content, "\n"))-responseContextHeight))) } + maxLineNumber := len(lines) + offset + padding := lipgloss.Width(fmt.Sprintf("%d", maxLineNumber)) for i, ln := range lines { - num := t.S().Muted. - Background(t.BgSubtle). - PaddingLeft(4). - PaddingRight(2). - Render(fmt.Sprintf("%d", i+1+offset)) - w := v.textWidth() - 2 - lipgloss.Width(num) // -2 for left padding + num := t.S().Base. + Foreground(t.FgMuted). + Background(t.BgBase). + PaddingRight(1). + PaddingLeft(1). + Render(pad(i+1+offset, padding)) + w := v.textWidth() - 10 - lipgloss.Width(num) // -4 for left padding lines[i] = lipgloss.JoinHorizontal(lipgloss.Left, num, t.S().Base. - Width(w). - Background(t.BgSubtle). - Render(v.fit(ln, w))) + PaddingLeft(1). + Render(v.fit(ln, w-1))) } return lipgloss.JoinVertical(lipgloss.Left, lines...) } @@ -647,8 +699,9 @@ func renderCodeContent(v *toolCallCmp, path, content string, offset int) string func (v *toolCallCmp) renderToolError() string { t := styles.CurrentTheme() err := strings.ReplaceAll(v.result.Content, "\n", " ") - err = fmt.Sprintf("Error: %s", err) - return t.S().Base.Foreground(t.Error).Render(v.fit(err, v.textWidth())) + errTag := t.S().Base.Padding(0, 1).Background(t.Red).Foreground(t.White).Render("ERROR") + err = fmt.Sprintf("%s %s", errTag, t.S().Base.Foreground(t.FgHalfMuted).Render(v.fit(err, v.textWidth()-2-lipgloss.Width(errTag)))) + return err } func truncateHeight(s string, h int) string { @@ -662,7 +715,7 @@ func truncateHeight(s string, h int) string { func prettifyToolName(name string) string { switch name { case agent.AgentToolName: - return "Task" + return "Agent" case tools.BashToolName: return "Bash" case tools.EditToolName: diff --git a/internal/tui/components/chat/messages/tool.go b/internal/tui/components/chat/messages/tool.go index 65274b11c489e4e78ef0e70fe7a3adbe1f82806d..458e5ed320c2ce6c33fc35afef8a076a6e594e56 100644 --- a/internal/tui/components/chat/messages/tool.go +++ b/internal/tui/components/chat/messages/tool.go @@ -25,7 +25,7 @@ type ToolCallCmp interface { SetToolResult(message.ToolResult) // Update tool result SetToolCall(message.ToolCall) // Update tool call SetCancelled() // Mark as cancelled - ParentMessageId() string // Get parent message ID + ParentMessageID() string // Get parent message ID Spinning() bool // Animation state for pending tools GetNestedToolCalls() []ToolCallCmp // Get nested tool calls SetNestedToolCalls([]ToolCallCmp) // Set nested tool calls @@ -83,10 +83,10 @@ func WithToolCallNestedCalls(calls []ToolCallCmp) ToolCallOption { // NewToolCallCmp creates a new tool call component with the given parent message ID, // tool call, and optional configuration -func NewToolCallCmp(parentMessageId string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp { +func NewToolCallCmp(parentMessageID string, tc message.ToolCall, opts ...ToolCallOption) ToolCallCmp { m := &toolCallCmp{ call: tc, - parentMessageID: parentMessageId, + parentMessageID: parentMessageID, } for _, opt := range opts { opt(m) @@ -137,9 +137,6 @@ func (m *toolCallCmp) View() tea.View { box := m.style() if !m.call.Finished && !m.cancelled { - if m.isNested { - return tea.NewView(box.Render(m.renderPending())) - } return tea.NewView(box.Render(m.renderPending())) } @@ -166,8 +163,8 @@ func (m *toolCallCmp) SetToolCall(call message.ToolCall) { } } -// ParentMessageId returns the ID of the message that initiated this tool call -func (m *toolCallCmp) ParentMessageId() string { +// ParentMessageID returns the ID of the message that initiated this tool call +func (m *toolCallCmp) ParentMessageID() string { return m.parentMessageID } @@ -210,9 +207,13 @@ func (m *toolCallCmp) SetIsNested(isNested bool) { // renderPending displays the tool name with a loading animation for pending tool calls func (m *toolCallCmp) renderPending() string { t := styles.CurrentTheme() + if m.isNested { + tool := t.S().Base.Foreground(t.FgHalfMuted).Render(prettifyToolName(m.call.Name)) + return fmt.Sprintf("%s %s", tool, m.anim.View()) + } icon := t.S().Base.Foreground(t.GreenDark).Render(styles.ToolPending) tool := t.S().Base.Foreground(t.Blue).Render(prettifyToolName(m.call.Name)) - return fmt.Sprintf("%s %s: %s", icon, tool, m.anim.View()) + return fmt.Sprintf("%s %s %s", icon, tool, m.anim.View()) } // style returns the lipgloss style for the tool call component. @@ -229,14 +230,17 @@ func (m *toolCallCmp) style() lipgloss.Style { // textWidth calculates the available width for text content, // accounting for borders and padding func (m *toolCallCmp) textWidth() int { + if m.isNested { + return m.width - 6 + } return m.width - 5 // take into account the border and PaddingLeft } // fit truncates content to fit within the specified width with ellipsis func (m *toolCallCmp) fit(content string, width int) string { t := styles.CurrentTheme() - lineStyle := t.S().Muted.Background(t.BgSubtle) - dots := lineStyle.Render("...") + lineStyle := t.S().Muted + dots := lineStyle.Render("…") return ansi.Truncate(content, width, dots) } diff --git a/internal/tui/components/chat/sidebar/sidebar.go b/internal/tui/components/chat/sidebar/sidebar.go index 54d9cb6b3ab78aa0d673a6c143b2119d73d08d7f..12beb139071da58168e9c4f07b991c9ad05f7320 100644 --- a/internal/tui/components/chat/sidebar/sidebar.go +++ b/internal/tui/components/chat/sidebar/sidebar.go @@ -288,9 +288,9 @@ func (m *sidebarCmp) filesBlock() string { }) for _, file := range files { - // Extract just the filename from the path - - // Create status indicators for additions/deletions + if file.Additions == 0 && file.Deletions == 0 { + continue // skip files with no changes + } var statusParts []string if file.Additions > 0 { statusParts = append(statusParts, t.S().Base.Foreground(t.Success).Render(fmt.Sprintf("+%d", file.Additions))) diff --git a/internal/tui/components/core/helpers.go b/internal/tui/components/core/helpers.go index bd14febf1c94577fbd4882248d1344f364464b46..b13a23d868a80518fe6a5079f2053fe2d38463c8 100644 --- a/internal/tui/components/core/helpers.go +++ b/internal/tui/components/core/helpers.go @@ -148,8 +148,9 @@ func SelectableButtons(buttons []ButtonOpts, spacing string) string { } func DiffFormatter() *diffview.DiffView { + t := styles.CurrentTheme() formatDiff := diffview.New() style := chroma.MustNewStyle("crush", styles.GetChromaTheme()) - diff := formatDiff.ChromaStyle(style) + diff := formatDiff.ChromaStyle(style).Style(t.S().Diff) return diff } diff --git a/internal/tui/components/core/list/keys.go b/internal/tui/components/core/list/keys.go index 4ad2a9e27807063609215f1f6c834872ceff2aac..0e33b62d1b615ea49866881b770d292486b688de 100644 --- a/internal/tui/components/core/list/keys.go +++ b/internal/tui/components/core/list/keys.go @@ -32,10 +32,10 @@ func DefaultKeyMap() KeyMap { key.WithKeys("k"), ), UpOneItem: key.NewBinding( - key.WithKeys("shift+up", "shift+k"), + key.WithKeys("shift+up", "K"), ), DownOneItem: key.NewBinding( - key.WithKeys("shift+down", "shift+j"), + key.WithKeys("shift+down", "J"), ), HalfPageDown: key.NewBinding( key.WithKeys("d"), @@ -47,7 +47,7 @@ func DefaultKeyMap() KeyMap { key.WithKeys("g", "home"), ), End: key.NewBinding( - key.WithKeys("shift+g", "end"), + key.WithKeys("G", "end"), ), } } diff --git a/internal/tui/components/core/list/list.go b/internal/tui/components/core/list/list.go index 14a32777117a744b708c280f2d0bc3dde70306c5..79ad6d62b0744650a5d2a0deb5f3b46582704407 100644 --- a/internal/tui/components/core/list/list.go +++ b/internal/tui/components/core/list/list.go @@ -749,8 +749,8 @@ func (m *model) ensureVisibleReverse(cachedItem renderedItem) { func (m *model) goToBottom() tea.Cmd { cmds := []tea.Cmd{m.blurSelected()} m.viewState.reverse = true + m.selectionState.selectedIndex = m.findLastSelectableItem() if m.isFocused { - m.selectionState.selectedIndex = m.findLastSelectableItem() cmds = append(cmds, m.focusSelected()) } m.ResetView() @@ -764,7 +764,9 @@ func (m *model) goToTop() tea.Cmd { cmds := []tea.Cmd{m.blurSelected()} m.viewState.reverse = false m.selectionState.selectedIndex = m.findFirstSelectableItem() - cmds = append(cmds, m.focusSelected()) + if m.isFocused { + cmds = append(cmds, m.focusSelected()) + } m.ResetView() return tea.Batch(cmds...) } diff --git a/internal/tui/components/core/status/status.go b/internal/tui/components/core/status/status.go index 7b91c186f7ab9e572685de3e346204873d8cede2..bded453e78ecdfd85d6d182b4785a55d641dfd44 100644 --- a/internal/tui/components/core/status/status.go +++ b/internal/tui/components/core/status/status.go @@ -1,6 +1,7 @@ package status import ( + "strings" "time" "github.com/charmbracelet/bubbles/v2/help" @@ -10,6 +11,8 @@ import ( "github.com/charmbracelet/crush/internal/session" "github.com/charmbracelet/crush/internal/tui/styles" "github.com/charmbracelet/crush/internal/tui/util" + "github.com/charmbracelet/lipgloss/v2" + "github.com/charmbracelet/x/ansi" ) type StatusCmp interface { @@ -85,6 +88,7 @@ func (m *statusCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { TTL: msg.Payload.PersistTime, } } + return m, m.clearMessageCmd(m.info.TTL) } } return m, nil @@ -94,18 +98,32 @@ func (m *statusCmp) View() tea.View { t := styles.CurrentTheme() status := t.S().Base.Padding(0, 1, 1, 1).Render(m.help.View(m.keyMap)) if m.info.Msg != "" { - switch m.info.Type { - case util.InfoTypeError: - status = t.S().Base.Background(t.Error).Padding(0, 1).Width(m.width).Render(m.info.Msg) - case util.InfoTypeWarn: - status = t.S().Base.Background(t.Warning).Padding(0, 1).Width(m.width).Render(m.info.Msg) - default: - status = t.S().Base.Background(t.Info).Padding(0, 1).Width(m.width).Render(m.info.Msg) - } + status = m.infoMsg() } return tea.NewView(status) } +func (m *statusCmp) infoMsg() string { + t := styles.CurrentTheme() + message := "" + infoType := "" + switch m.info.Type { + case util.InfoTypeError: + infoType = t.S().Base.Background(t.Red).Padding(0, 1).Render("ERROR") + width := m.width - lipgloss.Width(infoType) + message = t.S().Base.Background(t.Error).Foreground(t.White).Padding(0, 1).Width(width).Render(ansi.Truncate(m.info.Msg, width, "…")) + case util.InfoTypeWarn: + infoType = t.S().Base.Foreground(t.BgOverlay).Background(t.Yellow).Padding(0, 1).Render("WARNING") + width := m.width - lipgloss.Width(infoType) + message = t.S().Base.Foreground(t.BgOverlay).Background(t.Warning).Padding(0, 1).Width(width).Render(ansi.Truncate(m.info.Msg, width, "…")) + default: + infoType = t.S().Base.Foreground(t.BgOverlay).Background(t.Green).Padding(0, 1).Render("OKAY!") + width := m.width - lipgloss.Width(infoType) + message = t.S().Base.Background(t.Success).Foreground(t.White).Padding(0, 1).Width(width).Render(ansi.Truncate(m.info.Msg, width, "…")) + } + return strings.Join([]string{infoType, message}, "") +} + func (m *statusCmp) ToggleFullHelp() { m.help.ShowAll = !m.help.ShowAll } @@ -119,7 +137,7 @@ func NewStatusCmp(keyMap help.KeyMap) StatusCmp { help := help.New() help.Styles = t.S().Help return &statusCmp{ - messageTTL: 10 * time.Second, + messageTTL: 5 * time.Second, help: help, keyMap: keyMap, } diff --git a/internal/tui/components/dialogs/commands/arguments.go b/internal/tui/components/dialogs/commands/arguments.go index 1128acf21b031ab914662f6686ffc9f57b9b7653..7e4bdcc271c0dcbdf1923c773f204c14a0fbf32b 100644 --- a/internal/tui/components/dialogs/commands/arguments.go +++ b/internal/tui/components/dialogs/commands/arguments.go @@ -211,15 +211,6 @@ func (c *commandArgumentsDialogCmp) moveCursor(cursor *tea.Cursor) *tea.Cursor { return cursor } -func (c *commandArgumentsDialogCmp) style() lipgloss.Style { - t := styles.CurrentTheme() - return t.S().Base. - Width(c.width). - Padding(1). - Border(lipgloss.RoundedBorder()). - BorderForeground(t.BorderFocus) -} - func (c *commandArgumentsDialogCmp) Position() (int, int) { row := c.wHeight / 2 row -= c.wHeight / 2 diff --git a/internal/tui/components/dialogs/models/models.go b/internal/tui/components/dialogs/models/models.go index f8d23006929fa42cfb5d1a6d2841080d2541b330..02bb8514e59c94aaaf85c5739d8b8a7e92b0d1d2 100644 --- a/internal/tui/components/dialogs/models/models.go +++ b/internal/tui/components/dialogs/models/models.go @@ -193,15 +193,15 @@ func (m *modelDialogCmp) listHeight() int { func GetSelectedModel(cfg *config.Config) models.Model { agentCfg := cfg.Agents[config.AgentCoder] - selectedModelId := agentCfg.Model - return models.SupportedModels[selectedModelId] + selectedModelID := agentCfg.Model + return models.SupportedModels[selectedModelID] } func getEnabledProviders(cfg *config.Config) []models.ModelProvider { var providers []models.ModelProvider - for providerId, provider := range cfg.Providers { + for providerID, provider := range cfg.Providers { if !provider.Disabled { - providers = append(providers, providerId) + providers = append(providers, providerID) } } diff --git a/internal/tui/components/dialogs/permissions/keys.go b/internal/tui/components/dialogs/permissions/keys.go index d626eecf9a819cfb209823f922f96dfb58ea3ca4..c77810f15be294e5a71f911d61da93b324bd7f17 100644 --- a/internal/tui/components/dialogs/permissions/keys.go +++ b/internal/tui/components/dialogs/permissions/keys.go @@ -11,7 +11,12 @@ type KeyMap struct { Select, Allow, AllowSession, - Deny key.Binding + Deny, + ToggleDiffMode, + ScrollDown, + ScrollUp key.Binding + ScrollLeft, + ScrollRight key.Binding } func DefaultKeyMap() KeyMap { @@ -41,9 +46,29 @@ func DefaultKeyMap() KeyMap { key.WithHelp("d", "deny"), ), Select: key.NewBinding( - key.WithKeys("enter", "tab", "ctrl+y"), + key.WithKeys("enter", "ctrl+y"), key.WithHelp("enter", "confirm"), ), + ToggleDiffMode: key.NewBinding( + key.WithKeys("t"), + key.WithHelp("t", "toggle diff mode"), + ), + ScrollDown: key.NewBinding( + key.WithKeys("shift+down", "J"), + key.WithHelp("shift+↓", "scroll down"), + ), + ScrollUp: key.NewBinding( + key.WithKeys("shift+up", "K"), + key.WithHelp("shift+↑", "scroll up"), + ), + ScrollLeft: key.NewBinding( + key.WithKeys("shift+left", "H"), + key.WithHelp("shift+←", "scroll left"), + ), + ScrollRight: key.NewBinding( + key.WithKeys("shift+right", "L"), + key.WithHelp("shift+→", "scroll right"), + ), } } @@ -57,6 +82,11 @@ func (k KeyMap) KeyBindings() []key.Binding { k.Allow, k.AllowSession, k.Deny, + k.ToggleDiffMode, + k.ScrollDown, + k.ScrollUp, + k.ScrollLeft, + k.ScrollRight, } } @@ -74,9 +104,14 @@ func (k KeyMap) FullHelp() [][]key.Binding { // ShortHelp implements help.KeyMap. func (k KeyMap) ShortHelp() []key.Binding { return []key.Binding{ - k.Allow, - k.AllowSession, - k.Deny, - k.Select, + k.ToggleDiffMode, + key.NewBinding( + key.WithKeys("shift+left", "shift+down", "shift+up", "shift+right"), + key.WithHelp("shift+←↓↑→", "scroll"), + ), + key.NewBinding( + key.WithKeys("shift+h", "shift+j", "shift+k", "shift+l"), + key.WithHelp("shift+hjkl", "scroll"), + ), } } diff --git a/internal/tui/components/dialogs/permissions/permissions.go b/internal/tui/components/dialogs/permissions/permissions.go index 4d0563b244af45d8640a741bb79baa9007a0ff3c..1fc6398ce24205537806d0aebd4cb82abcb0b122 100644 --- a/internal/tui/components/dialogs/permissions/permissions.go +++ b/internal/tui/components/dialogs/permissions/permissions.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" + "github.com/charmbracelet/bubbles/v2/help" "github.com/charmbracelet/bubbles/v2/key" "github.com/charmbracelet/bubbles/v2/viewport" tea "github.com/charmbracelet/bubbletea/v2" @@ -50,6 +51,11 @@ type permissionDialogCmp struct { contentViewPort viewport.Model selectedOption int // 0: Allow, 1: Allow for session, 2: Deny + // Diff view state + diffSplitMode bool // true for split, false for unified + diffXOffset int // horizontal scroll offset + diffYOffset int // vertical scroll offset + keyMap KeyMap } @@ -101,6 +107,21 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { util.CmdHandler(dialogs.CloseDialogMsg{}), util.CmdHandler(PermissionResponseMsg{Action: PermissionDeny, Permission: p.permission}), ) + case key.Matches(msg, p.keyMap.ToggleDiffMode): + p.diffSplitMode = !p.diffSplitMode + return p, nil + case key.Matches(msg, p.keyMap.ScrollDown): + p.diffYOffset += 1 + return p, nil + case key.Matches(msg, p.keyMap.ScrollUp): + p.diffYOffset = max(0, p.diffYOffset-1) + return p, nil + case key.Matches(msg, p.keyMap.ScrollLeft): + p.diffXOffset = max(0, p.diffXOffset-5) + return p, nil + case key.Matches(msg, p.keyMap.ScrollRight): + p.diffXOffset += 5 + return p, nil default: // Pass other keys to viewport viewPort, cmd := p.contentViewPort.Update(msg) @@ -269,8 +290,15 @@ func (p *permissionDialogCmp) renderEditContent() string { formatter := core.DiffFormatter(). Before(fsext.PrettyPath(pr.FilePath), pr.OldContent). After(fsext.PrettyPath(pr.FilePath), pr.NewContent). + Height(p.contentViewPort.Height()). Width(p.contentViewPort.Width()). - Split() + XOffset(p.diffXOffset). + YOffset(p.diffYOffset) + if p.diffSplitMode { + formatter = formatter.Split() + } else { + formatter = formatter.Unified() + } diff := formatter.String() contentHeight := min(p.height-9, lipgloss.Height(diff)) @@ -367,11 +395,13 @@ func (p *permissionDialogCmp) render() string { // Render content based on tool type var contentFinal string + var contentHelp string switch p.permission.ToolName { case tools.BashToolName: contentFinal = p.renderBashContent() case tools.EditToolName: contentFinal = p.renderEditContent() + contentHelp = help.New().View(p.keyMap) case tools.WriteToolName: contentFinal = p.renderWriteContent() case tools.FetchToolName: @@ -381,8 +411,7 @@ func (p *permissionDialogCmp) render() string { } // Calculate content height dynamically based on window size - content := lipgloss.JoinVertical( - lipgloss.Top, + strs := []string{ title, "", headerContent, @@ -390,7 +419,11 @@ func (p *permissionDialogCmp) render() string { "", buttons, "", - ) + } + if contentHelp != "" { + strs = append(strs, "", contentHelp) + } + content := lipgloss.JoinVertical(lipgloss.Top, strs...) return baseStyle. Padding(0, 1). diff --git a/internal/tui/keys.go b/internal/tui/keys.go index dda3ad4dba02192626adf74540d2c62aad44a5a1..8af028cd10338eba2108f94156035b8f58f342e2 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -61,8 +61,8 @@ func (k KeyMap) FullHelp() [][]key.Binding { } } - for i := 0; i < len(cleaned); i += 2 { - end := min(i+2, len(cleaned)) + for i := 0; i < len(cleaned); i += 3 { + end := min(i+3, len(cleaned)) m = append(m, cleaned[i:end]) } return m diff --git a/internal/tui/page/chat/chat.go b/internal/tui/page/chat/chat.go index 909ef31f451e8eb5bfabd61e3cb1750e738f838d..e79eb35e27cb5805c000e774b209c66c96b01ebd 100644 --- a/internal/tui/page/chat/chat.go +++ b/internal/tui/page/chat/chat.go @@ -3,6 +3,7 @@ package chat import ( "context" "strings" + "time" "github.com/charmbracelet/bubbles/v2/key" tea "github.com/charmbracelet/bubbletea/v2" @@ -33,6 +34,7 @@ type ( ChatFocusedMsg struct { Focused bool // True if the chat input is focused, false otherwise } + CancelTimerExpiredMsg struct{} ) type ChatPage interface { @@ -57,6 +59,8 @@ type chatPage struct { showDetails bool // Show details in the header header header.Header compactSidebar layout.Container + + cancelPending bool // True if ESC was pressed once and waiting for second press } func (p *chatPage) Init() tea.Cmd { @@ -67,9 +71,19 @@ func (p *chatPage) Init() tea.Cmd { ) } +// cancelTimerCmd creates a command that expires the cancel timer after 2 seconds +func (p *chatPage) cancelTimerCmd() tea.Cmd { + return tea.Tick(2*time.Second, func(time.Time) tea.Msg { + return CancelTimerExpiredMsg{} + }) +} + func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmds []tea.Cmd switch msg := msg.(type) { + case CancelTimerExpiredMsg: + p.cancelPending = false + return p, nil case tea.WindowSizeMsg: h, cmd := p.header.Update(msg) cmds = append(cmds, cmd) @@ -181,10 +195,16 @@ func (p *chatPage) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return p, tea.Batch(cmds...) case key.Matches(msg, p.keyMap.Cancel): if p.session.ID != "" { - // Cancel the current session's generation process - // This allows users to interrupt long-running operations - p.app.CoderAgent.Cancel(p.session.ID) - return p, nil + if p.cancelPending { + // Second ESC press - actually cancel the session + p.cancelPending = false + p.app.CoderAgent.Cancel(p.session.ID) + return p, nil + } else { + // First ESC press - start the timer + p.cancelPending = true + return p, p.cancelTimerCmd() + } } case key.Matches(msg, p.keyMap.Details): if p.session.ID == "" || !p.compactMode { @@ -336,7 +356,14 @@ func (p *chatPage) Bindings() []key.Binding { p.keyMap.AddAttachment, } if p.app.CoderAgent.IsBusy() { - bindings = append([]key.Binding{p.keyMap.Cancel}, bindings...) + cancelBinding := p.keyMap.Cancel + if p.cancelPending { + cancelBinding = key.NewBinding( + key.WithKeys("esc"), + key.WithHelp("esc", "press again to cancel"), + ) + } + bindings = append([]key.Binding{cancelBinding}, bindings...) } if p.chatFocused { diff --git a/internal/tui/styles/crush.go b/internal/tui/styles/crush.go index 41acdfad103e70b19955a84722a27876575d15b4..975c7f6080e654bad0a7d760543535bc6eea5827 100644 --- a/internal/tui/styles/crush.go +++ b/internal/tui/styles/crush.go @@ -14,9 +14,10 @@ func NewCrushTheme() *Theme { Tertiary: charmtone.Bok, Accent: charmtone.Zest, // Backgrounds - BgBase: charmtone.Pepper, - BgSubtle: charmtone.Charcoal, - BgOverlay: charmtone.Iron, + BgBase: charmtone.Pepper, + BgBaseLighter: Lighten(charmtone.Pepper, 2), + BgSubtle: charmtone.Charcoal, + BgOverlay: charmtone.Iron, // Foregrounds FgBase: charmtone.Ash, @@ -38,7 +39,10 @@ func NewCrushTheme() *Theme { // Colors White: charmtone.Butter, - Blue: charmtone.Malibu, + BlueLight: charmtone.Sardine, + Blue: charmtone.Malibu, + + Yellow: charmtone.Mustard, Green: charmtone.Julep, GreenDark: charmtone.Guac, diff --git a/internal/tui/styles/theme.go b/internal/tui/styles/theme.go index 8d3c014048b950a034357f10f0e7f6ce7883d2f0..b6a5b4d1e2b41b7bb1190d5a802bd48f4aeceec3 100644 --- a/internal/tui/styles/theme.go +++ b/internal/tui/styles/theme.go @@ -10,6 +10,7 @@ import ( "github.com/charmbracelet/bubbles/v2/textarea" "github.com/charmbracelet/bubbles/v2/textinput" tea "github.com/charmbracelet/bubbletea/v2" + "github.com/charmbracelet/crush/internal/exp/diffview" "github.com/charmbracelet/glamour/v2/ansi" "github.com/charmbracelet/lipgloss/v2" "github.com/lucasb-eyer/go-colorful" @@ -31,9 +32,10 @@ type Theme struct { Tertiary color.Color Accent color.Color - BgBase color.Color - BgSubtle color.Color - BgOverlay color.Color + BgBase color.Color + BgBaseLighter color.Color + BgSubtle color.Color + BgOverlay color.Color FgBase color.Color FgMuted color.Color @@ -52,8 +54,13 @@ type Theme struct { // Colors // White White color.Color + // Blues - Blue color.Color + BlueLight color.Color + Blue color.Color + + // Yellows + Yellow color.Color // Greens Green color.Color @@ -65,26 +72,9 @@ type Theme struct { RedDark color.Color RedLight color.Color - // TODO: add any others needed - styles *Styles } -type Diff struct { - Added color.Color - Removed color.Color - Context color.Color - HunkHeader color.Color - HighlightAdded color.Color - HighlightRemoved color.Color - AddedBg color.Color - RemovedBg color.Color - ContextBg color.Color - LineNumber color.Color - AddedLineNumberBg color.Color - RemovedLineNumberBg color.Color -} - type Styles struct { Base lipgloss.Style SelectedBase lipgloss.Style @@ -112,7 +102,7 @@ type Styles struct { Help help.Styles // Diff - Diff Diff + Diff diffview.Style // FilePicker FilePicker filepicker.Styles @@ -421,22 +411,50 @@ func (t *Theme) buildStyles() *Styles { FullSeparator: base.Foreground(t.Border), }, - // TODO: Fix this this is bad - Diff: Diff{ - Added: t.Green, - Removed: t.Red, - Context: t.FgSubtle, - HunkHeader: t.FgSubtle, - HighlightAdded: t.GreenLight, - HighlightRemoved: t.RedLight, - AddedBg: t.GreenDark, - RemovedBg: t.RedDark, - ContextBg: t.BgSubtle, - LineNumber: t.FgMuted, - AddedLineNumberBg: t.GreenDark, - RemovedLineNumberBg: t.RedDark, + Diff: diffview.Style{ + DividerLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(t.FgHalfMuted). + Background(t.BgBaseLighter), + Code: lipgloss.NewStyle(). + Foreground(t.FgHalfMuted). + Background(t.BgBaseLighter), + }, + MissingLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Background(t.BgBaseLighter), + Code: lipgloss.NewStyle(). + Background(t.BgBaseLighter), + }, + EqualLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(t.FgMuted). + Background(t.BgBase), + Code: lipgloss.NewStyle(). + Foreground(t.FgMuted). + Background(t.BgBase), + }, + InsertLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#629657")). + Background(lipgloss.Color("#2b322a")), + Symbol: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#629657")). + Background(lipgloss.Color("#323931")), + Code: lipgloss.NewStyle(). + Background(lipgloss.Color("#323931")), + }, + DeleteLine: diffview.LineStyle{ + LineNumber: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#a45c59")). + Background(lipgloss.Color("#312929")), + Symbol: lipgloss.NewStyle(). + Foreground(lipgloss.Color("#a45c59")). + Background(lipgloss.Color("#383030")), + Code: lipgloss.NewStyle(). + Background(lipgloss.Color("#383030")), + }, }, - FilePicker: filepicker.Styles{ DisabledCursor: base.Foreground(t.FgMuted), Cursor: base.Foreground(t.FgBase), diff --git a/internal/tui/tui.go b/internal/tui/tui.go index e7f2f99da3e7bbf726ac78d654d6501018ba1351..c6dee6532993becfbda24d115b8e1e5d05e4fd60 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -94,12 +94,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyboardEnhancementsMsg: - logging.Info( - "Keyboard enhancements detected", - "Disambiguation", msg.SupportsKeyDisambiguation(), - "ReleaseKeys", msg.SupportsKeyReleases(), - "UniformKeys", msg.SupportsUniformKeyLayout(), - ) return a, nil case tea.WindowSizeMsg: return a, a.handleWindowResize(msg.Width, msg.Height) @@ -260,7 +254,7 @@ func (a *appModel) handleWindowResize(width, height int) tea.Cmd { var cmds []tea.Cmd a.wWidth, a.wHeight = width, height if a.showingFullHelp { - height -= 3 + height -= 4 } else { height -= 2 } diff --git a/todos.md b/todos.md index ca0ad74ef08258b6b209a3da7ff79f3922ae9e40..080bf64df8dd6e4d5a496531ba5f8f2be5fcf8a4 100644 --- a/todos.md +++ b/todos.md @@ -20,7 +20,16 @@ - [ ] Parallel tool calls and permissions - [ ] Run the tools in parallel and add results in parallel - [ ] Show multiple permissions dialogs +- [ ] Add another space around buttons +- [ ] Completions + - [ ] Should change the help to show the completions stuff + - [ ] Should make it wider + - [ ] Tab and ctrl+y should accept + - [ ] Words should line up + - [ ] If there are no completions and cick tab/ctrl+y/enter it should close it - [ ] Investigate messages issues + - [ ] Make the agent separator look like the + - [ ] Cleanup tool calls (watch all states) - [ ] Weird behavior sometimes the message does not update - [ ] Message length (I saw the message go beyond the correct length when there are errors) - [ ] Address UX issues