Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ Preview panel (panel 5).
| `j/k` | Scroll content |
| `g/G` | Jump to top/bottom |
| `L` | Go to line |
| `t` | Jump to markdown heading |
| `PgDn/PgUp` | Page down/up |
| `W` | Toggle word wrap |
| `n` | Toggle line numbers |
Expand Down
1 change: 1 addition & 0 deletions internal/keybindings/keybindings.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
{ "key": "j/k", "action": "Scroll content" },
{ "key": "g/G", "action": "Jump to top/bottom" },
{ "key": "L", "action": "Go to line" },
{ "key": "t", "action": "Jump to markdown heading" },
{ "key": "PgDn/PgUp", "action": "Page down/up" },
{ "key": "W", "action": "Toggle word wrap" },
{ "key": "n", "action": "Toggle line numbers" },
Expand Down
6 changes: 4 additions & 2 deletions internal/panels/preview/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const (
keyDown = "down"
keyEsc = "esc"
keyEscape = "escape"
keyEnter = "enter"
keyEnd = "end"

// clipboardTimeout bounds how long clipboard subprocess calls may block.
clipboardTimeout = 2 * time.Second
Expand Down Expand Up @@ -283,7 +285,7 @@ func handleEditKeyPress(p *Preview, msg tea.KeyPressMsg) (panels.Panel, tea.Cmd)
ensureCursorVisible(p)
}

case "enter":
case keyEnter:
if p.editBuf != nil {
p.editBuf.SplitLine(p.cursorLine, p.cursorCol, p.editCfg.AutoIndent)
p.cursorLine++
Expand Down Expand Up @@ -551,7 +553,7 @@ func handleEditKeyPress(p *Preview, msg tea.KeyPressMsg) (panels.Panel, tea.Cmd)
clearEditSelection(p)
p.cursorCol = 0

case "end", "ctrl+e": //nolint:goconst // key name, not a magic string
case keyEnd, "ctrl+e":
clearEditSelection(p)
if p.editBuf != nil {
line := p.editBuf.Line(p.cursorLine)
Expand Down
27 changes: 26 additions & 1 deletion internal/panels/preview/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@ type Preview struct {
// as a line-number entry and scrolls to the entered line on Enter.
gotoLineActive bool
gotoLineInput string
// Markdown heading-jump overlay state. mdHeadings is populated on load for
// markdown files; tocTargets holds the resolved display line per heading
// while the overlay is open.
tocActive bool
tocCursor int
mdHeadings []tocHeading
tocTargets []int
// GitHub content mode – when a GitHub item (issue/PR/action run) is
// selected, the preview shows the item detail instead of a file.
ghMode bool // true when showing GitHub content instead of file
Expand Down Expand Up @@ -96,6 +103,7 @@ type fileLoadedMsg struct {
err error
path string
lines []string
headings []tocHeading
isBinary bool
isLarge bool
}
Expand Down Expand Up @@ -263,6 +271,10 @@ func (p *Preview) Update(msg tea.Msg) (panels.Panel, tea.Cmd) {
p.blameMode = false
p.blameLines = nil
p.diffLines = nil
p.tocActive = false
p.tocCursor = 0
p.mdHeadings = nil
p.tocTargets = nil
cmds := []tea.Cmd{p.loadFileCmd(msg.Path)}
if p.gitClient != nil {
cmds = append(cmds, p.loadContextDiffCmd(msg.Path, p.diffContext))
Expand All @@ -273,6 +285,7 @@ func (p *Preview) Update(msg tea.Msg) (panels.Panel, tea.Cmd) {
if msg.path == p.filePath {
p.loading = false
p.lines = msg.lines
p.mdHeadings = msg.headings
p.err = msg.err
p.isBinary = msg.isBinary
p.isLarge = msg.isLarge
Expand Down Expand Up @@ -471,6 +484,10 @@ func (p *Preview) Update(msg tea.Msg) (panels.Panel, tea.Cmd) {
if p.gotoLineActive {
return p.handleGotoLineKey(msg)
}
// The heading-jump overlay captures all input while it is open.
if p.tocActive {
return p.handleTOCKey(msg)
}
switch msg.String() {
case "e":
// Edit is only allowed in file mode (not diff) and for local files.
Expand All @@ -496,6 +513,8 @@ func (p *Preview) Update(msg tea.Msg) (panels.Panel, tea.Cmd) {
p.scrollToBottom()
case "L":
return p, p.openGotoLine()
case "t":
return p, p.openTOC()
case "W":
p.wordWrap = !p.wordWrap
case "n":
Expand Down Expand Up @@ -561,7 +580,7 @@ func (p *Preview) handleGotoLineKey(msg tea.KeyPressMsg) (panels.Panel, tea.Cmd)
switch msg.String() {
case keyEscape, keyEsc:
return p, p.closeGotoLine()
case "enter":
case keyEnter:
p.commitGotoLine()
return p, p.closeGotoLine()
case "backspace":
Expand Down Expand Up @@ -636,6 +655,10 @@ func (p *Preview) View(width, height int) string {
if p.editMode && p.editBuf != nil {
return renderEditContent(p, width, height)
}
// Heading-jump overlay takes over the content area while it is open.
if p.tocActive {
return p.renderTOC(width, height)
}
// Blame mode
if p.blameMode && len(p.blameLines) > 0 {
return p.renderBlameContent(width, height)
Expand Down Expand Up @@ -717,6 +740,7 @@ func (p *Preview) KeyBindings() []panels.KeyBinding {
{Key: "g", Description: "Go to top", Action: "goto_top"},
{Key: "G", Description: "Go to bottom", Action: "goto_bottom"},
{Key: "L", Description: "Go to line", Action: "goto_line"},
{Key: "t", Description: "Jump to heading", Action: "goto_heading"},
{Key: "W", Description: "Toggle word wrap", Action: "toggle_wrap"},
{Key: "n", Description: "Toggle line numbers", Action: "toggle_line_numbers"},
{Key: "m", Description: "Toggle markdown render", Action: "toggle_markdown_render"},
Expand Down Expand Up @@ -794,6 +818,7 @@ func (p *Preview) loadFileCmd(path string) tea.Cmd {
// Render based on file type
switch ext {
case extMD, extMarkdown, extMdown, extMkd:
result.headings = parseMarkdownHeadings(source)
if renderMD {
result.lines = markdown.RenderStatic(source, width)
} else if cfg.GetSyntaxHighlighting() {
Expand Down
3 changes: 2 additions & 1 deletion internal/panels/preview/preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -778,7 +778,7 @@ func TestKeyBindings(t *testing.T) {
bindings := p.KeyBindings()

assert.NotEmpty(t, bindings)
assert.Len(t, bindings, 15)
assert.Len(t, bindings, 16)

// Verify all expected bindings are present
actions := make([]string, len(bindings))
Expand All @@ -795,6 +795,7 @@ func TestKeyBindings(t *testing.T) {
assert.Contains(t, actions, "goto_top")
assert.Contains(t, actions, "goto_bottom")
assert.Contains(t, actions, "goto_line")
assert.Contains(t, actions, "goto_heading")
assert.Contains(t, actions, "toggle_wrap")
assert.Contains(t, actions, "toggle_line_numbers")
assert.Contains(t, actions, "toggle_markdown_render")
Expand Down
Loading
Loading