Something went wrong. Try again.
forked from charmbracelet/soft-serve
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240package code
import ( "math" "strings" "sync"
tea "charm.land/bubbletea/v2" "charm.land/glamour/v2" gansi "charm.land/glamour/v2/ansi" "charm.land/lipgloss/v2" "github.com/alecthomas/chroma/v2/lexers" "github.com/charmbracelet/soft-serve/pkg/ui/common" vp "github.com/charmbracelet/soft-serve/pkg/ui/components/viewport")
const ( defaultTabWidth = 4 defaultSideNotePercent = 0.3)
// Code is a code snippet.type Code struct { *vp.Viewport common common.Common sidenote string content string extension string renderContext gansi.RenderContext renderMutex sync.Mutex styleConfig gansi.StyleConfig
SideNotePercent float64 TabWidth int ShowLineNumber bool NoContentStyle lipgloss.Style UseGlamour bool}
// New returns a new Code.func New(c common.Common, content, extension string) *Code { r := &Code{ common: c, content: content, extension: extension, TabWidth: defaultTabWidth, SideNotePercent: defaultSideNotePercent, Viewport: vp.New(c), NoContentStyle: c.Styles.NoContent.SetString("No Content."), } st := common.StyleConfig() r.styleConfig = st r.renderContext = common.StyleRendererWithStyles(st) r.SetSize(c.Width, c.Height) return r}
// SetSize implements common.Component.func (r *Code) SetSize(width, height int) { r.common.SetSize(width, height) r.Viewport.SetSize(width, height)}
// SetContent sets the content of the Code.func (r *Code) SetContent(c, ext string) tea.Cmd { r.content = c r.extension = ext return r.Init()}
// SetSideNote sets the sidenote of the Code.func (r *Code) SetSideNote(s string) tea.Cmd { r.sidenote = s return r.Init()}
// Init implements tea.Model.func (r *Code) Init() tea.Cmd { // XXX: We probably won't need the GetHorizontalFrameSize margin // subtraction if we get the new viewport soft wrapping to play nicely with // Glamour. This also introduces a bug where when it soft wraps, the // viewport scrolls left/right for 2 columns on each side of the screen. w := r.common.Width - r.common.Styles.App.GetHorizontalFrameSize() content := r.content if content == "" { r.Viewport.Model.SetContent(r.NoContentStyle.String()) return nil }
// FIXME chroma & glamour might break wrapping when using tabs since tab // width depends on the terminal. This is a workaround to replace tabs with // 4-spaces. content = strings.ReplaceAll(content, "\t", strings.Repeat(" ", r.TabWidth))
if r.UseGlamour && common.IsFileMarkdown(content, r.extension) { md, err := r.glamourize(w, content) if err != nil { return common.ErrorCmd(err) } content = md } else { f, err := r.renderFile(r.extension, content) if err != nil { return common.ErrorCmd(err) } content = f if r.ShowLineNumber { var ml int content, ml = common.FormatLineNumber(r.common.Styles, content, true) w -= ml } }
if r.sidenote != "" { lines := strings.Split(r.sidenote, "\n") sideNoteWidth := int(math.Ceil(float64(r.Model.Width()) * r.SideNotePercent)) for i, l := range lines { lines[i] = common.TruncateString(l, sideNoteWidth) } content = lipgloss.JoinHorizontal(lipgloss.Top, strings.Join(lines, "\n"), content) }
// Fix styles after hard wrapping // https://github.com/muesli/reflow/issues/43 // // TODO: solve this upstream in Glamour/Reflow. content = lipgloss.NewStyle().Width(w).Render(content)
r.Viewport.Model.SetContent(content)
return nil}
// Update implements tea.Model.func (r *Code) Update(msg tea.Msg) (common.Model, tea.Cmd) { cmds := make([]tea.Cmd, 0) switch msg.(type) { case tea.WindowSizeMsg: // Recalculate content width and line wrap. cmds = append(cmds, r.Init()) } v, cmd := r.Viewport.Update(msg) r.Viewport = v.(*vp.Viewport) if cmd != nil { cmds = append(cmds, cmd) } return r, tea.Batch(cmds...)}
// View implements tea.View.func (r *Code) View() string { return r.Viewport.View()}
// GotoTop moves the viewport to the top of the log.func (r *Code) GotoTop() { r.Viewport.GotoTop()}
// GotoBottom moves the viewport to the bottom of the log.func (r *Code) GotoBottom() { r.Viewport.GotoBottom()}
// HalfViewDown moves the viewport down by half the viewport height.func (r *Code) HalfViewDown() { r.Viewport.HalfViewDown()}
// HalfViewUp moves the viewport up by half the viewport height.func (r *Code) HalfViewUp() { r.Viewport.HalfViewUp()}
// ScrollPercent returns the viewport's scroll percentage.func (r *Code) ScrollPercent() float64 { return r.Viewport.ScrollPercent()}
// ScrollPosition returns the viewport's scroll position.func (r *Code) ScrollPosition() int { scroll := r.ScrollPercent() * 100 if scroll < 0 || math.IsNaN(scroll) { scroll = 0 } return int(scroll)}
func (r *Code) glamourize(w int, md string) (string, error) { r.renderMutex.Lock() defer r.renderMutex.Unlock() if w > 120 { w = 120 } tr, err := glamour.NewTermRenderer( glamour.WithStyles(r.styleConfig), glamour.WithWordWrap(w), ) if err != nil { return "", err } mdt, err := tr.Render(md) if err != nil { return "", err } return mdt, nil}
func (r *Code) renderFile(path, content string) (string, error) { lexer := lexers.Match(path) if path == "" { lexer = lexers.Analyse(content) } lang := "" if lexer != nil && lexer.Config() != nil { lang = lexer.Config().Name }
formatter := &gansi.CodeBlockElement{ Code: content, Language: lang, } s := strings.Builder{} rc := r.renderContext if r.ShowLineNumber { st := common.StyleConfig() var m uint st.CodeBlock.Margin = &m rc = gansi.NewRenderContext(gansi.Options{ Styles: st, }) } err := formatter.Render(&s, rc) if err != nil { return "", err }
return s.String(), nil}