fileviewer.go 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. package fileviewer
  2. import (
  3. "fmt"
  4. "strings"
  5. "github.com/charmbracelet/bubbles/v2/viewport"
  6. tea "github.com/charmbracelet/bubbletea/v2"
  7. "github.com/sst/opencode/internal/app"
  8. "github.com/sst/opencode/internal/commands"
  9. "github.com/sst/opencode/internal/components/dialog"
  10. "github.com/sst/opencode/internal/components/diff"
  11. "github.com/sst/opencode/internal/layout"
  12. "github.com/sst/opencode/internal/styles"
  13. "github.com/sst/opencode/internal/theme"
  14. "github.com/sst/opencode/internal/util"
  15. )
  16. type DiffStyle int
  17. const (
  18. DiffStyleSplit DiffStyle = iota
  19. DiffStyleUnified
  20. )
  21. type Model struct {
  22. app *app.App
  23. width, height int
  24. viewport viewport.Model
  25. filename *string
  26. content *string
  27. isDiff *bool
  28. diffStyle DiffStyle
  29. }
  30. type fileRenderedMsg struct {
  31. content string
  32. }
  33. func New(app *app.App) Model {
  34. vp := viewport.New()
  35. m := Model{
  36. app: app,
  37. viewport: vp,
  38. diffStyle: DiffStyleUnified,
  39. }
  40. if app.State.SplitDiff {
  41. m.diffStyle = DiffStyleSplit
  42. }
  43. return m
  44. }
  45. func (m Model) Init() tea.Cmd {
  46. return m.viewport.Init()
  47. }
  48. func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
  49. var cmds []tea.Cmd
  50. switch msg := msg.(type) {
  51. case fileRenderedMsg:
  52. m.viewport.SetContent(msg.content)
  53. return m, util.CmdHandler(app.FileRenderedMsg{
  54. FilePath: *m.filename,
  55. })
  56. case dialog.ThemeSelectedMsg:
  57. return m, m.render()
  58. case tea.KeyMsg:
  59. switch msg.String() {
  60. // TODO
  61. }
  62. }
  63. vp, cmd := m.viewport.Update(msg)
  64. m.viewport = vp
  65. cmds = append(cmds, cmd)
  66. return m, tea.Batch(cmds...)
  67. }
  68. func (m Model) View() string {
  69. if !m.HasFile() {
  70. return ""
  71. }
  72. header := *m.filename
  73. header = styles.NewStyle().
  74. Padding(1, 2).
  75. Width(m.width).
  76. Background(theme.CurrentTheme().BackgroundElement()).
  77. Foreground(theme.CurrentTheme().Text()).
  78. Render(header)
  79. t := theme.CurrentTheme()
  80. close := m.app.Key(commands.FileCloseCommand)
  81. diffToggle := m.app.Key(commands.FileDiffToggleCommand)
  82. if m.isDiff == nil || *m.isDiff == false {
  83. diffToggle = ""
  84. }
  85. layoutToggle := m.app.Key(commands.MessagesLayoutToggleCommand)
  86. background := t.Background()
  87. footer := layout.Render(
  88. layout.FlexOptions{
  89. Background: &background,
  90. Direction: layout.Row,
  91. Justify: layout.JustifyCenter,
  92. Align: layout.AlignStretch,
  93. Width: m.width - 2,
  94. Gap: 5,
  95. },
  96. layout.FlexItem{
  97. View: close,
  98. },
  99. layout.FlexItem{
  100. View: layoutToggle,
  101. },
  102. layout.FlexItem{
  103. View: diffToggle,
  104. },
  105. )
  106. footer = styles.NewStyle().Background(t.Background()).Padding(0, 1).Render(footer)
  107. return header + "\n" + m.viewport.View() + "\n" + footer
  108. }
  109. func (m *Model) Clear() (Model, tea.Cmd) {
  110. m.filename = nil
  111. m.content = nil
  112. m.isDiff = nil
  113. return *m, m.render()
  114. }
  115. func (m *Model) ToggleDiff() (Model, tea.Cmd) {
  116. switch m.diffStyle {
  117. case DiffStyleSplit:
  118. m.diffStyle = DiffStyleUnified
  119. default:
  120. m.diffStyle = DiffStyleSplit
  121. }
  122. return *m, m.render()
  123. }
  124. func (m *Model) DiffStyle() DiffStyle {
  125. return m.diffStyle
  126. }
  127. func (m Model) HasFile() bool {
  128. return m.filename != nil && m.content != nil
  129. }
  130. func (m Model) Filename() string {
  131. if m.filename == nil {
  132. return ""
  133. }
  134. return *m.filename
  135. }
  136. func (m *Model) SetSize(width, height int) (Model, tea.Cmd) {
  137. if m.width != width || m.height != height {
  138. m.width = width
  139. m.height = height
  140. m.viewport.SetWidth(width)
  141. m.viewport.SetHeight(height - 4)
  142. return *m, m.render()
  143. }
  144. return *m, nil
  145. }
  146. func (m *Model) SetFile(filename string, content string, isDiff bool) (Model, tea.Cmd) {
  147. m.filename = &filename
  148. m.content = &content
  149. m.isDiff = &isDiff
  150. return *m, m.render()
  151. }
  152. func (m *Model) render() tea.Cmd {
  153. if m.filename == nil || m.content == nil {
  154. m.viewport.SetContent("")
  155. return nil
  156. }
  157. return func() tea.Msg {
  158. t := theme.CurrentTheme()
  159. var rendered string
  160. if m.isDiff != nil && *m.isDiff {
  161. diffResult := ""
  162. var err error
  163. if m.diffStyle == DiffStyleSplit {
  164. diffResult, err = diff.FormatDiff(
  165. *m.filename,
  166. *m.content,
  167. diff.WithWidth(m.width),
  168. )
  169. } else if m.diffStyle == DiffStyleUnified {
  170. diffResult, err = diff.FormatUnifiedDiff(
  171. *m.filename,
  172. *m.content,
  173. diff.WithWidth(m.width),
  174. )
  175. }
  176. if err != nil {
  177. rendered = styles.NewStyle().
  178. Foreground(t.Error()).
  179. Render(fmt.Sprintf("Error rendering diff: %v", err))
  180. } else {
  181. rendered = strings.TrimRight(diffResult, "\n")
  182. }
  183. } else {
  184. rendered = util.RenderFile(
  185. *m.filename,
  186. *m.content,
  187. m.width,
  188. )
  189. }
  190. rendered = styles.NewStyle().
  191. Width(m.width).
  192. Background(t.BackgroundPanel()).
  193. Render(rendered)
  194. return fileRenderedMsg{
  195. content: rendered,
  196. }
  197. }
  198. }
  199. func (m *Model) ScrollTo(line int) {
  200. m.viewport.SetYOffset(line)
  201. }
  202. func (m *Model) ScrollToBottom() {
  203. m.viewport.GotoBottom()
  204. }
  205. func (m *Model) ScrollToTop() {
  206. m.viewport.GotoTop()
  207. }
  208. func (m *Model) PageUp() (Model, tea.Cmd) {
  209. m.viewport.ViewUp()
  210. return *m, nil
  211. }
  212. func (m *Model) PageDown() (Model, tea.Cmd) {
  213. m.viewport.ViewDown()
  214. return *m, nil
  215. }
  216. func (m *Model) HalfPageUp() (Model, tea.Cmd) {
  217. m.viewport.HalfViewUp()
  218. return *m, nil
  219. }
  220. func (m *Model) HalfPageDown() (Model, tea.Cmd) {
  221. m.viewport.HalfViewDown()
  222. return *m, nil
  223. }
  224. func (m Model) AtTop() bool {
  225. return m.viewport.AtTop()
  226. }
  227. func (m Model) AtBottom() bool {
  228. return m.viewport.AtBottom()
  229. }
  230. func (m Model) ScrollPercent() float64 {
  231. return m.viewport.ScrollPercent()
  232. }
  233. func (m Model) TotalLineCount() int {
  234. return m.viewport.TotalLineCount()
  235. }
  236. func (m Model) VisibleLineCount() int {
  237. return m.viewport.VisibleLineCount()
  238. }