diff --git a/ui/components/code/code.go b/ui/components/code/code.go index 0df64c5cbe53a5be7aacb2e0213aefad62c23bb8..fa05614dc2968a7b810f2c7d3a22473b12836ccc 100644 --- a/ui/components/code/code.go +++ b/ui/components/code/code.go @@ -1,6 +1,7 @@ package code import ( + "fmt" "strings" "sync" @@ -15,6 +16,11 @@ import ( "github.com/muesli/termenv" ) +var ( + lineDigitStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("239")) + lineBarStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("236")) +) + // Code is a code snippet. type Code struct { *vp.Viewport @@ -24,7 +30,11 @@ type Code struct { renderContext gansi.RenderContext renderMutex sync.Mutex styleConfig gansi.StyleConfig + showLineNumber bool + NoContentStyle lipgloss.Style + LineDigitStyle lipgloss.Style + LineBarStyle lipgloss.Style } // New returns a new Code. @@ -35,6 +45,8 @@ func New(c common.Common, content, extension string) *Code { extension: extension, Viewport: vp.New(c), NoContentStyle: c.Styles.CodeNoContent.Copy(), + LineDigitStyle: lineDigitStyle, + LineBarStyle: lineBarStyle, } st := common.StyleConfig() r.styleConfig = st @@ -46,6 +58,11 @@ func New(c common.Common, content, extension string) *Code { return r } +// SetShowLineNumber sets whether to show line numbers. +func (r *Code) SetShowLineNumber(show bool) { + r.showLineNumber = show +} + // SetSize implements common.Component. func (r *Code) SetSize(width, height int) { r.common.SetSize(width, height) @@ -64,12 +81,16 @@ func (r *Code) Init() tea.Cmd { w := r.common.Width c := r.content if c == "" { - c = r.NoContentStyle.String() + r.Viewport.Model.SetContent(r.NoContentStyle.String()) + return nil } f, err := r.renderFile(r.extension, c, w) if err != nil { return common.ErrorCmd(err) } + if r.showLineNumber { + f = withLineNumber(f) + } // FIXME: this is a hack to reset formatting at the end of every line. c = wrap.String(f, w) s := strings.Split(c, "\n") @@ -196,3 +217,23 @@ func (r *Code) renderFile(path, content string, width int) (string, error) { } return s.String(), nil } + +func withLineNumber(s string) string { + lines := strings.Split(s, "\n") + // NB: len() is not a particularly safe way to count string width (because + // it's counting bytes instead of runes) but in this case it's okay + // because we're only dealing with digits, which are one byte each. + mll := len(fmt.Sprintf("%d", len(lines))) + for i, l := range lines { + digit := fmt.Sprintf("%*d", mll, i+1) + bar := "│" + digit = lineDigitStyle.Render(digit) + bar = lineBarStyle.Render(bar) + if i < len(lines)-1 || len(l) != 0 { + // If the final line was a newline we'll get an empty string for + // the final line, so drop the newline altogether. + lines[i] = fmt.Sprintf(" %s %s %s", digit, bar, l) + } + } + return strings.Join(lines, "\n") +} diff --git a/ui/pages/repo/files.go b/ui/pages/repo/files.go index 3e36fad0762afe5020f53f2699af3e8e05c3bed8..9ba0845668c4a289ea045771acac6d274cfe57e8 100644 --- a/ui/pages/repo/files.go +++ b/ui/pages/repo/files.go @@ -28,6 +28,13 @@ var ( errInvalidFile = errors.New("invalid file") ) +var ( + lineNo = key.NewBinding( + key.WithKeys("l"), + key.WithHelp("l", "toggle line numbers"), + ) +) + // FileItemsMsg is a message that contains a list of files. type FileItemsMsg []selector.IdentifiableItem @@ -49,6 +56,7 @@ type Files struct { currentItem *FileItem currentContent FileContentMsg lastSelected []int + lineNumber bool } // NewFiles creates a new files model. @@ -58,6 +66,7 @@ func NewFiles(common common.Common) *Files { code: code.New(common, "", ""), activeView: filesViewFiles, lastSelected: make([]int, 0), + lineNumber: true, } selector := selector.New(common, []selector.IdentifiableItem{}, FileItemDelegate{&common}) selector.SetShowFilter(false) @@ -70,6 +79,7 @@ func NewFiles(common common.Common) *Files { selector.KeyMap.NextPage = common.KeyMap.NextPage selector.KeyMap.PrevPage = common.KeyMap.PrevPage f.selector = selector + f.code.SetShowLineNumber(f.lineNumber) return f } @@ -101,6 +111,7 @@ func (f *Files) ShortHelp() []key.Binding { f.common.KeyMap.UpDown, f.common.KeyMap.BackItem, copyKey, + lineNo, } default: return []key.Binding{} @@ -158,6 +169,7 @@ func (f *Files) FullHelp() [][]key.Binding { }, { copyKey, + lineNo, }, }...) } @@ -222,6 +234,10 @@ func (f *Files) Update(msg tea.Msg) (tea.Model, tea.Cmd) { cmds = append(cmds, f.deselectItemCmd) case key.Matches(msg, f.common.KeyMap.Copy): f.common.Copy.Copy(f.currentContent.content) + case key.Matches(msg, lineNo): + f.lineNumber = !f.lineNumber + f.code.SetShowLineNumber(f.lineNumber) + cmds = append(cmds, f.code.SetContent(f.currentContent.content, f.currentContent.ext)) } } case tea.WindowSizeMsg: