diff --git a/suggestions.go b/suggestions.go index 0618845..75baa20 100644 --- a/suggestions.go +++ b/suggestions.go @@ -2,10 +2,15 @@ package main import ( "strings" + "sync" "github.com/charmbracelet/x/ansi" ) +const suggestionRenderCacheLimit = 256 + +var renderedSuggestions = newSuggestionRenderCache(suggestionRenderCacheLimit) + type parsedCommentBody struct { Prose string Suggestions []string @@ -16,6 +21,98 @@ type codeRange struct { End int } +type suggestionRenderCacheKey struct { + path string + removed string + removedLines int + replacement string + width int +} + +type suggestionRenderCacheEntry struct { + removed []detailLine + added []detailLine +} + +type suggestionRenderCache struct { + mu sync.Mutex + limit int + entries map[suggestionRenderCacheKey]suggestionRenderCacheEntry + order []suggestionRenderCacheKey +} + +func newSuggestionRenderCache(limit int) *suggestionRenderCache { + return &suggestionRenderCache{ + limit: limit, entries: make(map[suggestionRenderCacheKey]suggestionRenderCacheEntry), + } +} + +func (c *suggestionRenderCache) get( + key suggestionRenderCacheKey, +) (suggestionRenderCacheEntry, bool) { + c.mu.Lock() + defer c.mu.Unlock() + entry, ok := c.entries[key] + return cloneSuggestionRenderEntry(entry), ok +} + +func (c *suggestionRenderCache) put( + key suggestionRenderCacheKey, entry suggestionRenderCacheEntry, +) suggestionRenderCacheEntry { + c.mu.Lock() + defer c.mu.Unlock() + if cached, ok := c.entries[key]; ok { + return cloneSuggestionRenderEntry(cached) + } + if len(c.entries) >= c.limit { + delete(c.entries, c.order[0]) + c.order = c.order[1:] + } + c.entries[key] = cloneSuggestionRenderEntry(entry) + c.order = append(c.order, key) + return cloneSuggestionRenderEntry(entry) +} + +func (c *suggestionRenderCache) clear() { + c.mu.Lock() + defer c.mu.Unlock() + c.entries = make(map[suggestionRenderCacheKey]suggestionRenderCacheEntry) + c.order = nil +} + +func cloneSuggestionRenderEntry(entry suggestionRenderCacheEntry) suggestionRenderCacheEntry { + return suggestionRenderCacheEntry{ + removed: append([]detailLine(nil), entry.removed...), + added: append([]detailLine(nil), entry.added...), + } +} + +func renderSuggestion( + path string, reviewed []string, replacement string, width int, +) suggestionRenderCacheEntry { + key := suggestionRenderCacheKey{ + path: path, removed: strings.Join(reviewed, "\n"), removedLines: len(reviewed), + replacement: replacement, width: width, + } + if cached, ok := renderedSuggestions.get(key); ok { + return cached + } + removed, added := normalizeSuggestion(reviewed, replacement) + removedRanges, addedRanges := suggestionChangedRanges(removed, added) + entry := suggestionRenderCacheEntry{} + for index, source := range removed { + entry.removed = append(entry.removed, wrapSuggestionLine( + path, source, '-', removedRanges[index], width, + )...) + } + for index, source := range added { + entry.added = append(entry.added, wrapSuggestionLine( + path, source, '+', addedRanges[index], width, + )...) + } + return renderedSuggestions.put(key, entry) +} + func parseCommentBody(body string) parsedCommentBody { var ( result parsedCommentBody diff --git a/suggestions_test.go b/suggestions_test.go index 1ab201a..506dd11 100644 --- a/suggestions_test.go +++ b/suggestions_test.go @@ -85,6 +85,27 @@ func TestDetailRendersSuggestionAsRemovalAndAddition(t *testing.T) { } } +func TestRenderedSuggestionsAreCachedWithoutSharingMutableLines(t *testing.T) { + renderedSuggestions.clear() + reviewed := []string{"old_value = compute()", "return old_value"} + first := renderSuggestion( + "example.py", reviewed, "new_value = compute()\nreturn new_value", 50, + ) + if len(renderedSuggestions.entries) != 1 { + t.Fatalf("suggestion cache entries = %d, want 1", len(renderedSuggestions.entries)) + } + first.removed[0].rail = "mutated" + second := renderSuggestion( + "example.py", reviewed, "new_value = compute()\nreturn new_value", 50, + ) + if second.removed[0].rail != "" { + t.Fatal("caller mutation changed cached suggestion lines") + } + if len(renderedSuggestions.entries) != 1 { + t.Fatalf("cache miss for unchanged suggestion: %d entries", len(renderedSuggestions.entries)) + } +} + func TestSuggestionBackgroundIsDirectionalWithoutTextUnderline(t *testing.T) { removed := suggestionHighlight(" - ", "old", 12, '-') added := suggestionHighlight(" + ", "new", 12, '+') diff --git a/theme.go b/theme.go index 22ce37d..1d2b8a9 100644 --- a/theme.go +++ b/theme.go @@ -102,6 +102,7 @@ func applyTheme(name string, custom ...CustomThemeConfig) error { currentThemeName = name commentMarkdownRenderers.Clear() commentMarkdownLines.clear() + renderedSuggestions.clear() return nil } diff --git a/tui.go b/tui.go index 1373b04..9ee3195 100644 --- a/tui.go +++ b/tui.go @@ -3766,27 +3766,16 @@ func (m App) detailLines(width int) []detailLine { } } for suggestionIndex, suggestion := range content.Suggestions { - removed, added := normalizeSuggestion(reviewedSourceLines(thread, comment), suggestion) - removedRanges, addedRanges := suggestionChangedRanges(removed, added) label := "suggested change" if len(content.Suggestions) > 1 { label = fmt.Sprintf("suggested change %d/%d", suggestionIndex+1, len(content.Suggestions)) } lines = append(lines, detailLine{rail: rail}, detailLine{rail: rail, text: dimStyle.Render(label)}) - for sourceIndex, source := range removed { - lines = append(lines, addCommentRail( - wrapSuggestionLine( - thread.Path, source, '-', removedRanges[sourceIndex], max(1, width-4), - ), rail, - )...) - } - for sourceIndex, source := range added { - lines = append(lines, addCommentRail( - wrapSuggestionLine( - thread.Path, source, '+', addedRanges[sourceIndex], max(1, width-4), - ), rail, - )...) - } + rendered := renderSuggestion( + thread.Path, reviewedSourceLines(thread, comment), suggestion, max(1, width-4), + ) + lines = append(lines, addCommentRail(rendered.removed, rail)...) + lines = append(lines, addCommentRail(rendered.added, rail)...) } for reactionIndex, reactionLine := range renderReactionSummary( comment.Reactions, max(1, width-4), diff --git a/version.go b/version.go index 6e5c57b..a1b48dd 100644 --- a/version.go +++ b/version.go @@ -1,3 +1,3 @@ package main -const dipleVersion = "0.3.1" +const dipleVersion = "0.3.2"