perf(chat): render each message once, drop redundant embed parse
This commit is contained in:
parent
5fae84533d
commit
fab3ae2812
4 changed files with 41 additions and 27 deletions
2
go.mod
2
go.mod
|
|
@ -11,7 +11,7 @@ require (
|
|||
github.com/alecthomas/chroma/v2 v2.24.1
|
||||
github.com/andybalholm/brotli v1.2.1
|
||||
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97
|
||||
github.com/ayn2op/tview v0.0.0-20260505203159-135c30590e98
|
||||
github.com/ayn2op/tview v0.0.0-20260518023109-03ba9fc985b3
|
||||
github.com/deckarep/gosx-notifier v0.0.0-20180201035817-e127226297fb
|
||||
github.com/diamondburned/arikawa/v3 v3.6.1-0.20260311205148-176ad9b9440f
|
||||
github.com/diamondburned/ningen/v3 v3.0.1-0.20260306213430-5a08d3a709b4
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -14,8 +14,8 @@ github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eT
|
|||
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97 h1:WujETUV+v0DEJyZgjeLzQvihWyL80c0Tg4qf0dDo+Io=
|
||||
github.com/ayn2op/clipboard v0.0.0-20260308203959-c5ad7df3fc97/go.mod h1:3kFnpNCa3dF6WryzOMCDao7PfZ7DTCh+pievlfuwV80=
|
||||
github.com/ayn2op/tview v0.0.0-20260505203159-135c30590e98 h1:C1b9nZC/O5v1pg46NAQJaudWA9xKk7FWGcylfxaDL7s=
|
||||
github.com/ayn2op/tview v0.0.0-20260505203159-135c30590e98/go.mod h1:K4qMjKIPIYbOM7aP7dXFRdTrAtVbpWPPT8Q93+DBfKI=
|
||||
github.com/ayn2op/tview v0.0.0-20260518023109-03ba9fc985b3 h1:C0TqOk1z+HozivyiXEvZsD1mpztI15thXFZzxHumTEo=
|
||||
github.com/ayn2op/tview v0.0.0-20260518023109-03ba9fc985b3/go.mod h1:iOI0nEHAzgBFbED6yCJ5k8ID1Zd2R9QSXW6S2JE27Nk=
|
||||
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ func newMentionsList(cfg *config.Config) *mentionsList {
|
|||
m := &mentionsList{
|
||||
Model: list.NewModel(),
|
||||
}
|
||||
m.SetSelectedStyle(tcell.StyleDefault.Reverse(true))
|
||||
m.SetKeybinds(list.Keybinds{
|
||||
SelectUp: cfg.Keybinds.MentionsList.Up.Keybind,
|
||||
SelectDown: cfg.Keybinds.MentionsList.Down.Keybind,
|
||||
|
|
@ -50,16 +51,13 @@ func (m *mentionsList) clear() {
|
|||
}
|
||||
|
||||
func (m *mentionsList) rebuild() {
|
||||
m.SetBuilder(func(index int, cursor int) list.Item {
|
||||
m.SetBuilder(func(index int) list.Item {
|
||||
if index < 0 || index >= len(m.items) {
|
||||
return nil
|
||||
}
|
||||
|
||||
item := m.items[index]
|
||||
style := item.style
|
||||
if index == cursor {
|
||||
style = style.Reverse(true)
|
||||
}
|
||||
line := tview.NewLine(tview.NewSegment(item.displayText, style))
|
||||
|
||||
return tview.NewTextView().
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ type messagesList struct {
|
|||
rowsDirty bool
|
||||
|
||||
renderer *markdown.Renderer
|
||||
// itemByID caches unselected message TextViews.
|
||||
// itemByID caches rendered message TextViews.
|
||||
itemByID map[discord.MessageID]*tview.TextView
|
||||
|
||||
attachmentsPicker *attachmentsPicker
|
||||
|
|
@ -95,6 +95,7 @@ func newMessagesList(cfg *config.Config, chat *Model) *messagesList {
|
|||
ml.SetBuilder(ml.buildItem)
|
||||
ml.SetChangedFunc(ml.onRowCursorChanged)
|
||||
ml.SetTrackEnd(true)
|
||||
ml.SetSelectedStyle(cfg.Theme.MessagesList.SelectedMessageStyle.Style)
|
||||
ml.SetKeybinds(list.Keybinds{
|
||||
ScrollUp: cfg.Keybinds.MessagesList.ScrollUp.Keybind,
|
||||
ScrollDown: cfg.Keybinds.MessagesList.ScrollDown.Keybind,
|
||||
|
|
@ -169,7 +170,7 @@ func (ml *messagesList) clearSelection() {
|
|||
ml.SetCursor(-1)
|
||||
}
|
||||
|
||||
func (ml *messagesList) buildItem(index int, cursor int) list.Item {
|
||||
func (ml *messagesList) buildItem(index int) list.Item {
|
||||
ml.ensureRows()
|
||||
|
||||
if index < 0 || index >= len(ml.rows) {
|
||||
|
|
@ -181,14 +182,10 @@ func (ml *messagesList) buildItem(index int, cursor int) list.Item {
|
|||
return ml.buildSeparatorItem(row.timestamp)
|
||||
}
|
||||
|
||||
// The list applies the selection style at draw time, so a message is
|
||||
// rendered once and reused for every cursor position. Cursor moves no
|
||||
// longer re-parse markdown or re-run syntax highlighting.
|
||||
message := ml.messages[row.messageIndex]
|
||||
if index == cursor {
|
||||
return tview.NewTextView().
|
||||
SetWrap(true).
|
||||
SetWordWrap(true).
|
||||
SetLines(ml.renderMessage(message, ml.cfg.Theme.MessagesList.SelectedMessageStyle.Style))
|
||||
}
|
||||
|
||||
item, ok := ml.itemByID[message.ID]
|
||||
if !ok {
|
||||
item = tview.NewTextView().
|
||||
|
|
@ -416,8 +413,12 @@ func (ml *messagesList) memberForMessage(message discord.Message) *discord.Membe
|
|||
return member
|
||||
}
|
||||
|
||||
func (ml *messagesList) drawContent(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
||||
lines, root := ml.renderContentLines(message, baseStyle)
|
||||
// drawContent renders the message body and returns the parsed markdown AST
|
||||
// together with the source bytes it indexes into, so callers can reuse them
|
||||
// instead of re-parsing the same content (see drawEmbeds). root is nil when
|
||||
// markdown rendering is disabled.
|
||||
func (ml *messagesList) drawContent(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) (ast.Node, []byte) {
|
||||
lines, root, source := ml.renderContentLines(message, baseStyle)
|
||||
if ml.cfg.Markdown.Enabled && builder.HasCurrentLine() {
|
||||
startsWithCodeBlock := false
|
||||
if root != nil {
|
||||
|
|
@ -439,23 +440,24 @@ func (ml *messagesList) drawContent(builder *tview.LineBuilder, message discord.
|
|||
}
|
||||
}
|
||||
builder.AppendLines(lines)
|
||||
return root, source
|
||||
}
|
||||
|
||||
func (ml *messagesList) renderContentLines(message discord.Message, baseStyle tcell.Style) ([]tview.Line, ast.Node) {
|
||||
func (ml *messagesList) renderContentLines(message discord.Message, baseStyle tcell.Style) ([]tview.Line, ast.Node, []byte) {
|
||||
return ml.renderContentLinesWithMarkdown(message, baseStyle, false)
|
||||
}
|
||||
|
||||
func (ml *messagesList) renderContentLinesWithMarkdown(message discord.Message, baseStyle tcell.Style, forceMarkdown bool) ([]tview.Line, ast.Node) {
|
||||
func (ml *messagesList) renderContentLinesWithMarkdown(message discord.Message, baseStyle tcell.Style, forceMarkdown bool) ([]tview.Line, ast.Node, []byte) {
|
||||
// Keep one rendering path for both normal messages and embed fragments so we preserve mention/link parsing behavior consistently across both.
|
||||
if forceMarkdown || ml.cfg.Markdown.Enabled {
|
||||
c := []byte(message.Content)
|
||||
root := discordmd.ParseWithMessage(c, *ml.chat.state.Cabinet, &message, false)
|
||||
return ml.renderer.RenderLines(c, root, baseStyle), root
|
||||
return ml.renderer.RenderLines(c, root, baseStyle), root, c
|
||||
}
|
||||
|
||||
b := tview.NewLineBuilder()
|
||||
b.Write(message.Content, baseStyle)
|
||||
return b.Finish(), nil
|
||||
return b.Finish(), nil, nil
|
||||
}
|
||||
|
||||
func (ml *messagesList) drawSnapshotContent(builder *tview.LineBuilder, parent discord.Message, snapshot discord.MessageSnapshotMessage, baseStyle tcell.Style) {
|
||||
|
|
@ -484,14 +486,14 @@ func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message d
|
|||
}
|
||||
|
||||
ml.drawAuthor(builder, message, baseStyle)
|
||||
ml.drawContent(builder, message, baseStyle)
|
||||
contentRoot, contentSource := ml.drawContent(builder, message, baseStyle)
|
||||
|
||||
if message.EditedTimestamp.IsValid() {
|
||||
dimStyle := baseStyle.Dim(true)
|
||||
builder.Write(" (edited)", dimStyle)
|
||||
}
|
||||
|
||||
ml.drawEmbeds(builder, message, baseStyle)
|
||||
ml.drawEmbeds(builder, message, baseStyle, contentRoot, contentSource)
|
||||
|
||||
attachmentStyle := ui.MergeStyle(baseStyle, ml.cfg.Theme.MessagesList.AttachmentStyle.Style)
|
||||
for _, a := range message.Attachments {
|
||||
|
|
@ -504,12 +506,20 @@ func (ml *messagesList) drawDefaultMessage(builder *tview.LineBuilder, message d
|
|||
}
|
||||
}
|
||||
|
||||
func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style) {
|
||||
func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.Message, baseStyle tcell.Style, contentRoot ast.Node, contentSource []byte) {
|
||||
if len(message.Embeds) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
contentListURLs := extractURLs(message.Content)
|
||||
// Embed URLs are deduplicated against links already shown in the message
|
||||
// body. Reuse the body's parsed AST when markdown is enabled; only the
|
||||
// markdown-disabled path (no AST) has to parse the content here.
|
||||
var contentListURLs []string
|
||||
if contentRoot != nil {
|
||||
contentListURLs = urlsFromAST(contentRoot, contentSource)
|
||||
} else {
|
||||
contentListURLs = extractURLs(message.Content)
|
||||
}
|
||||
contentURLs := make(map[string]struct{}, len(contentListURLs))
|
||||
for _, u := range contentListURLs {
|
||||
contentURLs[u] = struct{}{}
|
||||
|
|
@ -544,7 +554,7 @@ func (ml *messagesList) drawEmbeds(builder *tview.LineBuilder, message discord.M
|
|||
msg.Content = line.Text
|
||||
lineStyle := lineStyles[line.Kind]
|
||||
// Embed descriptions are always markdown-rendered to match Discord's rich embed semantics, even when message markdown is globally disabled.
|
||||
rendered, _ := ml.renderContentLinesWithMarkdown(msg, lineStyle, line.Kind == embedLineDescription)
|
||||
rendered, _, _ := ml.renderContentLinesWithMarkdown(msg, lineStyle, line.Kind == embedLineDescription)
|
||||
for _, renderedLine := range rendered {
|
||||
if line.URL != "" {
|
||||
renderedLine = lineWithURL(renderedLine, line.URL)
|
||||
|
|
@ -1096,7 +1106,13 @@ func extractURLs(content string) []string {
|
|||
parser.WithBlockParsers(discordmd.BlockParsers()...),
|
||||
parser.WithInlineParsers(discordmd.InlineParserWithLink()...),
|
||||
).Parse(text.NewReader(src))
|
||||
return urlsFromAST(node, src)
|
||||
}
|
||||
|
||||
// urlsFromAST collects link destinations from an already-parsed markdown AST.
|
||||
// src must be the byte slice the node was parsed from (AutoLink resolves its
|
||||
// URL against it).
|
||||
func urlsFromAST(node ast.Node, src []byte) []string {
|
||||
var urls []string
|
||||
ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
if entering {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue