reader: wide tables scroll sideways from the keys

`zl` / `zh` scroll the table, code block or formula at the top of the
view a few columns right or left, and `zL` / `zH` by half its width, as
vim scrolls a long line. It is the first one too wide for the column that
reaches down past the top of the reading view, so it is the one being
read. A CSV's table in the file pane answers the same keys. Before, those
only scrolled with the pointer or a trackpad.
This commit is contained in:
nvrl committed 2026-10-20 15:35:29 +02:00
1 parent 811e09416f
commit e728d468cb
7 files changed
+46 -6

No files matched your search

+7 -1
View File
@@ -713,13 +713,19 @@
void runCommand('d')
return
}
if (previous === 'z' && ['h', 'l', 'H', 'L'].includes(key)) {
event.preventDefault()
const amount = ({ h: -1, l: 1, H: 'halfleft', L: 'halfright' } as const)[key as 'h' | 'l' | 'H' | 'L']
if (!reading || !hooks.scrollWide(amount)) workspace.say('nothing wider than the column in view', 'error')
return
}
if ((previous === ']' && key === ']') || (previous === '[' && key === '[')) {
event.preventDefault()
// Past the last heading there is still the end, as in vim.
if (reading && !hooks.jumpHeading(key === ']' ? 1 : -1)) hooks.scrollReader(key === ']' ? 'bottom' : 'top')
return
}
if (key === 'g' || key === 'd' || key === ']' || key === '[') {
if (key === 'g' || key === 'd' || key === ']' || key === '[' || key === 'z') {
pending = key
return
}
+5 -1
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import { followLink, hooks, runCommand, type ScrollAmount } from './commands'
import { followLink, hooks, runCommand, type ScrollAmount, type SidewaysAmount } from './commands'
import { renderMarkdown } from './markdown'
import Outline from './Outline.svelte'
import { workspace, type Tile } from './store.svelte'
@@ -13,6 +13,7 @@
readerHeadings,
relative,
scrollPage,
scrollWide,
} from './util'
/** The tile this pane draws; the keys only reach it while it has focus. */
@@ -81,13 +82,16 @@
const list = () => readerHeadings(scroller, root)
// The outline in the margin is links too, so `F` reaches it as well.
const hints = () => scroller
const sideways = (amount: SidewaysAmount) => scrollWide(scroller, root, amount)
hooks.jumpHeading = jump
hooks.headings = list
hooks.hintRoot = hints
hooks.scrollWide = sideways
return () => {
if (hooks.jumpHeading === jump) hooks.jumpHeading = () => false
if (hooks.headings === list) hooks.headings = () => ({ headings: [], current: 0 })
if (hooks.hintRoot === hints) hooks.hintRoot = () => null
if (hooks.scrollWide === sideways) hooks.scrollWide = () => false
}
})
+6 -2
View File
@@ -1,11 +1,11 @@
<script lang="ts">
import CertView from './CertView.svelte'
import { hooks, runCommand, type ScrollAmount } from './commands'
import { hooks, runCommand, type ScrollAmount, type SidewaysAmount } from './commands'
import Icon from './Icon.svelte'
import { renderMarkdown } from './markdown'
import { looksLikeText, parseTable, previewKind, separatorOf } from './preview'
import { workspace, type Tile } from './store.svelte'
import { formatBytes, formatStamp, headingsIn, jumpHeading, readerHeadings, relative, scrollPage } from './util'
import { formatBytes, formatStamp, headingsIn, jumpHeading, readerHeadings, relative, scrollPage, scrollWide } from './util'
/** The tile this pane draws; the keys only reach it while it has focus. */
const { tile }: { tile: Tile } = $props()
@@ -91,6 +91,8 @@
return cert?.act() ?? false
}
const hints = () => reader ?? null
// A CSV's table or a long line of text is what runs off the side here.
const sideways = (amount: SidewaysAmount) => !!reader && scrollWide(reader, reader, amount)
const flip = () => {
if (kind !== 'markdown' || text === null) return false
source = !source
@@ -100,7 +102,9 @@
hooks.fileAction = act
hooks.hintRoot = hints
hooks.toggleSource = flip
hooks.scrollWide = sideways
return () => {
if (hooks.scrollWide === sideways) hooks.scrollWide = () => false
if (hooks.scrollReader === scroll) hooks.scrollReader = () => false
if (hooks.fileAction === act) hooks.fileAction = () => false
if (hooks.hintRoot === hints) hooks.hintRoot = () => null
+1
View File
@@ -16,6 +16,7 @@
['j / k · ↓ ↑', 'move down / up the list — scroll the document when it has focus'],
['gg / G', 'first / last row, or top / bottom of the document — a first g, d, ] or [ waits a second in the status line'],
['Ctrl-D / Ctrl-U', 'half a page down / up the open document'],
['zl / zh · zL / zH', 'the wide table or code block at the top of the view, sideways — a little, or half its width'],
[']] / [[', 'next / previous heading of the document — in the editor too'],
['gO', 'the headings as a list: j/k, Enter jumps there'],
['F', 'hints over the links and buttons in view — type one to follow it'],
+5 -2
View File
@@ -1,9 +1,9 @@
import { tick } from 'svelte'
import { api, ApiError, isContainer, type BackupLogin, type BackupStatus } from './api'
import { workspace } from './store.svelte'
import { formatDay, parseDay, parseDue, relative, sealedNote, type Heading, type ScrollAmount } from './util'
import { formatDay, parseDay, parseDue, relative, sealedNote, type Heading, type ScrollAmount, type SidewaysAmount } from './util'
export type { ScrollAmount }
export type { ScrollAmount, SidewaysAmount }
/** Filled in by App.svelte so ex-commands can reach the DOM-bound bits. */
export const hooks = {
@@ -21,6 +21,9 @@ export const hooks = {
/* Filled in while a document is on screen and not being edited: the reading
pane owns the scrollback, so the list keys have somewhere to send it. */
scrollReader: (_amount: ScrollAmount): boolean => false,
/* `zh` / `zl`: the wide table or code block at the top of the view,
sideways; false when nothing on screen is wider than the column. */
scrollWide: (_amount: SidewaysAmount): boolean => false,
/* `F`: what holds the links and buttons on screen, null when nothing does. */
hintRoot: (): HTMLElement | null => null,
/* Filled in by the file pane: what Enter does there, false when nothing. */
+21
View File
@@ -131,6 +131,27 @@ export function scrollPage(el: HTMLElement, amount: ScrollAmount) {
}
}
/** `zl` / `zh` a few columns, `zL` / `zH` half a width, as vim scrolls sideways. */
export type SidewaysAmount = 1 | -1 | 'halfright' | 'halfleft'
/**
* Scroll whatever is too wide for the column — a table, a code block, a
* formula — sideways: the first one that reaches down past the top of the
* view, the one being read. False when there is none on screen.
*/
export function scrollWide(scroller: HTMLElement, root: HTMLElement, amount: SidewaysAmount): boolean {
const view = scroller.getBoundingClientRect()
const wide = [...root.querySelectorAll<HTMLElement>('table, pre, .katex-display, .table')].find((el) => {
if (el.scrollWidth <= el.clientWidth + 1) return false
const box = el.getBoundingClientRect()
return box.bottom > view.top + 8 && box.top < view.bottom
})
if (!wide) return false
const step = typeof amount === 'number' ? amount * 80 : (amount === 'halfright' ? 1 : -1) * (wide.clientWidth / 2)
wide.scrollBy({ left: step })
return true
}
/** A heading to jump to: the reader's, or a line of the text being written. */
export interface Heading {
text: string