Skip to content

Commit 4699739

Browse files
committed
shitty hack for terrible charm bubbletea performance
1 parent c1d87c3 commit 4699739

File tree

11 files changed

+999
-13
lines changed

11 files changed

+999
-13
lines changed

bun.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/tui/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.0
55
require (
66
github.com/BurntSushi/toml v1.5.0
77
github.com/alecthomas/chroma/v2 v2.18.0
8+
github.com/charmbracelet/bubbles v0.21.0
89
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1
910
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4
1011
github.com/charmbracelet/glamour v0.10.0

packages/tui/go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWp
2020
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
2121
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
2222
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
23+
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
24+
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
2325
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1 h1:swACzss0FjnyPz1enfX56GKkLiuKg5FlyVmOLIlU2kE=
2426
github.com/charmbracelet/bubbles/v2 v2.0.0-beta.1/go.mod h1:6HamsBKWqEC/FVHuQMHgQL+knPyvHH55HwJDHl/adMw=
2527
github.com/charmbracelet/bubbletea/v2 v2.0.0-beta.4 h1:UgUuKKvBwgqm2ZEL+sKv/OLeavrUb4gfHgdxe6oIOno=

packages/tui/internal/components/chat/messages.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ package chat
22

33
import (
44
"fmt"
5+
"log/slog"
56
"strings"
67

7-
"github.com/charmbracelet/bubbles/v2/viewport"
88
tea "github.com/charmbracelet/bubbletea/v2"
99
"github.com/charmbracelet/lipgloss/v2"
1010
"github.com/sst/opencode-sdk-go"
@@ -15,6 +15,7 @@ import (
1515
"github.com/sst/opencode/internal/styles"
1616
"github.com/sst/opencode/internal/theme"
1717
"github.com/sst/opencode/internal/util"
18+
"github.com/sst/opencode/internal/viewport"
1819
)
1920

2021
type MessagesComponent interface {
@@ -99,8 +100,8 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
99100
m.lineCount = msg.lineCount
100101
m.rendering = false
101102
m.loading = false
102-
m.viewport.SetHeight(m.height - lipgloss.Height(m.header))
103-
m.viewport.SetContent(msg.content)
103+
m.tail = m.viewport.AtBottom()
104+
m.viewport = msg.viewport
104105
if m.tail {
105106
m.viewport.GotoBottom()
106107
}
@@ -109,16 +110,16 @@ func (m *messagesComponent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
109110
}
110111
}
111112

113+
m.tail = m.viewport.AtBottom()
112114
viewport, cmd := m.viewport.Update(msg)
113115
m.viewport = viewport
114-
m.tail = m.viewport.AtBottom()
115116
cmds = append(cmds, cmd)
116117

117118
return m, tea.Batch(cmds...)
118119
}
119120

120121
type renderCompleteMsg struct {
121-
content string
122+
viewport viewport.Model
122123
partCount int
123124
lineCount int
124125
}
@@ -127,6 +128,7 @@ func (m *messagesComponent) renderView() tea.Cmd {
127128
m.header = m.renderHeader()
128129

129130
if m.rendering {
131+
slog.Debug("pending render, skipping")
130132
m.dirty = true
131133
return func() tea.Msg {
132134
return nil
@@ -135,6 +137,8 @@ func (m *messagesComponent) renderView() tea.Cmd {
135137
m.dirty = false
136138
m.rendering = true
137139

140+
viewport := m.viewport
141+
138142
return func() tea.Msg {
139143
measure := util.Measure("messages.renderView")
140144
defer measure()
@@ -396,8 +400,11 @@ func (m *messagesComponent) renderView() tea.Cmd {
396400
}
397401

398402
content := "\n" + strings.Join(blocks, "\n\n")
403+
viewport.SetHeight(m.height - lipgloss.Height(m.header))
404+
viewport.SetContent(content)
405+
399406
return renderCompleteMsg{
400-
content: content,
407+
viewport: viewport,
401408
partCount: partCount,
402409
lineCount: lineCount,
403410
}
@@ -562,9 +569,12 @@ func (m *messagesComponent) View() string {
562569
)
563570
}
564571

572+
measure := util.Measure("messages.View")
573+
viewport := m.viewport.View()
574+
measure()
565575
return styles.NewStyle().
566576
Background(t.Background()).
567-
Render(m.header + "\n" + m.viewport.View())
577+
Render(m.header + "\n" + viewport)
568578
}
569579

570580
func (m *messagesComponent) Reload() tea.Cmd {

packages/tui/internal/components/dialog/help.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package dialog
22

33
import (
4-
"github.com/charmbracelet/bubbles/v2/viewport"
54
tea "github.com/charmbracelet/bubbletea/v2"
65
"github.com/sst/opencode/internal/app"
76
commandsComponent "github.com/sst/opencode/internal/components/commands"
87
"github.com/sst/opencode/internal/components/modal"
98
"github.com/sst/opencode/internal/layout"
109
"github.com/sst/opencode/internal/theme"
10+
"github.com/sst/opencode/internal/viewport"
1111
)
1212

1313
type helpDialog struct {

packages/tui/internal/components/fileviewer/fileviewer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"strings"
66

7-
"github.com/charmbracelet/bubbles/v2/viewport"
87
tea "github.com/charmbracelet/bubbletea/v2"
98

109
"github.com/sst/opencode/internal/app"
@@ -15,6 +14,7 @@ import (
1514
"github.com/sst/opencode/internal/styles"
1615
"github.com/sst/opencode/internal/theme"
1716
"github.com/sst/opencode/internal/util"
17+
"github.com/sst/opencode/internal/viewport"
1818
)
1919

2020
type DiffStyle int

packages/tui/internal/tui/tui.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ func (a appModel) Init() tea.Cmd {
103103
}
104104

105105
func (a appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
106+
measure := util.Measure("Update")
107+
defer measure("from", fmt.Sprintf("%T", msg))
108+
106109
var cmd tea.Cmd
107110
var cmds []tea.Cmd
108111

@@ -529,11 +532,13 @@ func (a appModel) View() string {
529532

530533
var mainLayout string
531534

535+
measure := util.Measure("app.View")
532536
if a.app.Session.ID == "" {
533537
mainLayout = a.home()
534538
} else {
535539
mainLayout = a.chat()
536540
}
541+
measure()
537542
mainLayout = styles.NewStyle().
538543
Background(t.Background()).
539544
Padding(0, 2).
@@ -691,6 +696,8 @@ func (a appModel) home() string {
691696
}
692697

693698
func (a appModel) chat() string {
699+
measure := util.Measure("chat.View")
700+
defer measure()
694701
effectiveWidth := a.width - 4
695702
t := theme.CurrentTheme()
696703
editorView := a.editor.View()

packages/tui/internal/util/util.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ func IsWsl() bool {
4040

4141
func Measure(tag string) func(...any) {
4242
startTime := time.Now()
43-
return func(tags ...any) {
44-
args := append([]any{"timeTakenMs", time.Since(startTime).Milliseconds()}, tags...)
43+
return func(args ...any) {
44+
args = append(args, []any{"timeTakenMs", time.Since(startTime).Milliseconds()}...)
4545
slog.Debug(tag, args...)
4646
}
4747
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package viewport
2+
3+
import (
4+
"github.com/charmbracelet/lipgloss/v2"
5+
"github.com/charmbracelet/x/ansi"
6+
"github.com/rivo/uniseg"
7+
)
8+
9+
// parseMatches converts the given matches into highlight ranges.
10+
//
11+
// Assumptions:
12+
// - matches are measured in bytes, e.g. what [regex.FindAllStringIndex] would return
13+
// - matches were made against the given content
14+
// - matches are in order
15+
// - matches do not overlap
16+
// - content is line terminated with \n only
17+
//
18+
// We'll then convert the ranges into [highlightInfo]s, which hold the starting
19+
// line and the grapheme positions.
20+
func parseMatches(
21+
content string,
22+
matches [][]int,
23+
) []highlightInfo {
24+
if len(matches) == 0 {
25+
return nil
26+
}
27+
28+
line := 0
29+
graphemePos := 0
30+
previousLinesOffset := 0
31+
bytePos := 0
32+
33+
highlights := make([]highlightInfo, 0, len(matches))
34+
gr := uniseg.NewGraphemes(ansi.Strip(content))
35+
36+
for _, match := range matches {
37+
byteStart, byteEnd := match[0], match[1]
38+
39+
// hilight for this match:
40+
hi := highlightInfo{
41+
lines: map[int][2]int{},
42+
}
43+
44+
// find the beginning of this byte range, setup current line and
45+
// grapheme position.
46+
for byteStart > bytePos {
47+
if !gr.Next() {
48+
break
49+
}
50+
if content[bytePos] == '\n' {
51+
previousLinesOffset = graphemePos + 1
52+
line++
53+
}
54+
graphemePos += max(1, gr.Width())
55+
bytePos += len(gr.Str())
56+
}
57+
58+
hi.lineStart = line
59+
hi.lineEnd = line
60+
61+
graphemeStart := graphemePos
62+
63+
// loop until we find the end
64+
for byteEnd > bytePos {
65+
if !gr.Next() {
66+
break
67+
}
68+
69+
// if it ends with a new line, add the range, increase line, and continue
70+
if content[bytePos] == '\n' {
71+
colstart := max(0, graphemeStart-previousLinesOffset)
72+
colend := max(graphemePos-previousLinesOffset+1, colstart) // +1 its \n itself
73+
74+
if colend > colstart {
75+
hi.lines[line] = [2]int{colstart, colend}
76+
hi.lineEnd = line
77+
}
78+
79+
previousLinesOffset = graphemePos + 1
80+
line++
81+
}
82+
83+
graphemePos += max(1, gr.Width())
84+
bytePos += len(gr.Str())
85+
}
86+
87+
// we found it!, add highlight and continue
88+
if bytePos == byteEnd {
89+
colstart := max(0, graphemeStart-previousLinesOffset)
90+
colend := max(graphemePos-previousLinesOffset, colstart)
91+
92+
if colend > colstart {
93+
hi.lines[line] = [2]int{colstart, colend}
94+
hi.lineEnd = line
95+
}
96+
}
97+
98+
highlights = append(highlights, hi)
99+
}
100+
101+
return highlights
102+
}
103+
104+
type highlightInfo struct {
105+
// in which line this highlight starts and ends
106+
lineStart, lineEnd int
107+
108+
// the grapheme highlight ranges for each of these lines
109+
lines map[int][2]int
110+
}
111+
112+
// coords returns the line x column of this highlight.
113+
func (hi highlightInfo) coords() (int, int, int) {
114+
for i := hi.lineStart; i <= hi.lineEnd; i++ {
115+
hl, ok := hi.lines[i]
116+
if !ok {
117+
continue
118+
}
119+
return i, hl[0], hl[1]
120+
}
121+
return hi.lineStart, 0, 0
122+
}
123+
124+
func makeHighlightRanges(
125+
highlights []highlightInfo,
126+
line int,
127+
style lipgloss.Style,
128+
) []lipgloss.Range {
129+
result := []lipgloss.Range{}
130+
for _, hi := range highlights {
131+
lihi, ok := hi.lines[line]
132+
if !ok {
133+
continue
134+
}
135+
if lihi == [2]int{} {
136+
continue
137+
}
138+
result = append(result, lipgloss.NewRange(lihi[0], lihi[1], style))
139+
}
140+
return result
141+
}

0 commit comments

Comments
 (0)