1package code
  2
  3import (
  4	"strings"
  5
  6	"github.com/alecthomas/chroma/lexers"
  7	tea "github.com/charmbracelet/bubbletea"
  8	"github.com/charmbracelet/glamour"
  9	gansi "github.com/charmbracelet/glamour/ansi"
 10	"github.com/charmbracelet/lipgloss"
 11	"github.com/charmbracelet/soft-serve/ui/common"
 12	vp "github.com/charmbracelet/soft-serve/ui/components/viewport"
 13	"github.com/muesli/reflow/wrap"
 14	"github.com/muesli/termenv"
 15)
 16
 17// Code is a code snippet.
 18type Code struct {
 19	common         common.Common
 20	content        string
 21	extension      string
 22	viewport       *vp.Viewport
 23	NoContentStyle lipgloss.Style
 24}
 25
 26// New returns a new Code.
 27func New(c common.Common, content, extension string) *Code {
 28	r := &Code{
 29		common:         c,
 30		content:        content,
 31		extension:      extension,
 32		viewport:       vp.New(),
 33		NoContentStyle: c.Styles.CodeNoContent.Copy(),
 34	}
 35	r.SetSize(c.Width, c.Height)
 36	return r
 37}
 38
 39// SetSize implements common.Component.
 40func (r *Code) SetSize(width, height int) {
 41	r.common.SetSize(width, height)
 42	r.viewport.SetSize(width, height)
 43}
 44
 45// SetContent sets the content of the Code.
 46func (r *Code) SetContent(c, ext string) tea.Cmd {
 47	r.content = c
 48	r.extension = ext
 49	return r.Init()
 50}
 51
 52// GotoTop reset the viewport to the top.
 53func (r *Code) GotoTop() {
 54	r.viewport.Viewport.GotoTop()
 55}
 56
 57// Init implements tea.Model.
 58func (r *Code) Init() tea.Cmd {
 59	w := r.common.Width
 60	c := r.content
 61	if c == "" {
 62		c = r.NoContentStyle.String()
 63	}
 64	f, err := renderFile(r.extension, c, w)
 65	if err != nil {
 66		return common.ErrorCmd(err)
 67	}
 68	// FIXME: this is a hack to reset formatting at the end of every line.
 69	c = wrap.String(f, w)
 70	s := strings.Split(c, "\n")
 71	for i, l := range s {
 72		s[i] = l + "\x1b[0m"
 73	}
 74	r.viewport.Viewport.SetContent(strings.Join(s, "\n"))
 75	return nil
 76}
 77
 78// Update implements tea.Model.
 79func (r *Code) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 80	cmds := make([]tea.Cmd, 0)
 81	switch msg.(type) {
 82	case tea.WindowSizeMsg:
 83		// Recalculate content width and line wrap.
 84		cmds = append(cmds, r.Init())
 85	}
 86	v, cmd := r.viewport.Update(msg)
 87	r.viewport = v.(*vp.Viewport)
 88	if cmd != nil {
 89		cmds = append(cmds, cmd)
 90	}
 91	return r, tea.Batch(cmds...)
 92}
 93
 94// View implements tea.View.
 95func (r *Code) View() string {
 96	return r.viewport.View()
 97}
 98
 99func styleConfig() gansi.StyleConfig {
100	noColor := ""
101	s := glamour.DarkStyleConfig
102	s.Document.StylePrimitive.Color = &noColor
103	s.CodeBlock.Chroma.Text.Color = &noColor
104	s.CodeBlock.Chroma.Name.Color = &noColor
105	return s
106}
107
108func renderCtx() gansi.RenderContext {
109	return gansi.NewRenderContext(gansi.Options{
110		ColorProfile: termenv.TrueColor,
111		Styles:       styleConfig(),
112	})
113}
114
115func glamourize(w int, md string) (string, error) {
116	if w > 120 {
117		w = 120
118	}
119	tr, err := glamour.NewTermRenderer(
120		glamour.WithStyles(styleConfig()),
121		glamour.WithWordWrap(w),
122	)
123
124	if err != nil {
125		return "", err
126	}
127	mdt, err := tr.Render(md)
128	if err != nil {
129		return "", err
130	}
131	return mdt, nil
132}
133
134func renderFile(path, content string, width int) (string, error) {
135	lexer := lexers.Fallback
136	if path == "" {
137		lexer = lexers.Analyse(content)
138	} else {
139		lexer = lexers.Match(path)
140	}
141	lang := ""
142	if lexer != nil && lexer.Config() != nil {
143		lang = lexer.Config().Name
144	}
145	if lang == "markdown" {
146		md, err := glamourize(width, content)
147		if err != nil {
148			return "", err
149		}
150		return md, nil
151	}
152	formatter := &gansi.CodeBlockElement{
153		Code:     content,
154		Language: lang,
155	}
156	r := strings.Builder{}
157	err := formatter.Render(&r, renderCtx())
158	if err != nil {
159		return "", err
160	}
161	return r.String(), nil
162}